openscad/src/scintillaeditor.cpp

605 lines
19 KiB
C++

#include <stdlib.h>
#include <algorithm>
#include <QString>
#include <QChar>
#include "boosty.h"
#include "scintillaeditor.h"
#include <Qsci/qscicommandset.h>
#include "Preferences.h"
#include "PlatformUtils.h"
#include "settings.h"
class SettingsConverter {
public:
QsciScintilla::WrapMode toWrapMode(Value val);
QsciScintilla::WrapVisualFlag toLineWrapVisualization(Value val);
QsciScintilla::WrapIndentMode toLineWrapIndentationStyle(Value val);
QsciScintilla::WhitespaceVisibility toShowWhitespaces(Value val);
};
QsciScintilla::WrapMode SettingsConverter::toWrapMode(Value val)
{
std::string v = val.toString();
if (v == "Char") {
return QsciScintilla::WrapCharacter;
} else if (v == "Word") {
return QsciScintilla::WrapWord;
} else {
return QsciScintilla::WrapNone;
}
}
QsciScintilla::WrapVisualFlag SettingsConverter::toLineWrapVisualization(Value val)
{
std::string v = val.toString();
if (v == "Text") {
return QsciScintilla::WrapFlagByText;
} else if (v == "Border") {
return QsciScintilla::WrapFlagByBorder;
#if QSCINTILLA_VERSION >= 0x020700
} else if (v == "Margin") {
return QsciScintilla::WrapFlagInMargin;
#endif
} else {
return QsciScintilla::WrapFlagNone;
}
}
QsciScintilla::WrapIndentMode SettingsConverter::toLineWrapIndentationStyle(Value val)
{
std::string v = val.toString();
if (v == "Same") {
return QsciScintilla::WrapIndentSame;
} else if (v == "Indented") {
return QsciScintilla::WrapIndentIndented;
} else {
return QsciScintilla::WrapIndentFixed;
}
}
QsciScintilla::WhitespaceVisibility SettingsConverter::toShowWhitespaces(Value val)
{
std::string v = val.toString();
if (v == "Always") {
return QsciScintilla::WsVisible;
} else if (v == "AfterIndentation") {
return QsciScintilla::WsVisibleAfterIndent;
} else {
return QsciScintilla::WsInvisible;
}
}
EditorColorScheme::EditorColorScheme(fs::path path) : path(path)
{
try {
boost::property_tree::read_json(boosty::stringy(path).c_str(), pt);
_name = QString(pt.get<std::string>("name").c_str());
_index = pt.get<int>("index");
} catch (const std::exception & e) {
PRINTB("Error reading color scheme file '%s': %s", boosty::stringy(path).c_str() % e.what());
_name = "";
_index = 0;
}
}
EditorColorScheme::~EditorColorScheme()
{
}
bool EditorColorScheme::valid() const
{
return !_name.isEmpty();
}
const QString & EditorColorScheme::name() const
{
return _name;
}
int EditorColorScheme::index() const
{
return _index;
}
const boost::property_tree::ptree & EditorColorScheme::propertyTree() const
{
return pt;
}
ScintillaEditor::ScintillaEditor(QWidget *parent) : EditorInterface(parent)
{
scintillaLayout = new QVBoxLayout(this);
qsci = new QsciScintilla(this);
// Force EOL mode to Unix, since QTextStream will manage local EOL modes.
qsci->setEolMode(QsciScintilla::EolUnix);
//
// Remapping some scintilla key binding which conflict with OpenSCAD global
// key bindings, as well as some minor scintilla bugs
//
QsciCommand *c;
#ifdef Q_OS_MAC
// Alt-Backspace should delete left word (Alt-Delete already deletes right word)
c = qsci->standardCommands()->find(QsciCommand::DeleteWordLeft);
c->setKey(Qt::Key_Backspace | Qt::ALT);
#endif
// Cmd/Ctrl-T is handled by the menu
c = qsci->standardCommands()->boundTo(Qt::Key_T | Qt::CTRL);
c->setKey(0);
// Cmd/Ctrl-D is handled by the menu
c = qsci->standardCommands()->boundTo(Qt::Key_D | Qt::CTRL);
c->setKey(0);
// Ctrl-Shift-Z should redo on all platforms
c = qsci->standardCommands()->find(QsciCommand::Redo);
c->setKey(Qt::Key_Z | Qt::CTRL | Qt::SHIFT);
scintillaLayout->setContentsMargins(0, 0, 0, 0);
scintillaLayout->addWidget(qsci);
qsci->indicatorDefine(QsciScintilla::RoundBoxIndicator, indicatorNumber);
qsci->markerDefine(QsciScintilla::Circle, markerNumber);
qsci->setUtf8(true);
qsci->setFolding(QsciScintilla::BoxedTreeFoldStyle, 4);
lexer = new ScadLexer(this);
qsci->setLexer(lexer);
initMargin();
connect(qsci, SIGNAL(textChanged()), this, SIGNAL(contentsChanged()));
connect(qsci, SIGNAL(modificationChanged(bool)), this, SIGNAL(modificationChanged(bool)));
}
/**
* Apply the settings that are changeable in the preferences. This is also
* called in the event handler from the preferences.
*/
void ScintillaEditor::applySettings()
{
SettingsConverter conv;
Settings::Settings *s = Settings::Settings::inst();
qsci->setIndentationWidth(s->get(Settings::Settings::indentationWidth).toDouble());
qsci->setTabWidth(s->get(Settings::Settings::tabWidth).toDouble());
qsci->setWrapMode(conv.toWrapMode(s->get(Settings::Settings::lineWrap)));
qsci->setWrapIndentMode(conv.toLineWrapIndentationStyle(s->get(Settings::Settings::lineWrapIndentationStyle)));
qsci->setWrapVisualFlags(conv.toLineWrapVisualization(s->get(Settings::Settings::lineWrapVisualizationEnd)),
conv.toLineWrapVisualization(s->get(Settings::Settings::lineWrapVisualizationBegin)),
s->get(Settings::Settings::lineWrapIndentation).toDouble());
qsci->setWhitespaceVisibility(conv.toShowWhitespaces(s->get(Settings::Settings::showWhitespace)));
qsci->setWhitespaceSize(s->get(Settings::Settings::showWhitespaceSize).toDouble());
qsci->setAutoIndent(s->get(Settings::Settings::autoIndent).toBool());
std::string indentStyle = s->get(Settings::Settings::indentStyle).toString();
qsci->setIndentationsUseTabs(indentStyle == "Tabs");
std::string tabKeyFunction = s->get(Settings::Settings::tabKeyFunction).toString();
qsci->setTabIndents(tabKeyFunction == "Indent");
qsci->setBraceMatching(s->get(Settings::Settings::enableBraceMatching).toBool() ? QsciScintilla::SloppyBraceMatch : QsciScintilla::NoBraceMatch);
qsci->setCaretLineVisible(s->get(Settings::Settings::highlightCurrentLine).toBool());
}
void ScintillaEditor::setPlainText(const QString &text)
{
qsci->setText(text);
setContentModified(false);
}
QString ScintillaEditor::toPlainText()
{
return qsci->text();
}
void ScintillaEditor::setContentModified(bool modified)
{
// FIXME: Due to an issue with QScintilla, we need to do this on the document itself.
#if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
qsci->SCN_SAVEPOINTLEFT();
#endif
qsci->setModified(modified);
}
bool ScintillaEditor::isContentModified()
{
return qsci->isModified();
}
void ScintillaEditor::highlightError(int error_pos)
{
int line, index;
qsci->lineIndexFromPosition(error_pos, &line, &index);
qsci->fillIndicatorRange(line, index, line, index + 1, indicatorNumber);
qsci->markerAdd(line, markerNumber);
}
void ScintillaEditor::unhighlightLastError()
{
int totalLength = qsci->text().length();
int line, index;
qsci->lineIndexFromPosition(totalLength, &line, &index);
qsci->clearIndicatorRange(0, 0, line, index, indicatorNumber);
qsci->markerDeleteAll(markerNumber);
}
QColor ScintillaEditor::readColor(const boost::property_tree::ptree &pt, const std::string name, const QColor defaultColor)
{
try {
const std::string val = pt.get<std::string>(name);
#if QT_VERSION < QT_VERSION_CHECK(5, 0, 0)
if ((val.length() == 9) && (val.at(0) == '#')) {
const std::string rgb = std::string("#") + val.substr(3);
QColor qcol(rgb.c_str());
unsigned long alpha = std::strtoul(val.substr(1, 2).c_str(), 0, 16);
qcol.setAlpha(alpha);
return qcol;
}
#endif
return QColor(val.c_str());
} catch (std::exception e) {
return defaultColor;
}
}
std::string ScintillaEditor::readString(const boost::property_tree::ptree &pt, const std::string name, const std::string defaultValue)
{
try {
const std::string val = pt.get<std::string>(name);
return val;
} catch (std::exception e) {
return defaultValue;
}
}
int ScintillaEditor::readInt(const boost::property_tree::ptree &pt, const std::string name, const int defaultValue)
{
try {
const int val = pt.get<int>(name);
return val;
} catch (std::exception e) {
return defaultValue;
}
}
void ScintillaEditor::setColormap(const EditorColorScheme *colorScheme)
{
const boost::property_tree::ptree & pt = colorScheme->propertyTree();
try {
QFont font = lexer->font(lexer->defaultStyle());
const QColor textColor(pt.get<std::string>("text").c_str());
const QColor paperColor(pt.get<std::string>("paper").c_str());
ScadLexer *l = new ScadLexer(this);
// Keywords must be set before the lexer is attached to QScintilla
// as they seem to be read and cached at attach time.
boost::optional<const boost::property_tree::ptree&> keywords = pt.get_child_optional("keywords");
if (keywords.is_initialized()) {
l->setKeywords(1, readString(keywords.get(), "keyword-set1", ""));
l->setKeywords(2, readString(keywords.get(), "keyword-set2", ""));
l->setKeywords(3, readString(keywords.get(), "keyword-set-doc", ""));
l->setKeywords(4, readString(keywords.get(), "keyword-set3", ""));
}
qsci->setLexer(l);
delete lexer;
lexer = l;
// All other properties must be set after attaching to QSCintilla so
// the editor gets the change events and updates itself to match
l->setFont(font);
l->setColor(textColor);
l->setPaper(paperColor);
// Somehow, the margin font got lost when we deleted the old lexer
qsci->setMarginsFont(font);
const boost::property_tree::ptree& colors = pt.get_child("colors");
l->setColor(readColor(colors, "keyword1", textColor), QsciLexerCPP::Keyword);
l->setColor(readColor(colors, "keyword2", textColor), QsciLexerCPP::KeywordSet2);
l->setColor(readColor(colors, "keyword3", textColor), QsciLexerCPP::GlobalClass);
l->setColor(readColor(colors, "number", textColor), QsciLexerCPP::Number);
l->setColor(readColor(colors, "string", textColor), QsciLexerCPP::SingleQuotedString);
l->setColor(readColor(colors, "string", textColor), QsciLexerCPP::DoubleQuotedString);
l->setColor(readColor(colors, "operator", textColor), QsciLexerCPP::Operator);
l->setColor(readColor(colors, "comment", textColor), QsciLexerCPP::Comment);
l->setColor(readColor(colors, "commentline", textColor), QsciLexerCPP::CommentLine);
l->setColor(readColor(colors, "commentdoc", textColor), QsciLexerCPP::CommentDoc);
l->setColor(readColor(colors, "commentdoc", textColor), QsciLexerCPP::CommentLineDoc);
l->setColor(readColor(colors, "commentdockeyword", textColor), QsciLexerCPP::CommentDocKeyword);
const boost::property_tree::ptree& caret = pt.get_child("caret");
qsci->setCaretWidth(readInt(caret, "width", 1));
qsci->setCaretForegroundColor(readColor(caret, "foreground", textColor));
qsci->setCaretLineBackgroundColor(readColor(caret, "line-background", paperColor));
qsci->setMarkerBackgroundColor(readColor(colors, "error-marker", QColor(255, 0, 0, 100)), markerNumber);
qsci->setIndicatorForegroundColor(readColor(colors, "error-indicator", QColor(255, 0, 0, 100)), indicatorNumber);
qsci->setIndicatorOutlineColor(readColor(colors, "error-indicator-outline", QColor(255, 0, 0, 100)), indicatorNumber);
qsci->setWhitespaceForegroundColor(readColor(colors, "whitespace-foreground", textColor));
qsci->setMarginsBackgroundColor(readColor(colors, "margin-background", paperColor));
qsci->setMarginsForegroundColor(readColor(colors, "margin-foreground", textColor));
qsci->setFoldMarginColors(readColor(colors, "margin-background", paperColor),
readColor(colors, "margin-background", paperColor));
qsci->setMatchedBraceBackgroundColor(readColor(colors, "matched-brace-background", paperColor));
qsci->setMatchedBraceForegroundColor(readColor(colors, "matched-brace-foreground", textColor));
qsci->setUnmatchedBraceBackgroundColor(readColor(colors, "unmatched-brace-background", paperColor));
qsci->setUnmatchedBraceForegroundColor(readColor(colors, "unmatched-brace-foreground", textColor));
qsci->setSelectionForegroundColor(readColor(colors, "selection-foreground", paperColor));
qsci->setSelectionBackgroundColor(readColor(colors, "selection-background", textColor));
qsci->setEdgeColor(readColor(colors, "edge", textColor));
} catch (std::exception e) {
noColor();
}
}
void ScintillaEditor::noColor()
{
lexer->setPaper(Qt::white);
lexer->setColor(Qt::black);
qsci->setCaretWidth(2);
qsci->setCaretForegroundColor(Qt::black);
qsci->setMarkerBackgroundColor(QColor(255, 0, 0, 100), markerNumber);
qsci->setIndicatorForegroundColor(QColor(255, 0, 0, 128), indicatorNumber);
qsci->setIndicatorOutlineColor(QColor(0, 0, 0, 255), indicatorNumber); // only alpha part is used
qsci->setCaretLineBackgroundColor(Qt::white);
qsci->setWhitespaceForegroundColor(Qt::black);
qsci->setSelectionForegroundColor(Qt::black);
qsci->setSelectionBackgroundColor(QColor("LightSkyBlue"));
qsci->setMatchedBraceBackgroundColor(QColor("LightBlue"));
qsci->setMatchedBraceForegroundColor(Qt::black);
qsci->setUnmatchedBraceBackgroundColor(QColor("pink"));
qsci->setUnmatchedBraceForegroundColor(Qt::black);
qsci->setMarginsBackgroundColor(QColor("whiteSmoke"));
qsci->setMarginsForegroundColor(QColor("gray"));
qsci->setFoldMarginColors(QColor("whiteSmoke"), QColor("whiteSmoke"));
qsci->setEdgeColor(Qt::black);
}
void ScintillaEditor::enumerateColorSchemesInPath(ScintillaEditor::colorscheme_set_t &result_set, const fs::path path)
{
const fs::path color_schemes = path / "color-schemes" / "editor";
fs::directory_iterator end_iter;
if (fs::exists(color_schemes) && fs::is_directory(color_schemes)) {
for (fs::directory_iterator dir_iter(color_schemes); dir_iter != end_iter; ++dir_iter) {
if (!fs::is_regular_file(dir_iter->status())) {
continue;
}
const fs::path path = (*dir_iter).path();
if (!(path.extension() == ".json")) {
continue;
}
EditorColorScheme *colorScheme = new EditorColorScheme(path);
if (colorScheme->valid()) {
result_set.insert(colorscheme_set_t::value_type(colorScheme->index(), boost::shared_ptr<EditorColorScheme>(colorScheme)));
} else {
delete colorScheme;
}
}
}
}
ScintillaEditor::colorscheme_set_t ScintillaEditor::enumerateColorSchemes()
{
colorscheme_set_t result_set;
enumerateColorSchemesInPath(result_set, PlatformUtils::resourceBasePath());
enumerateColorSchemesInPath(result_set, PlatformUtils::userConfigPath());
return result_set;
}
QStringList ScintillaEditor::colorSchemes()
{
const colorscheme_set_t colorscheme_set = enumerateColorSchemes();
QStringList colorSchemes;
for (colorscheme_set_t::const_iterator it = colorscheme_set.begin(); it != colorscheme_set.end(); it++) {
colorSchemes << (*it).second.get()->name();
}
colorSchemes << "Off";
return colorSchemes;
}
void ScintillaEditor::setHighlightScheme(const QString &name)
{
const colorscheme_set_t colorscheme_set = enumerateColorSchemes();
for (colorscheme_set_t::const_iterator it = colorscheme_set.begin(); it != colorscheme_set.end(); it++) {
const EditorColorScheme *colorScheme = (*it).second.get();
if (colorScheme->name() == name) {
setColormap(colorScheme);
return;
}
}
noColor();
}
void ScintillaEditor::insert(const QString &text)
{
qsci->insert(text);
}
void ScintillaEditor::setText(const QString &text)
{
qsci->selectAll(true);
qsci->replaceSelectedText(text);
}
void ScintillaEditor::undo()
{
qsci->undo();
}
void ScintillaEditor::redo()
{
qsci->redo();
}
void ScintillaEditor::cut()
{
qsci->cut();
}
void ScintillaEditor::copy()
{
qsci->copy();
}
void ScintillaEditor::paste()
{
qsci->paste();
}
void ScintillaEditor::zoomIn()
{
qsci->zoomIn();
}
void ScintillaEditor::zoomOut()
{
qsci->zoomOut();
}
void ScintillaEditor::initFont(const QString& fontName, uint size)
{
this->currentFont = QFont(fontName, size);
this->currentFont.setFixedPitch(true);
lexer->setFont(this->currentFont);
qsci->setMarginsFont(this->currentFont);
onTextChanged(); // Update margin width
}
void ScintillaEditor::initMargin()
{
qsci->setMarginLineNumbers(1, true);
connect(qsci, SIGNAL(textChanged()), this, SLOT(onTextChanged()));
}
void ScintillaEditor::onTextChanged()
{
QFontMetrics fontmetrics(this->currentFont);
qsci->setMarginWidth(1, QString(trunc(log10(qsci->lines())+2), '0'));
}
bool ScintillaEditor::find(const QString &expr, bool findNext, bool findBackwards)
{
int startline = -1, startindex = -1;
// If findNext, start from the end of the current selection
if (qsci->hasSelectedText()) {
int lineFrom, indexFrom, lineTo, indexTo;
qsci->getSelection(&lineFrom, &indexFrom, &lineTo, &indexTo);
startline = !(findBackwards xor findNext) ? std::min(lineFrom, lineTo) : std::max(lineFrom, lineTo);
startindex = !(findBackwards xor findNext) ? std::min(indexFrom, indexTo) : std::max(indexFrom, indexTo);
}
return qsci->findFirst(expr, false, false, false, true,
!findBackwards, startline, startindex);
}
void ScintillaEditor::replaceSelectedText(const QString &newText)
{
if (qsci->selectedText() != newText) qsci->replaceSelectedText(newText);
}
void ScintillaEditor::replaceAll(const QString &findText, const QString &replaceText)
{
// We need to issue a Select All first due to a bug in QScintilla:
// It doesn't update the find range when just doing findFirst() + findNext() causing the search
// to end prematurely if the replaced string is larger than the selected string.
#if QSCINTILLA_VERSION >= 0x020700
qsci->selectAll();
if (qsci->findFirstInSelection(findText,
false /*re*/, false /*cs*/, false /*wo*/,
false /*wrap*/, true /*forward*/)) {
#else
// findFirstInSelection() was introduced in QScintilla 2.7
if (qsci->findFirst(findText,
false /*re*/, false /*cs*/, false /*wo*/,
false /*wrap*/, true /*forward*/, 0, 0)) {
#endif
qsci->replace(replaceText);
while (qsci->findNext()) {
qsci->replace(replaceText);
}
}
}
void ScintillaEditor::getRange(int *lineFrom, int *lineTo)
{
int indexFrom, indexTo;
if (qsci->hasSelectedText()) {
qsci->getSelection(lineFrom, &indexFrom, lineTo, &indexTo);
if (indexTo == 0) {
*lineTo = *lineTo - 1;
}
} else {
qsci->getCursorPosition(lineFrom, &indexFrom);
*lineTo = *lineFrom;
}
}
void ScintillaEditor::indentSelection()
{
int lineFrom, lineTo;
getRange(&lineFrom, &lineTo);
for (int line = lineFrom; line <= lineTo; line++) {
qsci->indent(line);
}
}
void ScintillaEditor::unindentSelection()
{
int lineFrom, lineTo;
getRange(&lineFrom, &lineTo);
for (int line = lineFrom; line <= lineTo; line++) {
qsci->unindent(line);
}
}
void ScintillaEditor::commentSelection()
{
bool hasSelection = qsci->hasSelectedText();
int lineFrom, lineTo;
getRange(&lineFrom, &lineTo);
for (int line = lineFrom; line <= lineTo; line++) {
qsci->insertAt("//", line, 0);
}
if (hasSelection) {
qsci->setSelection(lineFrom, 0, lineTo, std::max(0, qsci->lineLength(lineTo) - 1));
}
}
void ScintillaEditor::uncommentSelection()
{
bool hasSelection = qsci->hasSelectedText();
int lineFrom, lineTo;
getRange(&lineFrom, &lineTo);
for (int line = lineFrom; line <= lineTo; line++) {
QString lineText = qsci->text(line);
if (lineText.startsWith("//")) {
qsci->setSelection(line, 0, line, 2);
qsci->removeSelectedText();
}
}
if (hasSelection) {
qsci->setSelection(lineFrom, 0, lineTo, std::max(0, qsci->lineLength(lineTo) - 1));
}
}
QString ScintillaEditor::selectedText()
{
return qsci->selectedText();
}