JSON mode for cell editor

Support for JSON in the Database Cell editor using the QScintilla library.

The lexJSON lexer has been added to the compilation, including the
necessary files from the QScintilla source package.

See issue #1173
This commit is contained in:
mgrojo
2017-11-18 16:28:38 +01:00
parent 1d701adec7
commit 3c910a9e59
11 changed files with 1145 additions and 23 deletions

View File

@@ -105,6 +105,7 @@ set(SQLB_MOC_HDR
src/VacuumDialog.h
src/sqlitetablemodel.h
src/sqltextedit.h
src/jsontextedit.h
src/DbStructureModel.h
src/Application.h
src/CipherDialog.h
@@ -139,6 +140,7 @@ set(SQLB_SRC
src/sqlitetablemodel.cpp
src/sqlitetypes.cpp
src/sqltextedit.cpp
src/jsontextedit.cpp
src/csvparser.cpp
src/DbStructureModel.cpp
src/grammar/Sqlite3Lexer.cpp

View File

@@ -22,6 +22,7 @@ set(QSCINTILLA_SRC
qscilexer.cpp
qscilexercustom.cpp
qscilexersql.cpp
qscilexerjson.cpp
qscimacro.cpp
qsciprinter.cpp
qscistyle.cpp
@@ -33,6 +34,7 @@ set(QSCINTILLA_SRC
PlatQt.cpp
ScintillaQt.cpp
../lexers/LexSQL.cpp
../lexers/LexJSON.cpp
../lexlib/Accessor.cpp
../lexlib/CharacterCategory.cpp
../lexlib/CharacterSet.cpp
@@ -143,6 +145,7 @@ set(QSCINTILLA_MOC_HDR
./Qsci/qscilexer.h
./Qsci/qscilexercustom.h
./Qsci/qscilexersql.h
./Qsci/qscilexerjson.h
./Qsci/qscimacro.h
SciClasses.h
ScintillaQt.h

View File

@@ -0,0 +1,298 @@
// This module implements the QsciLexerJSON class.
//
// Copyright (c) 2017 Riverbank Computing Limited <info@riverbankcomputing.com>
//
// This file is part of QScintilla.
//
// This file may be used under the terms of the GNU General Public License
// version 3.0 as published by the Free Software Foundation and appearing in
// the file LICENSE included in the packaging of this file. Please review the
// following information to ensure the GNU General Public License version 3.0
// requirements will be met: http://www.gnu.org/copyleft/gpl.html.
//
// If you do not wish to use this file under the terms of the GPL version 3.0
// then you may purchase a commercial license. For more information contact
// info@riverbankcomputing.com.
//
// This file is provided AS IS with NO WARRANTY OF ANY KIND, INCLUDING THE
// WARRANTY OF DESIGN, MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
#include "Qsci/qscilexerjson.h"
#include <qcolor.h>
#include <qfont.h>
#include <qsettings.h>
// The ctor.
QsciLexerJSON::QsciLexerJSON(QObject *parent)
: QsciLexer(parent),
allow_comments(true), escape_sequence(true), fold_compact(true)
{
}
// The dtor.
QsciLexerJSON::~QsciLexerJSON()
{
}
// Returns the language name.
const char *QsciLexerJSON::language() const
{
return "JSON";
}
// Returns the lexer name.
const char *QsciLexerJSON::lexer() const
{
return "json";
}
// Returns the foreground colour of the text for a style.
QColor QsciLexerJSON::defaultColor(int style) const
{
switch (style)
{
case UnclosedString:
case Error:
return QColor(0xff, 0xff, 0xff);
case Number:
return QColor(0x00, 0x7f, 0x7f);
case String:
return QColor(0x7f, 0x00, 0x00);
case Property:
return QColor(0x88, 0x0a, 0xe8);
case EscapeSequence:
return QColor(0x0b, 0x98, 0x2e);
case CommentLine:
case CommentBlock:
return QColor(0x05, 0xbb, 0xae);
case Operator:
return QColor(0x18, 0x64, 0x4a);
case IRI:
return QColor(0x00, 0x00, 0xff);
case IRICompact:
return QColor(0xd1, 0x37, 0xc1);
case Keyword:
return QColor(0x0b, 0xce, 0xa7);
case KeywordLD:
return QColor(0xec, 0x28, 0x06);
}
return QsciLexer::defaultColor(style);
}
// Returns the end-of-line fill for a style.
bool QsciLexerJSON::defaultEolFill(int style) const
{
switch (style)
{
case UnclosedString:
return true;
}
return QsciLexer::defaultEolFill(style);
}
// Returns the font of the text for a style.
QFont QsciLexerJSON::defaultFont(int style) const
{
QFont f;
switch (style)
{
case CommentLine:
f = QsciLexer::defaultFont(style);
f.setItalic(true);
break;
case Keyword:
f = QsciLexer::defaultFont(style);
f.setBold(true);
break;
default:
f = QsciLexer::defaultFont(style);
}
return f;
}
// Returns the set of keywords.
const char *QsciLexerJSON::keywords(int set) const
{
if (set == 1)
return "false true null";
if (set == 2)
return
"@id @context @type @value @language @container @list @set "
"@reverse @index @base @vocab @graph";
return 0;
}
// Returns the user name of a style.
QString QsciLexerJSON::description(int style) const
{
switch (style)
{
case Default:
return tr("Default");
case Number:
return tr("Number");
case String:
return tr("String");
case UnclosedString:
return tr("Unclosed string");
case Property:
return tr("Property");
case EscapeSequence:
return tr("Escape sequence");
case CommentLine:
return tr("Line comment");
case CommentBlock:
return tr("Block comment");
case Operator:
return tr("Operator");
case IRI:
return tr("IRI");
case IRICompact:
return tr("JSON-LD compact IRI");
case Keyword:
return tr("JSON keyword");
case KeywordLD:
return tr("JSON-LD keyword");
case Error:
return tr("Parsing error");
}
return QString();
}
// Returns the background colour of the text for a style.
QColor QsciLexerJSON::defaultPaper(int style) const
{
switch (style)
{
case UnclosedString:
case Error:
return QColor(0xff, 0x00, 0x00);
}
return QsciLexer::defaultPaper(style);
}
// Refresh all properties.
void QsciLexerJSON::refreshProperties()
{
setAllowCommentsProp();
setEscapeSequenceProp();
setCompactProp();
}
// Read properties from the settings.
bool QsciLexerJSON::readProperties(QSettings &qs,const QString &prefix)
{
allow_comments = qs.value(prefix + "allowcomments", true).toBool();
escape_sequence = qs.value(prefix + "escapesequence", true).toBool();
fold_compact = qs.value(prefix + "foldcompact", true).toBool();
return true;
}
// Write properties to the settings.
bool QsciLexerJSON::writeProperties(QSettings &qs,const QString &prefix) const
{
qs.setValue(prefix + "allowcomments", allow_comments);
qs.setValue(prefix + "escapesequence", escape_sequence);
qs.setValue(prefix + "foldcompact", fold_compact);
return true;
}
// Set if comments are highlighted
void QsciLexerJSON::setHighlightComments(bool highlight)
{
allow_comments = highlight;
setAllowCommentsProp();
}
// Set the "lexer.json.allow.comments" property.
void QsciLexerJSON::setAllowCommentsProp()
{
emit propertyChanged("lexer.json.allow.comments",
(allow_comments ? "1" : "0"));
}
// Set if escape sequences are highlighted.
void QsciLexerJSON::setHighlightEscapeSequences(bool highlight)
{
escape_sequence = highlight;
setEscapeSequenceProp();
}
// Set the "lexer.json.escape.sequence" property.
void QsciLexerJSON::setEscapeSequenceProp()
{
emit propertyChanged("lexer.json.escape.sequence",
(escape_sequence ? "1" : "0"));
}
// Set if folds are compact.
void QsciLexerJSON::setFoldCompact(bool fold)
{
fold_compact = fold;
setCompactProp();
}
// Set the "fold.compact" property.
void QsciLexerJSON::setCompactProp()
{
emit propertyChanged("fold.compact", (fold_compact ? "1" : "0"));
}

View File

@@ -86,6 +86,7 @@ HEADERS = \
./Qsci/qscilexer.h \
./Qsci/qscilexercustom.h \
./Qsci/qscilexersql.h \
./Qsci/qscilexerjson.h \
./Qsci/qscimacro.h \
./Qsci/qsciprinter.h \
./Qsci/qscistyle.h \
@@ -158,6 +159,7 @@ SOURCES = \
qscilexer.cpp \
qscilexercustom.cpp \
qscilexersql.cpp \
qscilexerjson.cpp \
qscimacro.cpp \
qsciprinter.cpp \
qscistyle.cpp \
@@ -169,6 +171,7 @@ SOURCES = \
PlatQt.cpp \
ScintillaQt.cpp \
../lexers/LexSQL.cpp \
../lexers/LexJSON.cpp \
../lexlib/Accessor.cpp \
../lexlib/CharacterCategory.cpp \
../lexlib/CharacterSet.cpp \

View File

@@ -0,0 +1,497 @@
// Scintilla source code edit control
/**
* @file LexJSON.cxx
* @date February 19, 2016
* @brief Lexer for JSON and JSON-LD formats
* @author nkmathew
*
* The License.txt file describes the conditions under which this software may
* be distributed.
*
*/
#include <cstdlib>
#include <cassert>
#include <cctype>
#include <cstdio>
#include <string>
#include <vector>
#include <map>
#include "ILexer.h"
#include "Scintilla.h"
#include "SciLexer.h"
#include "WordList.h"
#include "LexAccessor.h"
#include "StyleContext.h"
#include "CharacterSet.h"
#include "LexerModule.h"
#include "OptionSet.h"
#ifdef SCI_NAMESPACE
using namespace Scintilla;
#endif
static const char *const JSONWordListDesc[] = {
"JSON Keywords",
"JSON-LD Keywords",
0
};
/**
* Used to detect compact IRI/URLs in JSON-LD without first looking ahead for the
* colon separating the prefix and suffix
*
* https://www.w3.org/TR/json-ld/#dfn-compact-iri
*/
struct CompactIRI {
int colonCount;
bool foundInvalidChar;
CharacterSet setCompactIRI;
CompactIRI() {
colonCount = 0;
foundInvalidChar = false;
setCompactIRI = CharacterSet(CharacterSet::setAlpha, "$_-");
}
void resetState() {
colonCount = 0;
foundInvalidChar = false;
}
void checkChar(int ch) {
if (ch == ':') {
colonCount++;
} else {
foundInvalidChar |= !setCompactIRI.Contains(ch);
}
}
bool shouldHighlight() const {
return !foundInvalidChar && colonCount == 1;
}
};
/**
* Keeps track of escaped characters in strings as per:
*
* https://tools.ietf.org/html/rfc7159#section-7
*/
struct EscapeSequence {
int digitsLeft;
CharacterSet setHexDigits;
CharacterSet setEscapeChars;
EscapeSequence() {
digitsLeft = 0;
setHexDigits = CharacterSet(CharacterSet::setDigits, "ABCDEFabcdef");
setEscapeChars = CharacterSet(CharacterSet::setNone, "\\\"tnbfru/");
}
// Returns true if the following character is a valid escaped character
bool newSequence(int nextChar) {
digitsLeft = 0;
if (nextChar == 'u') {
digitsLeft = 5;
} else if (!setEscapeChars.Contains(nextChar)) {
return false;
}
return true;
}
bool atEscapeEnd() const {
return digitsLeft <= 0;
}
bool isInvalidChar(int currChar) const {
return !setHexDigits.Contains(currChar);
}
};
struct OptionsJSON {
bool foldCompact;
bool fold;
bool allowComments;
bool escapeSequence;
OptionsJSON() {
foldCompact = false;
fold = false;
allowComments = false;
escapeSequence = false;
}
};
struct OptionSetJSON : public OptionSet<OptionsJSON> {
OptionSetJSON() {
DefineProperty("lexer.json.escape.sequence", &OptionsJSON::escapeSequence,
"Set to 1 to enable highlighting of escape sequences in strings");
DefineProperty("lexer.json.allow.comments", &OptionsJSON::allowComments,
"Set to 1 to enable highlighting of line/block comments in JSON");
DefineProperty("fold.compact", &OptionsJSON::foldCompact);
DefineProperty("fold", &OptionsJSON::fold);
DefineWordListSets(JSONWordListDesc);
}
};
class LexerJSON : public ILexer {
OptionsJSON options;
OptionSetJSON optSetJSON;
EscapeSequence escapeSeq;
WordList keywordsJSON;
WordList keywordsJSONLD;
CharacterSet setOperators;
CharacterSet setURL;
CharacterSet setKeywordJSONLD;
CharacterSet setKeywordJSON;
CompactIRI compactIRI;
static bool IsNextNonWhitespace(LexAccessor &styler, Sci_Position start, char ch) {
Sci_Position i = 0;
while (i < 50) {
i++;
char curr = styler.SafeGetCharAt(start+i, '\0');
char next = styler.SafeGetCharAt(start+i+1, '\0');
bool atEOL = (curr == '\r' && next != '\n') || (curr == '\n');
if (curr == ch) {
return true;
} else if (!isspacechar(curr) || atEOL) {
return false;
}
}
return false;
}
/**
* Looks for the colon following the end quote
*
* Assumes property names of lengths no longer than a 100 characters.
* The colon is also expected to be less than 50 spaces after the end
* quote for the string to be considered a property name
*/
static bool AtPropertyName(LexAccessor &styler, Sci_Position start) {
Sci_Position i = 0;
bool escaped = false;
while (i < 100) {
i++;
char curr = styler.SafeGetCharAt(start+i, '\0');
if (escaped) {
escaped = false;
continue;
}
escaped = curr == '\\';
if (curr == '"') {
return IsNextNonWhitespace(styler, start+i, ':');
} else if (!curr) {
return false;
}
}
return false;
}
static bool IsNextWordInList(WordList &keywordList, CharacterSet wordSet,
StyleContext &context, LexAccessor &styler) {
char word[51];
Sci_Position currPos = (Sci_Position) context.currentPos;
int i = 0;
while (i < 50) {
char ch = styler.SafeGetCharAt(currPos + i);
if (!wordSet.Contains(ch)) {
break;
}
word[i] = ch;
i++;
}
word[i] = '\0';
return keywordList.InList(word);
}
public:
LexerJSON() :
setOperators(CharacterSet::setNone, "[{}]:,"),
setURL(CharacterSet::setAlphaNum, "-._~:/?#[]@!$&'()*+,),="),
setKeywordJSONLD(CharacterSet::setAlpha, ":@"),
setKeywordJSON(CharacterSet::setAlpha, "$_") {
}
virtual ~LexerJSON() {}
virtual int SCI_METHOD Version() const {
return lvOriginal;
}
virtual void SCI_METHOD Release() {
delete this;
}
virtual const char *SCI_METHOD PropertyNames() {
return optSetJSON.PropertyNames();
}
virtual int SCI_METHOD PropertyType(const char *name) {
return optSetJSON.PropertyType(name);
}
virtual const char *SCI_METHOD DescribeProperty(const char *name) {
return optSetJSON.DescribeProperty(name);
}
virtual Sci_Position SCI_METHOD PropertySet(const char *key, const char *val) {
if (optSetJSON.PropertySet(&options, key, val)) {
return 0;
}
return -1;
}
virtual Sci_Position SCI_METHOD WordListSet(int n, const char *wl) {
WordList *wordListN = 0;
switch (n) {
case 0:
wordListN = &keywordsJSON;
break;
case 1:
wordListN = &keywordsJSONLD;
break;
}
Sci_Position firstModification = -1;
if (wordListN) {
WordList wlNew;
wlNew.Set(wl);
if (*wordListN != wlNew) {
wordListN->Set(wl);
firstModification = 0;
}
}
return firstModification;
}
virtual void *SCI_METHOD PrivateCall(int, void *) {
return 0;
}
static ILexer *LexerFactoryJSON() {
return new LexerJSON;
}
virtual const char *SCI_METHOD DescribeWordListSets() {
return optSetJSON.DescribeWordListSets();
}
virtual void SCI_METHOD Lex(Sci_PositionU startPos,
Sci_Position length,
int initStyle,
IDocument *pAccess);
virtual void SCI_METHOD Fold(Sci_PositionU startPos,
Sci_Position length,
int initStyle,
IDocument *pAccess);
};
void SCI_METHOD LexerJSON::Lex(Sci_PositionU startPos,
Sci_Position length,
int initStyle,
IDocument *pAccess) {
LexAccessor styler(pAccess);
StyleContext context(startPos, length, initStyle, styler);
int stringStyleBefore = SCE_JSON_STRING;
while (context.More()) {
switch (context.state) {
case SCE_JSON_BLOCKCOMMENT:
if (context.Match("*/")) {
context.Forward();
context.ForwardSetState(SCE_JSON_DEFAULT);
}
break;
case SCE_JSON_LINECOMMENT:
if (context.atLineEnd) {
context.SetState(SCE_JSON_DEFAULT);
}
break;
case SCE_JSON_STRINGEOL:
if (context.atLineStart) {
context.SetState(SCE_JSON_DEFAULT);
}
break;
case SCE_JSON_ESCAPESEQUENCE:
escapeSeq.digitsLeft--;
if (!escapeSeq.atEscapeEnd()) {
if (escapeSeq.isInvalidChar(context.ch)) {
context.SetState(SCE_JSON_ERROR);
}
break;
}
if (context.ch == '"') {
context.SetState(stringStyleBefore);
context.ForwardSetState(SCE_C_DEFAULT);
} else if (context.ch == '\\') {
if (!escapeSeq.newSequence(context.chNext)) {
context.SetState(SCE_JSON_ERROR);
}
context.Forward();
} else {
context.SetState(stringStyleBefore);
if (context.atLineEnd) {
context.ChangeState(SCE_JSON_STRINGEOL);
}
}
break;
case SCE_JSON_PROPERTYNAME:
case SCE_JSON_STRING:
if (context.ch == '"') {
if (compactIRI.shouldHighlight()) {
context.ChangeState(SCE_JSON_COMPACTIRI);
context.ForwardSetState(SCE_JSON_DEFAULT);
compactIRI.resetState();
} else {
context.ForwardSetState(SCE_JSON_DEFAULT);
}
} else if (context.atLineEnd) {
context.ChangeState(SCE_JSON_STRINGEOL);
} else if (context.ch == '\\') {
stringStyleBefore = context.state;
if (options.escapeSequence) {
context.SetState(SCE_JSON_ESCAPESEQUENCE);
if (!escapeSeq.newSequence(context.chNext)) {
context.SetState(SCE_JSON_ERROR);
}
}
context.Forward();
} else if (context.Match("https://") ||
context.Match("http://") ||
context.Match("ssh://") ||
context.Match("git://") ||
context.Match("svn://") ||
context.Match("ftp://") ||
context.Match("mailto:")) {
// Handle most common URI schemes only
stringStyleBefore = context.state;
context.SetState(SCE_JSON_URI);
} else if (context.ch == '@') {
// https://www.w3.org/TR/json-ld/#dfn-keyword
if (IsNextWordInList(keywordsJSONLD, setKeywordJSONLD, context, styler)) {
stringStyleBefore = context.state;
context.SetState(SCE_JSON_LDKEYWORD);
}
} else {
compactIRI.checkChar(context.ch);
}
break;
case SCE_JSON_LDKEYWORD:
case SCE_JSON_URI:
if ((!setKeywordJSONLD.Contains(context.ch) &&
(context.state == SCE_JSON_LDKEYWORD)) ||
(!setURL.Contains(context.ch))) {
context.SetState(stringStyleBefore);
}
if (context.ch == '"') {
context.ForwardSetState(SCE_JSON_DEFAULT);
} else if (context.atLineEnd) {
context.ChangeState(SCE_JSON_STRINGEOL);
}
break;
case SCE_JSON_OPERATOR:
case SCE_JSON_NUMBER:
context.SetState(SCE_JSON_DEFAULT);
break;
case SCE_JSON_ERROR:
if (context.atLineEnd) {
context.SetState(SCE_JSON_DEFAULT);
}
break;
case SCE_JSON_KEYWORD:
if (!setKeywordJSON.Contains(context.ch)) {
context.SetState(SCE_JSON_DEFAULT);
}
break;
}
if (context.state == SCE_JSON_DEFAULT) {
if (context.ch == '"') {
compactIRI.resetState();
context.SetState(SCE_JSON_STRING);
Sci_Position currPos = static_cast<Sci_Position>(context.currentPos);
if (AtPropertyName(styler, currPos)) {
context.SetState(SCE_JSON_PROPERTYNAME);
}
} else if (setOperators.Contains(context.ch)) {
context.SetState(SCE_JSON_OPERATOR);
} else if (options.allowComments && context.Match("/*")) {
context.SetState(SCE_JSON_BLOCKCOMMENT);
context.Forward();
} else if (options.allowComments && context.Match("//")) {
context.SetState(SCE_JSON_LINECOMMENT);
} else if (setKeywordJSON.Contains(context.ch)) {
if (IsNextWordInList(keywordsJSON, setKeywordJSON, context, styler)) {
context.SetState(SCE_JSON_KEYWORD);
}
}
bool numberStart =
IsADigit(context.ch) && (context.chPrev == '+'||
context.chPrev == '-' ||
context.atLineStart ||
IsASpace(context.chPrev) ||
setOperators.Contains(context.chPrev));
bool exponentPart =
tolower(context.ch) == 'e' &&
IsADigit(context.chPrev) &&
(IsADigit(context.chNext) ||
context.chNext == '+' ||
context.chNext == '-');
bool signPart =
(context.ch == '-' || context.ch == '+') &&
((tolower(context.chPrev) == 'e' && IsADigit(context.chNext)) ||
((IsASpace(context.chPrev) || setOperators.Contains(context.chPrev))
&& IsADigit(context.chNext)));
bool adjacentDigit =
IsADigit(context.ch) && IsADigit(context.chPrev);
bool afterExponent = IsADigit(context.ch) && tolower(context.chPrev) == 'e';
bool dotPart = context.ch == '.' &&
IsADigit(context.chPrev) &&
IsADigit(context.chNext);
bool afterDot = IsADigit(context.ch) && context.chPrev == '.';
if (numberStart ||
exponentPart ||
signPart ||
adjacentDigit ||
dotPart ||
afterExponent ||
afterDot) {
context.SetState(SCE_JSON_NUMBER);
} else if (context.state == SCE_JSON_DEFAULT && !IsASpace(context.ch)) {
context.SetState(SCE_JSON_ERROR);
}
}
context.Forward();
}
context.Complete();
}
void SCI_METHOD LexerJSON::Fold(Sci_PositionU startPos,
Sci_Position length,
int,
IDocument *pAccess) {
if (!options.fold) {
return;
}
LexAccessor styler(pAccess);
Sci_PositionU currLine = styler.GetLine(startPos);
Sci_PositionU endPos = startPos + length;
int currLevel = styler.LevelAt(currLine) & SC_FOLDLEVELNUMBERMASK;
int nextLevel = currLevel;
int visibleChars = 0;
for (Sci_PositionU i = startPos; i < endPos; i++) {
char curr = styler.SafeGetCharAt(i);
char next = styler.SafeGetCharAt(i+1);
bool atEOL = (curr == '\r' && next != '\n') || (curr == '\n');
if (styler.StyleAt(i) == SCE_JSON_OPERATOR) {
if (curr == '{' || curr == '[') {
nextLevel++;
} else if (curr == '}' || curr == ']') {
nextLevel--;
}
}
if (atEOL || i == (endPos-1)) {
int level = currLevel;
if (!visibleChars && options.foldCompact) {
level |= SC_FOLDLEVELWHITEFLAG;
} else if (nextLevel > currLevel) {
level |= SC_FOLDLEVELHEADERFLAG;
}
if (level != styler.LevelAt(currLine)) {
styler.SetLevel(currLine, level);
}
currLine++;
currLevel = nextLevel;
visibleChars = 0;
}
if (!isspacechar(curr)) {
visibleChars++;
}
}
}
LexerModule lmJSON(SCLEX_JSON,
LexerJSON::LexerFactoryJSON,
"json",
JSONWordListDesc);

View File

@@ -78,6 +78,7 @@ int Scintilla_LinkLexers() {
//++Autogenerated -- run scripts/LexGen.py to regenerate
//**\(\tLINK_LEXER(\*);\n\)
LINK_LEXER(lmSQL);
LINK_LEXER(lmJSON);
//--Autogenerated -- end of automatically generated section

View File

@@ -31,11 +31,17 @@ EditDialog::EditDialog(QWidget* parent)
hexLayout->addWidget(hexEdit);
hexEdit->setOverwriteMode(false);
QHBoxLayout* jsonLayout = new QHBoxLayout(ui->editorJSON);
jsonEdit = new JsonTextEdit(this);
jsonLayout->addWidget(jsonEdit);
QShortcut* ins = new QShortcut(QKeySequence(Qt::Key_Insert), this);
connect(ins, SIGNAL(activated()), this, SLOT(toggleOverwriteMode()));
connect(ui->editorText, SIGNAL(textChanged()), this, SLOT(updateApplyButton()));
connect(hexEdit, SIGNAL(dataChanged()), this, SLOT(updateApplyButton()));
connect(jsonEdit, SIGNAL(textChanged()), this, SLOT(updateApplyButton()));
connect(jsonEdit, SIGNAL(textChanged()), this, SLOT(editTextChanged()));
reloadSettings();
}
@@ -89,6 +95,9 @@ void EditDialog::loadData(const QByteArray& data)
// Data type specific handling
switch (dataType) {
case Null:
// Set enabled any of the text widgets
ui->editorText->setEnabled(true);
jsonEdit->setEnabled(true);
switch (editMode) {
case TextEditor:
// The text widget buffer is now the main data source
@@ -96,10 +105,19 @@ void EditDialog::loadData(const QByteArray& data)
// Empty the text editor contents, then enable text editing
ui->editorText->clear();
ui->editorText->setEnabled(true);
break;
case JsonEditor:
// The JSON widget buffer is now the main data source
dataSource = JsonBuffer;
// Empty the text editor contents, then enable text editing
jsonEdit->clear();
break;
case HexEditor:
// The hex widget buffer is now the main data source
dataSource = HexBuffer;
@@ -124,6 +142,10 @@ void EditDialog::loadData(const QByteArray& data)
break;
case Text:
// Set enabled any of the text widgets
ui->editorText->setEnabled(true);
jsonEdit->setEnabled(true);
switch (editMode) {
case TextEditor:
// The text widget buffer is now the main data source
@@ -133,14 +155,24 @@ void EditDialog::loadData(const QByteArray& data)
textData = QString::fromUtf8(data.constData(), data.size());
ui->editorText->setPlainText(textData);
// Enable text editing
ui->editorText->setEnabled(true);
// Select all of the text by default
ui->editorText->selectAll();
break;
case JsonEditor:
// The JSON widget buffer is now the main data source
dataSource = JsonBuffer;
// Load the text into the text editor
textData = QString::fromUtf8(data.constData(), data.size());
jsonEdit->setText(textData);
// Select all of the text by default
jsonEdit->selectAll();
break;
case HexEditor:
// The hex widget buffer is now the main data source
dataSource = HexBuffer;
@@ -191,6 +223,12 @@ void EditDialog::loadData(const QByteArray& data)
ui->editorText->setEnabled(false);
break;
case JsonEditor:
// Disable text editing, and use a warning message as the contents
jsonEdit->setText(tr("Image data can't be viewed with the JSON editor"));
jsonEdit->setEnabled(false);
break;
case ImageViewer:
// Load the image into the image viewing widget
if (img.loadFromData(data)) {
@@ -219,6 +257,12 @@ void EditDialog::loadData(const QByteArray& data)
ui->editorText->setEnabled(false);
break;
case JsonEditor:
// Disable text editing, and use a warning message as the contents
jsonEdit->setText(QString(tr("Binary data can't be viewed with the JSON editor")));
jsonEdit->setEnabled(false);
break;
case ImageViewer:
// Clear any image from the image viewing widget
ui->editorImage->setPixmap(QPixmap(0,0));
@@ -281,12 +325,20 @@ void EditDialog::exportData()
QFile file(fileName);
if(file.open(QIODevice::WriteOnly))
{
if (dataSource == HexBuffer) {
switch (dataSource) {
case HexBuffer:
// Data source is the hex buffer
file.write(hexEdit->data());
} else {
break;
case TextBuffer:
// Data source is the text buffer
file.write(ui->editorText->toPlainText().toUtf8());
break;
case JsonBuffer:
// Data source is the JSON buffer
file.write(jsonEdit->text().toUtf8());
break;
}
file.close();
}
@@ -298,16 +350,19 @@ void EditDialog::setNull()
ui->editorText->clear();
ui->editorImage->clear();
hexEdit->setData(QByteArray());
jsonEdit->clear();
dataType = Null;
// Check if in text editor mode
int editMode = ui->editorStack->currentIndex();
if (editMode == TextEditor) {
if (editMode == TextEditor || editMode == JsonEditor) {
// Setting NULL in the text editor switches the data source to it
dataSource = TextBuffer;
// Ensure the text editor is enabled
ui->editorText->setEnabled(true);
// Ensure the JSON editor is enabled
jsonEdit->setEnabled(true);
// The text editor doesn't know the difference between an empty string
// and a NULL, so we need to record NULL outside of that
@@ -331,7 +386,8 @@ void EditDialog::accept()
if(!currentIndex.isValid())
return;
if (dataSource == TextBuffer) {
switch (dataSource) {
case TextBuffer:
// Check if a NULL is set in the text editor
if (textNullSet) {
emit recordTextUpdated(currentIndex, hexEdit->data(), true);
@@ -343,12 +399,28 @@ void EditDialog::accept()
// The data is different, so commit it back to the database
emit recordTextUpdated(currentIndex, newData.toUtf8(), false);
}
} else {
break;
case JsonBuffer:
// Check if a NULL is set in the text editor
if (textNullSet) {
emit recordTextUpdated(currentIndex, hexEdit->data(), true);
} else {
// It's not NULL, so proceed with normal text string checking
QString oldData = currentIndex.data(Qt::EditRole).toString();
QString newData = jsonEdit->text();
if (oldData != newData)
// The data is different, so commit it back to the database
emit recordTextUpdated(currentIndex, newData.toUtf8(), false);
}
break;
case HexBuffer:
// The data source is the hex widget buffer, thus binary data
QByteArray oldData = currentIndex.data(Qt::EditRole).toByteArray();
QByteArray newData = hexEdit->data();
if (newData != oldData)
emit recordTextUpdated(currentIndex, newData, true);
break;
}
}
@@ -356,12 +428,21 @@ void EditDialog::accept()
void EditDialog::editModeChanged(int newMode)
{
// * If the dataSource is the text buffer, the data is always text *
if (dataSource == TextBuffer) {
switch (dataSource) {
case TextBuffer:
switch (newMode) {
case TextEditor: // Switching to the text editor
// Nothing to do, as the text is already in the text buffer
break;
case JsonEditor: // Switching to the JSON editor
// Convert the text widget buffer for the JSON widget
jsonEdit->setText(ui->editorText->toPlainText().toUtf8());
// The JSON widget buffer is now the main data source
dataSource = JsonBuffer;
break;
case HexEditor: // Switching to the hex editor
// Convert the text widget buffer for the hex widget
hexEdit->setData(ui->editorText->toPlainText().toUtf8());
@@ -379,24 +460,56 @@ void EditDialog::editModeChanged(int newMode)
// Switch to the selected editor
ui->editorStack->setCurrentIndex(newMode);
return;
}
break;
case HexBuffer:
// * If the dataSource is the hex buffer, the contents could be anything
// so we just pass it to our loadData() function to handle *
if (dataSource == HexBuffer) {
// * If the dataSource is the hex buffer, the contents could be anything
// so we just pass it to our loadData() function to handle *
// Switch to the selected editor first, as loadData() relies on it
// being current
ui->editorStack->setCurrentIndex(newMode);
// Load the data into the appropriate widget, as done by loadData()
loadData(hexEdit->data());
}
break;
case JsonBuffer:
switch (newMode) {
case TextEditor: // Switching to the text editor
// Convert the text widget buffer for the JSON widget
ui->editorText->setText(jsonEdit->text());
// The Text widget buffer is now the main data source
dataSource = TextBuffer;
break;
case JsonEditor: // Switching to the JSON editor
// Nothing to do, as the text is already in the JSON buffer
break;
case HexEditor: // Switching to the hex editor
// Convert the text widget buffer for the hex widget
hexEdit->setData(jsonEdit->text().toUtf8());
// The hex widget buffer is now the main data source
dataSource = HexBuffer;
break;
case ImageViewer:
// Clear any image from the image viewing widget
ui->editorImage->setPixmap(QPixmap(0,0));
break;
}
// Switch to the selected editor
ui->editorStack->setCurrentIndex(newMode);
}
}
// Called for every keystroke in the text editor (only)
void EditDialog::editTextChanged()
{
if (dataSource == TextBuffer) {
if (dataSource == TextBuffer || dataSource == JsonBuffer) {
// Data has been changed in the text editor, so it can't be a NULL
// any more
textNullSet = false;
@@ -442,6 +555,7 @@ void EditDialog::toggleOverwriteMode()
hexEdit->setOverwriteMode(currentMode);
ui->editorText->setOverwriteMode(currentMode);
jsonEdit->setOverwriteMode(currentMode);
}
void EditDialog::setFocus()
@@ -467,6 +581,7 @@ void EditDialog::setReadOnly(bool ro)
ui->buttonImport->setEnabled(!ro);
ui->editorText->setReadOnly(ro);
ui->editorBinary->setEnabled(!ro); // We disable the entire hex editor here instead of setting it to read only because it doesn't have a setReadOnly() method
jsonEdit->setReadOnly(ro);
}
// Update the information labels in the bottom left corner of the dialog
@@ -544,9 +659,16 @@ QString EditDialog::humanReadableSize(double byteCount) const
void EditDialog::reloadSettings()
{
// Set the font for the text and hex editors
QFont editorFont(Settings::getValue("databrowser", "font").toString());
editorFont.setPointSize(Settings::getValue("databrowser", "fontsize").toInt());
ui->editorText->setFont(editorFont);
hexEdit->setFont(editorFont);
// Set the databrowser font for the text editor but the (SQL) editor
// font for hex editor, since it needs a Monospace font and the
// databrowser font would be usually of variable width.
QFont textFont(Settings::getValue("databrowser", "font").toString());
textFont.setPointSize(Settings::getValue("databrowser", "fontsize").toInt());
ui->editorText->setFont(textFont);
QFont hexFont(Settings::getValue("editor", "font").toString());
hexFont.setPointSize(Settings::getValue("databrowser", "fontsize").toInt());
hexEdit->setFont(hexFont);
jsonEdit->reloadSettings();
}

View File

@@ -4,6 +4,8 @@
#include <QDialog>
#include <QPersistentModelIndex>
#include "jsontextedit.h"
class QHexEdit;
namespace Ui {
@@ -47,6 +49,7 @@ signals:
private:
Ui::EditDialog* ui;
QHexEdit* hexEdit;
JsonTextEdit* jsonEdit;
QPersistentModelIndex currentIndex;
int dataSource;
int dataType;
@@ -55,7 +58,8 @@ private:
enum DataSources {
TextBuffer,
HexBuffer
HexBuffer,
JsonBuffer
};
enum DataTypes {
@@ -68,7 +72,8 @@ private:
enum EditModes {
TextEditor = 0,
HexEditor = 1,
ImageViewer = 2
JsonEditor = 2,
ImageViewer = 3
};
int checkDataType(const QByteArray& data);

View File

@@ -47,6 +47,11 @@
<string>Binary</string>
</property>
</item>
<item>
<property name="text">
<string>JSON</string>
</property>
</item>
<item>
<property name="text">
<string>Image</string>
@@ -151,6 +156,7 @@
</layout>
</widget>
<widget class="QWidget" name="editorBinary"/>
<widget class="QWidget" name="editorJSON"/>
<widget class="QScrollArea" name="editorImageScrollArea">
<property name="widgetResizable">
<bool>true</bool>

150
src/jsontextedit.cpp Normal file
View File

@@ -0,0 +1,150 @@
#include "jsontextedit.h"
#include "Settings.h"
#include <QFile>
#include <QDropEvent>
#include <QUrl>
#include <QMimeData>
#include <QDebug>
#include <cmath>
QsciLexerJSON* JsonTextEdit::jsonLexer = nullptr;
JsonTextEdit::JsonTextEdit(QWidget* parent) :
QsciScintilla(parent)
{
// Create lexer object if not done yet
if(jsonLexer == nullptr)
jsonLexer = new QsciLexerJSON(this);
// Set the lexer
setLexer(jsonLexer);
// Enable UTF8
setUtf8(true);
// Enable brace matching
setBraceMatching(QsciScintilla::SloppyBraceMatch);
// Enable auto indentation
setAutoIndent(true);
// Enable folding
setFolding(QsciScintilla::BoxedTreeFoldStyle);
jsonLexer->setFoldCompact(false);
// Set a sensible scroll width, so the scroll bar is avoided in
// most cases.
setScrollWidth(80);
// Scroll width is adjusted to ensure that all of the lines
// currently displayed can be completely scrolled. This mode never
// adjusts the scroll width to be narrower.
setScrollWidthTracking(true);
// Do rest of initialisation
reloadSettings();
// Connect signals
connect(this, SIGNAL(linesChanged()), this, SLOT(updateLineNumberAreaWidth()));
}
JsonTextEdit::~JsonTextEdit()
{
}
void JsonTextEdit::updateLineNumberAreaWidth()
{
// Calculate number of digits of the current number of lines
int digits = std::floor(std::log10(lines())) + 1;
// Calculate the width of this number if it was all zeros (this is because a 1 might require less space than a 0 and this could
// cause some flickering depending on the font) and set the new margin width.
QFont font = lexer()->font(QsciLexerJSON::Default);
setMarginWidth(0, QFontMetrics(font).width(QString("0").repeated(digits)) + 5);
}
void JsonTextEdit::dropEvent(QDropEvent* e)
{
QList<QUrl> urls = e->mimeData()->urls();
if(urls.isEmpty())
return QsciScintilla::dropEvent(e);
QString file = urls.first().toLocalFile();
if(!QFile::exists(file))
return;
QFile f(file);
f.open(QIODevice::ReadOnly);
setText(f.readAll());
f.close();
}
void JsonTextEdit::setupSyntaxHighlightingFormat(const QString& settings_name, int style)
{
jsonLexer->setColor(QColor(Settings::getValue("syntaxhighlighter", settings_name + "_colour").toString()), style);
QFont font(Settings::getValue("editor", "font").toString());
font.setPointSize(Settings::getValue("editor", "fontsize").toInt());
font.setBold(Settings::getValue("syntaxhighlighter", settings_name + "_bold").toBool());
font.setItalic(Settings::getValue("syntaxhighlighter", settings_name + "_italic").toBool());
font.setUnderline(Settings::getValue("syntaxhighlighter", settings_name + "_underline").toBool());
jsonLexer->setFont(font, style);
}
void JsonTextEdit::reloadKeywords()
{
// Set lexer again to reload the updated keywords list
setLexer(lexer());
}
void JsonTextEdit::reloadSettings()
{
// Enable auto completion if it hasn't been disabled
if(Settings::getValue("editor", "auto_completion").toBool())
{
setAutoCompletionThreshold(3);
setAutoCompletionCaseSensitivity(true);
setAutoCompletionShowSingle(true);
setAutoCompletionSource(QsciScintilla::AcsAPIs);
} else {
setAutoCompletionThreshold(0);
}
// Set syntax highlighting settings
QFont defaultfont(Settings::getValue("editor", "font").toString());
defaultfont.setStyleHint(QFont::TypeWriter);
defaultfont.setPointSize(Settings::getValue("editor", "fontsize").toInt());
jsonLexer->setFont(defaultfont);
setupSyntaxHighlightingFormat("comment", QsciLexerJSON::CommentLine);
setupSyntaxHighlightingFormat("comment", QsciLexerJSON::CommentBlock);
setupSyntaxHighlightingFormat("keyword", QsciLexerJSON::Keyword);
setupSyntaxHighlightingFormat("keyword", QsciLexerJSON::KeywordLD);
setupSyntaxHighlightingFormat("string", QsciLexerJSON::String);
setupSyntaxHighlightingFormat("table", QsciLexerJSON::Number);
setupSyntaxHighlightingFormat("identifier", QsciLexerJSON::Property);
jsonLexer->setHighlightComments(true);
// Set font
QFont font(Settings::getValue("editor", "font").toString());
font.setStyleHint(QFont::TypeWriter);
font.setPointSize(Settings::getValue("editor", "fontsize").toInt());
setFont(font);
// Show line numbers
QFont marginsfont(QFont(Settings::getValue("editor", "font").toString()));
marginsfont.setPointSize(font.pointSize());
setMarginsFont(marginsfont);
setMarginLineNumbers(0, true);
setMarginsBackgroundColor(Qt::lightGray);
updateLineNumberAreaWidth();
// Highlight current line
setCaretLineVisible(true);
setCaretLineBackgroundColor(QColor(Settings::getValue("syntaxhighlighter", "currentline_colour").toString()));
// Set tab width
setTabWidth(Settings::getValue("editor", "tabsize").toInt());
jsonLexer->refreshProperties();
}

35
src/jsontextedit.h Normal file
View File

@@ -0,0 +1,35 @@
#ifndef JSONTEXTEDIT_H
#define JSONTEXTEDIT_H
#include "Qsci/qsciscintilla.h"
#include <Qsci/qscilexerjson.h>
/**
* @brief The JsonTextEdit class
* This class is based on the QScintilla widget
*/
class JsonTextEdit : public QsciScintilla
{
Q_OBJECT
public:
explicit JsonTextEdit(QWidget *parent = nullptr);
virtual ~JsonTextEdit();
static QsciLexerJSON* jsonLexer;
public slots:
void reloadKeywords();
void reloadSettings();
protected:
void dropEvent(QDropEvent* e);
private:
void setupSyntaxHighlightingFormat(const QString& settings_name, int style);
private slots:
void updateLineNumberAreaWidth();
};
#endif