MuseScore/mscore/qmledit.cpp
robbie 843e3353f8 fix #249541: PluginCreator improvements.
Improved Help - added a very basic guide and external links.
Very simple autoindent implemented in editor - new lines line up with previous lines
"New" includes placeholders for description and version.
Refresh button on PluginManager forces modifications to be picked up
Make plugins stay on top while using Run
Indenting and layout tweaks, removed blank line.

More indenting/layout stuff.

... yet more layout fixes...

Push to retrigger travis-ci
2017-09-12 22:58:09 +10:00

687 lines
24 KiB
C++

//=============================================================================
// MuseScore
// Music Composition & Notation
//
// Copyright (C) 2002-2011 Werner Schweer
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License version 2
// as published by the Free Software Foundation and appearing in
// the file LICENCE.GPL
//
// Syntax highlighter based on example code from Ariya Hidayat
// (git://gitorious.org/ofi-labs/x2.git BSD licensed).
//=============================================================================
#include "qmledit.h"
#include "musescore.h"
namespace Ms {
//---------------------------------------------------------
// JSHighlighter
//---------------------------------------------------------
JSHighlighter::JSHighlighter(QTextDocument *parent)
: QSyntaxHighlighter(parent)
, m_markCaseSensitivity(Qt::CaseInsensitive)
{
// default color scheme
m_colors[QmlEdit::Normal] = QColor("#000000");
m_colors[QmlEdit::Comment] = QColor("#808080");
m_colors[QmlEdit::Number] = QColor("#008000");
m_colors[QmlEdit::String] = QColor("#800000");
m_colors[QmlEdit::Operator] = QColor("#808000");
m_colors[QmlEdit::Identifier] = QColor("#000020");
m_colors[QmlEdit::Keyword] = QColor("#000080");
m_colors[QmlEdit::BuiltIn] = QColor("#008080");
m_colors[QmlEdit::Marker] = QColor("#ffff00");
// https://developer.mozilla.org/en/JavaScript/Reference/Reserved_Words
static const char* data1[] = { "break", "case", "catch", "continue",
"default", "delete", "do", "else", "finally", "for", "function",
"if", "in", "instanceof", "new", "return", "switch", "this", "throw",
"try", "typeof", "var", "void", "while", "with", "true", "false",
"null" };
for (unsigned int i = 0; i < sizeof(data1)/sizeof(*data1); ++i)
m_keywords.insert(data1[i]);
// built-in and other popular objects + properties
static const char* data2[] = { "Object", "prototype", "create",
"defineProperty", "defineProperties", "getOwnPropertyDescriptor",
"keys", "getOwnPropertyNames", "constructor", "__parent__", "__proto__",
"__defineGetter__", "__defineSetter__", "eval", "hasOwnProperty",
"isPrototypeOf", "__lookupGetter__", "__lookupSetter__", "__noSuchMethod__",
"propertyIsEnumerable", "toSource", "toLocaleString", "toString",
"unwatch", "valueOf", "watch", "Function", "arguments", "arity", "caller",
"constructor", "length", "name", "apply", "bind", "call", "String",
"fromCharCode", "length", "charAt", "charCodeAt", "concat", "indexOf",
"lastIndexOf", "localCompare", "match", "quote", "replace", "search",
"slice", "split", "substr", "substring", "toLocaleLowerCase",
"toLocaleUpperCase", "toLowerCase", "toUpperCase", "trim", "trimLeft",
"trimRight", "Array", "isArray", "index", "input", "pop", "push",
"reverse", "shift", "sort", "splice", "unshift", "concat", "join",
"filter", "forEach", "every", "map", "some", "reduce", "reduceRight",
"RegExp", "global", "ignoreCase", "lastIndex", "multiline", "source",
"exec", "test", "JSON", "parse", "stringify", "decodeURI",
"decodeURIComponent", "encodeURI", "encodeURIComponent", "eval",
"isFinite", "isNaN", "parseFloat", "parseInt", "Infinity", "NaN",
"undefined", "Math", "E", "LN2", "LN10", "LOG2E", "LOG10E", "PI",
"SQRT1_2", "SQRT2", "abs", "acos", "asin", "atan", "atan2", "ceil",
"cos", "exp", "floor", "log", "max", "min", "pow", "random", "round",
"sin", "sqrt", "tan", "document", "window", "navigator", "userAgent"
};
for (unsigned int i = 0; i < sizeof(data2)/sizeof(*data2); ++i)
m_knownIds.insert(data2[i]);
}
//---------------------------------------------------------
// setColor
//---------------------------------------------------------
void JSHighlighter::setColor(QmlEdit::ColorComponent component, const QColor& color)
{
m_colors[component] = color;
rehighlight();
}
//---------------------------------------------------------
// highlightBlock
//---------------------------------------------------------
void JSHighlighter::highlightBlock(const QString& text)
{
// parsing state
enum class State : char {
Start = 0,
Number = 1,
Identifier = 2,
String = 3,
Comment = 4,
Regex = 5
};
QList<int> bracketPositions;
int blockState = previousBlockState();
int bracketLevel = blockState >> 4;
State state = State(blockState & 15);
if (blockState < 0) {
bracketLevel = 0;
state = State::Start;
}
int start = 0;
int i = 0;
while (i <= text.length()) {
QChar ch = (i < text.length()) ? text.at(i) : QChar();
QChar next = (i < text.length() - 1) ? text.at(i + 1) : QChar();
switch (state) {
case State::Start:
start = i;
if (ch.isSpace()) {
++i;
}
else if (ch.isDigit()) {
++i;
state = State::Number;
}
else if (ch.isLetter() || ch == '_') {
++i;
state = State::Identifier;
}
else if (ch == '\'' || ch == '\"') {
++i;
state = State::String;
}
else if (ch == '/' && next == '*') {
++i;
++i;
state = State::Comment;
}
else if (ch == '/' && next == '/') {
i = text.length();
setFormat(start, text.length(), m_colors[QmlEdit::Comment]);
}
else if (ch == '/' && next != '*') {
++i;
state = State::Regex;
}
else {
if (!QString("(){}[]").contains(ch))
setFormat(start, 1, m_colors[QmlEdit::Operator]);
if (ch =='{' || ch == '}') {
bracketPositions += i;
if (ch == '{')
bracketLevel++;
else
bracketLevel--;
}
++i;
state = State::Start;
}
break;
case State::Number:
if (ch.isSpace() || !ch.isDigit()) {
setFormat(start, i - start, m_colors[QmlEdit::Number]);
state = State::Start;
}
else {
++i;
}
break;
case State::Identifier:
if (ch.isSpace() || !(ch.isDigit() || ch.isLetter() || ch == '_')) {
QString token = text.mid(start, i - start).trimmed();
if (m_keywords.contains(token))
setFormat(start, i - start, m_colors[QmlEdit::Keyword]);
else if (m_knownIds.contains(token))
setFormat(start, i - start, m_colors[QmlEdit::BuiltIn]);
state = State::Start;
}
else {
++i;
}
break;
case State::String:
if (ch == text.at(start)) {
QChar prev = (i > 0) ? text.at(i - 1) : QChar();
if (prev != '\\') {
++i;
setFormat(start, i - start, m_colors[QmlEdit::String]);
state = State::Start;
}
else {
++i;
}
}
else {
++i;
}
break;
case State::Comment:
if (ch == '*' && next == '/') {
++i;
++i;
setFormat(start, i - start, m_colors[QmlEdit::Comment]);
state = State::Start;
}
else {
++i;
}
break;
case State::Regex:
if (ch == '/') {
QChar prev = (i > 0) ? text.at(i - 1) : QChar();
if (prev != '\\') {
++i;
setFormat(start, i - start, m_colors[QmlEdit::String]);
state = State::Start;
}
else {
++i;
}
}
else {
++i;
}
break;
default:
state = State::Start;
break;
}
}
if (state == State::Comment)
setFormat(start, text.length(), m_colors[QmlEdit::Comment]);
else
state = State::Start;
if (!m_markString.isEmpty()) {
int pos = 0;
int len = m_markString.length();
QTextCharFormat markerFormat;
markerFormat.setBackground(m_colors[QmlEdit::Marker]);
markerFormat.setForeground(m_colors[QmlEdit::Normal]);
for (;;) {
pos = text.indexOf(m_markString, pos, m_markCaseSensitivity);
if (pos < 0)
break;
setFormat(pos, len, markerFormat);
++pos;
}
}
if (!bracketPositions.isEmpty()) {
JSBlockData *blockData = reinterpret_cast<JSBlockData*>(currentBlock().userData());
if (!blockData) {
blockData = new JSBlockData;
currentBlock().setUserData(blockData);
}
blockData->bracketPositions = bracketPositions;
}
blockState = (int(state) & 15) | (bracketLevel << 4);
setCurrentBlockState(blockState);
}
//---------------------------------------------------------
// mark
//---------------------------------------------------------
void JSHighlighter::mark(const QString &str, Qt::CaseSensitivity caseSensitivity)
{
m_markString = str;
m_markCaseSensitivity = caseSensitivity;
rehighlight();
}
//---------------------------------------------------------
// keywords
//---------------------------------------------------------
QStringList JSHighlighter::keywords() const
{
return m_keywords.toList();
}
//---------------------------------------------------------
// setKeywords
//---------------------------------------------------------
void JSHighlighter::setKeywords(const QStringList &keywords)
{
m_keywords = QSet<QString>::fromList(keywords);
rehighlight();
}
//---------------------------------------------------------
// Binding
//---------------------------------------------------------
struct Binding {
const char* name;
int key1, key2;
const char* slot;
};
//---------------------------------------------------------
// QmlEdit
//---------------------------------------------------------
QmlEdit::QmlEdit(QWidget* parent)
: QPlainTextEdit(parent)
{
setBackgroundVisible(true);
setLineWrapMode(QPlainTextEdit::NoWrap);
QFont font("FreeMono", 12);
font.setStyleHint(QFont::TypeWriter);
font.setFixedPitch(true);
setFont(font);
document()->setDefaultFont(font);
QTextCursor c = textCursor();
QTextCharFormat cf = c.charFormat();
cf.setFont(font);
c.setCharFormat(cf);
setTextCursor(c);
#if 0
static const Binding bindings[] = {
{ "start", Qt::CTRL+Qt::Key_Q, Qt::CTRL+Qt::Key_E, SLOT(start()) },
{ "end", Qt::CTRL+Qt::Key_Q, Qt::CTRL+Qt::Key_X, SLOT(end()) },
{ "startOfLine", Qt::CTRL+Qt::Key_Q, Qt::CTRL+Qt::Key_S, SLOT(startOfLine()) },
{ "endOfLine", Qt::CTRL+Qt::Key_Q, Qt::CTRL+Qt::Key_D, SLOT(endOfLine()) },
{ "up", Qt::CTRL+Qt::Key_E, 0, SLOT(upLine()) },
{ "down", Qt::CTRL+Qt::Key_X, 0, SLOT(downLine()) },
{ "right", Qt::CTRL+Qt::Key_D, 0, SLOT(right()) },
{ "left", Qt::CTRL+Qt::Key_S, 0, SLOT(left()) },
{ "rightWord", Qt::CTRL+Qt::Key_F, 0, SLOT(rightWord()) },
{ "leftWord", Qt::CTRL+Qt::Key_A, 0, SLOT(leftWord()) },
{ "pick", Qt::Key_F8, 0, SLOT(pick()) },
{ "put", Qt::Key_F9, 0, SLOT(put()) },
{ "delLine", Qt::CTRL+Qt::Key_Y, 0, SLOT(delLine()) },
{ "delWord", Qt::CTRL+Qt::Key_T, 0, SLOT(delWord()) }
};
#endif
setTabChangesFocus(false);
setBackgroundVisible(false);
setCursorWidth(3);
QPalette p = palette();
p.setColor(QPalette::Text, Qt::black);
p.setColor(QPalette::Base, QColor(0xe0, 0xe0, 0xe0));
setPalette(p);
hl = new JSHighlighter(document());
lineNumberArea = new LineNumberArea(this);
#if 0
for (unsigned int i = 0; i < sizeof(bindings)/sizeof(*bindings); ++i) {
const Binding& b = bindings[i];
QAction* a = new QAction(b.name, this);
a->setShortcut(QKeySequence(b.key1, b.key2));
a->setShortcutContext(Qt::WidgetShortcut);
a->setPriority(QAction::HighPriority);
addAction(a);
connect(a, SIGNAL(triggered()), b.slot);
}
#endif
connect(this, SIGNAL(blockCountChanged(int)), SLOT(updateLineNumberAreaWidth(int)));
connect(this, SIGNAL(updateRequest(QRect,int)), SLOT(updateLineNumberArea(QRect,int)));
connect(this, SIGNAL(cursorPositionChanged()), SLOT(highlightCurrentLine()));
updateLineNumberAreaWidth(0);
highlightCurrentLine();
}
//---------------------------------------------------------
// focusInEvent
//---------------------------------------------------------
void QmlEdit::focusInEvent(QFocusEvent* event)
{
mscoreState = mscore->state();
mscore->changeState(STATE_DISABLED);
QPlainTextEdit::focusInEvent(event);
}
//---------------------------------------------------------
// focusOutEvent
//---------------------------------------------------------
void QmlEdit::focusOutEvent(QFocusEvent* event)
{
mscore->changeState(mscoreState);
QPlainTextEdit::focusOutEvent(event);
}
//---------------------------------------------------------
// move
//---------------------------------------------------------
void QmlEdit::move(QTextCursor::MoveOperation op)
{
QTextCursor tc(textCursor());
tc.movePosition(op);
setTextCursor(tc);
update();
}
//---------------------------------------------------------
// QmlEdit
//---------------------------------------------------------
QmlEdit::~QmlEdit()
{
delete hl;
}
//---------------------------------------------------------
// lineNumberAreaWidth
//---------------------------------------------------------
int QmlEdit::lineNumberAreaWidth()
{
int digits = 1;
int max = qMax(1, blockCount());
while (max >= 10) {
max /= 10;
++digits;
}
int space = 6 + fontMetrics().width(QLatin1Char('9')) * digits;
return space;
}
//---------------------------------------------------------
// updateLineNumberAreaWidth
//---------------------------------------------------------
void QmlEdit::updateLineNumberAreaWidth(int /* newBlockCount */)
{
setViewportMargins(lineNumberAreaWidth(), 0, 0, 0);
}
//---------------------------------------------------------
// updateLineNumberArea
//---------------------------------------------------------
void QmlEdit::updateLineNumberArea(const QRect& rect, int dy)
{
if (dy)
lineNumberArea->scroll(0, dy);
else
lineNumberArea->update(0, rect.y(), lineNumberArea->width(), rect.height());
if (rect.contains(viewport()->rect()))
updateLineNumberAreaWidth(0);
}
//---------------------------------------------------------
// resizeEvent
//---------------------------------------------------------
void QmlEdit::resizeEvent(QResizeEvent *e)
{
QPlainTextEdit::resizeEvent(e);
QRect cr = contentsRect();
lineNumberArea->setGeometry(QRect(cr.left(), cr.top(), lineNumberAreaWidth(), cr.height()));
}
//---------------------------------------------------------
// highlightCurrentLine
//---------------------------------------------------------
void QmlEdit::highlightCurrentLine()
{
QList<QTextEdit::ExtraSelection> extraSelections;
if (!isReadOnly()) {
QTextEdit::ExtraSelection selection;
QColor lineColor = QColor(Qt::white);
selection.format.setBackground(lineColor);
selection.format.setProperty(QTextFormat::FullWidthSelection, true);
selection.cursor = textCursor();
selection.cursor.clearSelection();
extraSelections.append(selection);
}
setExtraSelections(extraSelections);
}
//---------------------------------------------------------
// lineNumberAreaPaintEvent
//---------------------------------------------------------
void QmlEdit::lineNumberAreaPaintEvent(QPaintEvent *event)
{
QPainter painter(lineNumberArea);
painter.fillRect(event->rect(), Qt::lightGray);
QFont font("FreeMono", 12);
font.setStyleHint(QFont::TypeWriter);
font.setFixedPitch(true);
painter.setFont(font);
QTextBlock block = firstVisibleBlock();
int blockNumber = block.blockNumber();
int top = (int) blockBoundingGeometry(block).translated(contentOffset()).top();
int bottom = top + (int) blockBoundingRect(block).height();
int w = lineNumberArea->width();
int h = fontMetrics().height();
while (block.isValid() && top <= event->rect().bottom()) {
if (block.isVisible() && bottom >= event->rect().top()) {
QString number = QString::number(blockNumber + 1);
painter.setPen(Qt::black);
painter.drawText(0, top, w-3, h, Qt::AlignRight, number);
}
block = block.next();
top = bottom;
bottom = top + (int) blockBoundingRect(block).height();
++blockNumber;
}
}
//---------------------------------------------------------
// pick
//---------------------------------------------------------
void QmlEdit::pick()
{
pickBuffer = textCursor().block().text();
}
//---------------------------------------------------------
// put
//---------------------------------------------------------
void QmlEdit::put()
{
QTextCursor c = textCursor();
int column = c.columnNumber();
c.movePosition(QTextCursor::StartOfBlock);
c.insertText(pickBuffer + "\n");
c.movePosition(QTextCursor::Right, QTextCursor::MoveAnchor, column);
c.movePosition(QTextCursor::Up);
setTextCursor(c);
}
//---------------------------------------------------------
// delLine
//---------------------------------------------------------
void QmlEdit::delLine()
{
QTextCursor c = textCursor();
c.select(QTextCursor::BlockUnderCursor);
pickBuffer = c.selectedText().mid(1);
c.removeSelectedText();
c.movePosition(QTextCursor::Down);
setTextCursor(c);
}
//---------------------------------------------------------
// delWord
//---------------------------------------------------------
void QmlEdit::delWord()
{
QTextCursor c = textCursor();
int i = c.position();
if (document()->characterAt(i) == QChar(' ')) {
while(document()->characterAt(i) == QChar(' '))
c.deleteChar();
}
else {
for (;;) {
QChar ch = document()->characterAt(i);
if (ch == QChar(' ') || ch == QChar('\n'))
break;
c.deleteChar();
}
while(document()->characterAt(i) == QChar(' '))
c.deleteChar();
}
}
//---------------------------------------------------------
// downLine
//---------------------------------------------------------
void QmlEdit::downLine()
{
qDebug("Down line");
move(QTextCursor::Down);
}
//---------------------------------------------------------
// leftWord
//---------------------------------------------------------
void QmlEdit::leftWord()
{
QTextCursor c = textCursor();
if (c.positionInBlock() == 0)
return;
c.movePosition(QTextCursor::Left);
bool inSpace = true;
for (;c.positionInBlock();) {
int i = c.position();
if (document()->characterAt(i) == QChar(' ')) {
if (!inSpace) {
c.movePosition(QTextCursor::Right);
break;
}
}
else {
if (inSpace)
inSpace = false;
}
c.movePosition(QTextCursor::Left);
}
setTextCursor(c);
}
//---------------------------------------------------------
// keyPressEvent
//---------------------------------------------------------
void QmlEdit::keyPressEvent(QKeyEvent* event)
{
if (event->modifiers() != Qt::ControlModifier && event->key() == Qt::Key_Tab) {
tab();
event->accept();
return;
}
if (event->modifiers() != Qt::ControlModifier &&
(event->key() == Qt::Key_Enter || event->key() == Qt::Key_Return)) {
autoIndent();
event->accept();
return;
}
QPlainTextEdit::keyPressEvent(event);
}
//---------------------------------------------------------
// autoindent - line new line up with start of previous.
//---------------------------------------------------------
void QmlEdit::autoIndent()
{
QTextCursor c = textCursor();
QTextBlock b = c.block();
QString line = "";
while (line.trimmed() == "" && b.isValid()) // Find last non-blank line to line up on.
{
line = b.text();
b = b.previous();
}
int indent = 0;
c.insertText("\n");
while (line[indent] == " ") {
indent += 1;
c.insertText(" ");
}
}
//---------------------------------------------------------
// tab
//---------------------------------------------------------
void QmlEdit::tab()
{
QTextCursor c = textCursor();
c.insertText(" ");
while (c.positionInBlock() % 6)
c.insertText(" ");
setTextCursor(c);
}
}