mirror of
https://github.com/sqlitebrowser/sqlitebrowser.git
synced 2026-01-20 11:00:44 -06:00
1150 lines
38 KiB
C++
1150 lines
38 KiB
C++
#include "EditDialog.h"
|
|
#include "ui_EditDialog.h"
|
|
#include "sqlitedb.h"
|
|
#include "Settings.h"
|
|
#include "qhexedit.h"
|
|
#include "docktextedit.h"
|
|
#include "FileDialog.h"
|
|
#include "Data.h"
|
|
|
|
#include <QMainWindow>
|
|
#include <QKeySequence>
|
|
#include <QShortcut>
|
|
#include <QImageReader>
|
|
#include <QModelIndex>
|
|
#include <QtXml/QDomDocument>
|
|
#include <QMessageBox>
|
|
#include <QPrinter>
|
|
#include <QPrintPreviewDialog>
|
|
#include <QPainter>
|
|
#include <QClipboard>
|
|
#include <QTextDocument>
|
|
|
|
#include <Qsci/qsciscintilla.h>
|
|
#include <json.hpp>
|
|
|
|
using json = nlohmann::json;
|
|
|
|
EditDialog::EditDialog(QWidget* parent)
|
|
: QDialog(parent),
|
|
ui(new Ui::EditDialog),
|
|
currentIndex(QModelIndex()),
|
|
dataSource(SciBuffer),
|
|
dataType(Null),
|
|
isReadOnly(true)
|
|
{
|
|
ui->setupUi(this);
|
|
|
|
// Add Ctrl-Enter (Cmd-Enter on OSX) as a shortcut for the Apply button
|
|
ui->buttonApply->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_Return));
|
|
ui->buttonApply->setToolTip(ui->buttonApply->toolTip() + " [" + ui->buttonApply->shortcut().toString(QKeySequence::NativeText) + "]");
|
|
|
|
QHBoxLayout* hexLayout = new QHBoxLayout(ui->editorBinary);
|
|
hexEdit = new QHexEdit(this);
|
|
hexLayout->addWidget(hexEdit);
|
|
hexEdit->setOverwriteMode(false);
|
|
|
|
QHBoxLayout* sciLayout = new QHBoxLayout(ui->editorSci);
|
|
sciEdit = new DockTextEdit(this);
|
|
sciLayout->addWidget(sciEdit);
|
|
|
|
QShortcut* ins = new QShortcut(QKeySequence(Qt::Key_Insert), this);
|
|
connect(ins, &QShortcut::activated, this, &EditDialog::toggleOverwriteMode);
|
|
|
|
connect(sciEdit, &DockTextEdit::textChanged, this, &EditDialog::updateApplyButton);
|
|
connect(sciEdit, &DockTextEdit::textChanged, this, &EditDialog::editTextChanged);
|
|
connect(ui->qtEdit, &QTextEdit::textChanged, this, &EditDialog::updateApplyButton);
|
|
connect(hexEdit, &QHexEdit::dataChanged, this, &EditDialog::updateApplyButton);
|
|
|
|
// Create shortcuts for the widgets that doesn't have its own print action or printing mechanism.
|
|
QShortcut* shortcutPrint = new QShortcut(QKeySequence::Print, this, nullptr, nullptr, Qt::WidgetShortcut);
|
|
connect(shortcutPrint, &QShortcut::activated, this, &EditDialog::openPrintDialog);
|
|
|
|
// Add actions to editors that have a context menu based on actions. This also activates the shortcuts.
|
|
ui->editorImage->addAction(ui->actionPrintImage);
|
|
ui->editorBinary->addAction(ui->actionPrint);
|
|
ui->editorBinary->addAction(ui->actionCopyHexAscii);
|
|
|
|
mustIndentAndCompact = Settings::getValue("databrowser", "indent_compact").toBool();
|
|
ui->actionIndent->setChecked(mustIndentAndCompact);
|
|
|
|
ui->buttonAutoSwitchMode->setChecked(Settings::getValue("databrowser", "auto_switch_mode").toBool());
|
|
ui->actionWordWrap->setChecked(Settings::getValue("databrowser", "editor_word_wrap").toBool());
|
|
setWordWrapping(ui->actionWordWrap->isChecked());
|
|
|
|
reloadSettings();
|
|
}
|
|
|
|
EditDialog::~EditDialog()
|
|
{
|
|
Settings::setValue("databrowser", "indent_compact", mustIndentAndCompact);
|
|
Settings::setValue("databrowser", "auto_switch_mode", ui->buttonAutoSwitchMode->isChecked());
|
|
Settings::setValue("databrowser", "editor_word_wrap", ui->actionWordWrap->isChecked());
|
|
delete ui;
|
|
}
|
|
|
|
void EditDialog::setCurrentIndex(const QModelIndex& idx)
|
|
{
|
|
currentIndex = QPersistentModelIndex(idx);
|
|
|
|
QByteArray bArrData = idx.data(Qt::EditRole).toByteArray();
|
|
loadData(bArrData);
|
|
updateCellInfoAndMode(bArrData);
|
|
|
|
ui->buttonApply->setDisabled(true);
|
|
}
|
|
|
|
void EditDialog::showEvent(QShowEvent*)
|
|
{
|
|
// Whenever the dialog is shown, position it at the center of the parent dialog
|
|
QMainWindow* parentDialog = qobject_cast<QMainWindow*>(parent());
|
|
if(parentDialog)
|
|
{
|
|
move(parentDialog->x() + parentDialog->width() / 2 - width() / 2,
|
|
parentDialog->y() + parentDialog->height() / 2 - height() / 2);
|
|
}
|
|
}
|
|
|
|
void EditDialog::reject()
|
|
{
|
|
// We override this, to ensure the Escape key doesn't make the Edit Cell
|
|
// dock go away
|
|
return;
|
|
}
|
|
|
|
// Loads data from a cell into the Edit Cell window
|
|
void EditDialog::loadData(const QByteArray& bArrdata)
|
|
{
|
|
QImage img;
|
|
|
|
// Clear previously removed BOM
|
|
removedBom.clear();
|
|
|
|
// Determine the data type, saving that info in the class variable
|
|
dataType = checkDataType(bArrdata);
|
|
|
|
// Get the current editor mode (eg text, hex, image, json or xml mode)
|
|
int editMode = ui->comboMode->currentIndex();
|
|
|
|
// Data type specific handling
|
|
switch (dataType) {
|
|
case Null:
|
|
// Set enabled the text widget
|
|
sciEdit->setEnabled(true);
|
|
switch (editMode) {
|
|
case TextEditor:
|
|
case JsonEditor:
|
|
case XmlEditor:
|
|
|
|
// The JSON widget buffer is now the main data source
|
|
dataSource = SciBuffer;
|
|
|
|
// Empty the text editor contents, then enable text editing
|
|
sciEdit->clear();
|
|
|
|
break;
|
|
|
|
case RtlTextEditor:
|
|
// The text widget buffer is now the main data source
|
|
dataSource = QtBuffer;
|
|
|
|
// Empty the Qt text editor contents, then enable text editing
|
|
ui->qtEdit->clear();
|
|
|
|
break;
|
|
|
|
case HexEditor:
|
|
// The hex widget buffer is now the main data source
|
|
dataSource = HexBuffer;
|
|
|
|
// Load the Null into the hex editor
|
|
hexEdit->setData(bArrdata);
|
|
|
|
break;
|
|
|
|
case ImageViewer:
|
|
// The hex widget buffer is now the main data source
|
|
dataSource = HexBuffer;
|
|
|
|
// Clear any image from the image viewing widget
|
|
ui->editorImage->setPixmap(QPixmap(0,0));
|
|
|
|
// Load the Null into the hex editor
|
|
hexEdit->setData(bArrdata);
|
|
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case Text:
|
|
case RtlText:
|
|
case JSON:
|
|
case XML:
|
|
// Can be stored in any widget, except the ImageViewer
|
|
|
|
switch (editMode) {
|
|
case TextEditor:
|
|
case JsonEditor:
|
|
case XmlEditor:
|
|
setDataInBuffer(bArrdata, SciBuffer);
|
|
break;
|
|
case RtlTextEditor:
|
|
setDataInBuffer(bArrdata, QtBuffer);
|
|
break;
|
|
case HexEditor:
|
|
setDataInBuffer(bArrdata, HexBuffer);
|
|
break;
|
|
case ImageViewer:
|
|
// The image viewer cannot hold data nor display text.
|
|
|
|
// Clear any image from the image viewing widget
|
|
ui->editorImage->setPixmap(QPixmap(0,0));
|
|
|
|
// Load the text into the text editor
|
|
setDataInBuffer(bArrdata, SciBuffer);
|
|
|
|
break;
|
|
}
|
|
break;
|
|
|
|
case Image:
|
|
// Image data is kept in the hex widget, mainly for safety. If we
|
|
// stored it in the editorImage widget instead, it would be a pixmap
|
|
// and there's no good way to restore that back to the original
|
|
// (pristine) image data. eg image metadata would be lost
|
|
setDataInBuffer(bArrdata, HexBuffer);
|
|
|
|
// Update the display if in text edit or image viewer mode
|
|
switch (editMode) {
|
|
case TextEditor:
|
|
case XmlEditor:
|
|
case JsonEditor:
|
|
// Disable text editing, and use a warning message as the contents
|
|
sciEdit->setText(tr("Image data can't be viewed in this mode.") % '\n' %
|
|
tr("Try switching to Image or Binary mode."));
|
|
sciEdit->setTextInMargin(tr("Image"));
|
|
sciEdit->setEnabled(false);
|
|
break;
|
|
|
|
case RtlTextEditor:
|
|
// Disable text editing, and use a warning message as the contents
|
|
ui->qtEdit->setText(QString("<i>" %
|
|
tr("Image data can't be viewed in this mode.") % "<br/>" %
|
|
tr("Try switching to Image or Binary mode.") %
|
|
"</i>"));
|
|
ui->qtEdit->setEnabled(false);
|
|
break;
|
|
|
|
case ImageViewer:
|
|
// Load the image into the image viewing widget
|
|
if (img.loadFromData(bArrdata)) {
|
|
ui->editorImage->setPixmap(QPixmap::fromImage(img));
|
|
}
|
|
break;
|
|
}
|
|
break;
|
|
case SVG:
|
|
// Set the XML data in any buffer or update image in image viewer mode
|
|
switch (editMode) {
|
|
case TextEditor:
|
|
case JsonEditor:
|
|
case XmlEditor:
|
|
|
|
setDataInBuffer(bArrdata, SciBuffer);
|
|
break;
|
|
|
|
case HexEditor:
|
|
|
|
setDataInBuffer(bArrdata, HexBuffer);
|
|
break;
|
|
|
|
case ImageViewer:
|
|
// Set data in the XML (Sci) Buffer and load the SVG Image
|
|
setDataInBuffer(bArrdata, SciBuffer);
|
|
sciEdit->setLanguage(DockTextEdit::XML);
|
|
|
|
// Load the image into the image viewing widget
|
|
if (img.loadFromData(bArrdata)) {
|
|
ui->editorImage->setPixmap(QPixmap::fromImage(img));
|
|
}
|
|
break;
|
|
case RtlTextEditor:
|
|
setDataInBuffer(bArrdata, QtBuffer);
|
|
break;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
|
|
// The data seems to be general binary data, which is always loaded
|
|
// into the hex widget (the only safe place for it)
|
|
|
|
// Load the data into the hex buffer
|
|
setDataInBuffer(bArrdata, HexBuffer);
|
|
|
|
switch (editMode) {
|
|
case TextEditor:
|
|
case JsonEditor:
|
|
case XmlEditor:
|
|
// Disable text editing, and use a warning message as the contents
|
|
sciEdit->setText(QString(tr("Binary data can't be viewed in this mode.") % '\n' %
|
|
tr("Try switching to Binary mode.")));
|
|
sciEdit->setTextInMargin(Settings::getValue("databrowser", "blob_text").toString());
|
|
sciEdit->setEnabled(false);
|
|
break;
|
|
|
|
case RtlTextEditor:
|
|
// Disable text editing, and use a warning message as the contents
|
|
ui->qtEdit->setText(QString("<i>" %
|
|
tr("Binary data can't be viewed in this mode.") % "<br/>" %
|
|
tr("Try switching to Binary mode.") %
|
|
"</i>"));
|
|
ui->qtEdit->setEnabled(false);
|
|
break;
|
|
|
|
case ImageViewer:
|
|
// Clear any image from the image viewing widget
|
|
ui->editorImage->setPixmap(QPixmap(0,0));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void EditDialog::importData()
|
|
{
|
|
// Get list of supported image file formats to include them in the file dialog filter
|
|
QString image_formats;
|
|
QList<QByteArray> image_formats_list = QImageReader::supportedImageFormats();
|
|
for(int i=0;i<image_formats_list.size();++i)
|
|
image_formats.append(QString("*.%1 ").arg(QString::fromUtf8(image_formats_list.at(i))));
|
|
// Chop last space
|
|
image_formats.chop(1);
|
|
|
|
QStringList filters;
|
|
filters << FILE_FILTER_TXT
|
|
<< FILE_FILTER_JSON
|
|
<< FILE_FILTER_XML
|
|
<< tr("Image files (%1)").arg(image_formats)
|
|
<< FILE_FILTER_BIN
|
|
<< FILE_FILTER_ALL;
|
|
|
|
QString selectedFilter;
|
|
|
|
// Get the current editor mode (eg text, hex, image, json or xml mode)
|
|
int mode = ui->comboMode->currentIndex();
|
|
|
|
// Set as selected filter the appropriate for the current mode.
|
|
switch (mode) {
|
|
case TextEditor:
|
|
case RtlTextEditor:
|
|
selectedFilter = FILE_FILTER_TXT;
|
|
break;
|
|
case HexEditor:
|
|
selectedFilter = FILE_FILTER_BIN;
|
|
break;
|
|
case ImageViewer:
|
|
selectedFilter = tr("Image files (%1)").arg(image_formats);
|
|
break;
|
|
case JsonEditor:
|
|
selectedFilter = FILE_FILTER_JSON;
|
|
break;
|
|
case XmlEditor:
|
|
selectedFilter = FILE_FILTER_XML;
|
|
break;
|
|
}
|
|
QString fileName = FileDialog::getOpenFileName(
|
|
OpenDataFile,
|
|
this,
|
|
tr("Choose a file to import")
|
|
#ifndef Q_OS_MAC // Filters on OS X are buggy
|
|
, filters.join(";;")
|
|
, &selectedFilter
|
|
#endif
|
|
);
|
|
if(QFile::exists(fileName))
|
|
{
|
|
QFile file(fileName);
|
|
if(file.open(QIODevice::ReadOnly))
|
|
{
|
|
QByteArray d = file.readAll();
|
|
loadData(d);
|
|
file.close();
|
|
|
|
// Update the cell data info in the bottom left of the Edit Cell
|
|
// and update mode (if required) to the just imported data type.
|
|
updateCellInfoAndMode(d);
|
|
}
|
|
}
|
|
}
|
|
|
|
void EditDialog::exportData()
|
|
{
|
|
QStringList filters;
|
|
switch (dataType) {
|
|
case Image: {
|
|
// Images get special treatment.
|
|
// Determine the likely filename extension.
|
|
QByteArray cellData = hexEdit->data();
|
|
QBuffer imageBuffer(&cellData);
|
|
QImageReader imageReader(&imageBuffer);
|
|
QString imageFormat = imageReader.format();
|
|
filters << tr("%1 Image").arg(imageFormat.toUpper()) % " (*." % imageFormat.toLower() % ")";
|
|
break;
|
|
}
|
|
case Binary:
|
|
filters << tr("Binary files (*.bin)");
|
|
break;
|
|
case RtlText:
|
|
case Text:
|
|
// Include the XML case on the text data type, since XML detection is not very sofisticated.
|
|
if (ui->comboMode->currentIndex() == XmlEditor)
|
|
filters << FILE_FILTER_XML
|
|
<< FILE_FILTER_TXT;
|
|
else
|
|
filters << FILE_FILTER_TXT
|
|
<< FILE_FILTER_XML;
|
|
break;
|
|
case JSON:
|
|
filters << FILE_FILTER_JSON;
|
|
break;
|
|
case SVG:
|
|
filters << FILE_FILTER_SVG;
|
|
break;
|
|
case XML:
|
|
filters << FILE_FILTER_XML;
|
|
break;
|
|
case Null:
|
|
return;
|
|
}
|
|
|
|
if (dataSource == HexBuffer)
|
|
filters << FILE_FILTER_HEX;
|
|
|
|
filters << FILE_FILTER_ALL;
|
|
|
|
QString selectedFilter = filters.first();
|
|
QString fileName = FileDialog::getSaveFileName(
|
|
CreateDataFile,
|
|
this,
|
|
tr("Choose a filename to export data"),
|
|
filters.join(";;"),
|
|
/* defaultFileName */ QString(),
|
|
&selectedFilter);
|
|
if(fileName.size() > 0)
|
|
{
|
|
QFile file(fileName);
|
|
if(file.open(QIODevice::WriteOnly))
|
|
{
|
|
switch (dataSource) {
|
|
case HexBuffer:
|
|
// Data source is the hex buffer
|
|
// If text option has been selected, the readable representation of the content is saved to file.
|
|
if (selectedFilter == FILE_FILTER_HEX)
|
|
file.write(hexEdit->toReadableString().toUtf8());
|
|
else
|
|
file.write(hexEdit->data());
|
|
break;
|
|
case SciBuffer:
|
|
// Data source is the Scintilla buffer
|
|
file.write(sciEdit->text().toUtf8());
|
|
break;
|
|
case QtBuffer:
|
|
// Data source is the text buffer
|
|
file.write(ui->qtEdit->toPlainText().toUtf8());
|
|
break;
|
|
}
|
|
file.close();
|
|
}
|
|
}
|
|
}
|
|
|
|
void EditDialog::setNull()
|
|
{
|
|
ui->qtEdit->clear();
|
|
ui->editorImage->clear();
|
|
hexEdit->setData(QByteArray());
|
|
sciEdit->clear();
|
|
dataType = Null;
|
|
removedBom.clear();
|
|
|
|
// The text editors don't know the difference between an empty string
|
|
// and a NULL, so we need to record NULL outside of that
|
|
dataType = Null;
|
|
|
|
// Ensure the text (Scintilla) editor is enabled
|
|
sciEdit->setEnabled(true);
|
|
|
|
// Update the cell data info in the bottom left of the Edit Cell
|
|
// The mode is also (if required) updated to text since it gives
|
|
// the better visual clue of containing a NULL value (placeholder).
|
|
updateCellInfoAndMode(hexEdit->data());
|
|
|
|
sciEdit->setFocus();
|
|
}
|
|
|
|
void EditDialog::updateApplyButton()
|
|
{
|
|
if (!isReadOnly)
|
|
ui->buttonApply->setEnabled(true);
|
|
}
|
|
|
|
bool EditDialog::promptInvalidData(const QString& data_type, const QString& errorString)
|
|
{
|
|
QMessageBox::StandardButton reply = QMessageBox::question(
|
|
this,
|
|
tr("Invalid data for this mode"),
|
|
tr("The cell contains invalid %1 data. Reason: %2. Do you really want to apply it to the cell?").arg(data_type, errorString),
|
|
QMessageBox::Apply | QMessageBox::Cancel);
|
|
return (reply == QMessageBox::Apply);
|
|
}
|
|
|
|
void EditDialog::accept()
|
|
{
|
|
if(!currentIndex.isValid())
|
|
return;
|
|
|
|
if (dataType == Null) {
|
|
emit recordTextUpdated(currentIndex, hexEdit->data(), true);
|
|
return;
|
|
}
|
|
|
|
// New data for the plain text cases. This is processed after the case.
|
|
QString newTextData;
|
|
|
|
switch (dataSource) {
|
|
case QtBuffer:
|
|
newTextData = removedBom + ui->qtEdit->toPlainText();
|
|
break;
|
|
case SciBuffer:
|
|
switch (sciEdit->language()) {
|
|
case DockTextEdit::PlainText:
|
|
newTextData = removedBom + sciEdit->text();
|
|
break;
|
|
case DockTextEdit::JSON:
|
|
{
|
|
sciEdit->clearErrorIndicators();
|
|
|
|
QString oldData = currentIndex.data(Qt::EditRole).toString();
|
|
|
|
QString newData;
|
|
bool proceed = true;
|
|
json jsonDoc;
|
|
|
|
try {
|
|
jsonDoc = json::parse(sciEdit->text().toStdString());
|
|
} catch(json::parse_error& parseError) {
|
|
sciEdit->setErrorIndicator(static_cast<int>(parseError.byte - 1));
|
|
|
|
proceed = promptInvalidData("JSON", parseError.what());
|
|
}
|
|
|
|
if (!jsonDoc.is_null()) {
|
|
if (mustIndentAndCompact)
|
|
// Compact the JSON data before storing
|
|
newData = QString::fromStdString(jsonDoc.dump());
|
|
else
|
|
newData = sciEdit->text();
|
|
} else {
|
|
newData = sciEdit->text();
|
|
}
|
|
proceed = proceed && (oldData != newData);
|
|
|
|
if (proceed)
|
|
// The data is different, so commit it back to the database
|
|
emit recordTextUpdated(currentIndex, newData.toUtf8(), false);
|
|
}
|
|
break;
|
|
case DockTextEdit::XML:
|
|
{
|
|
QString oldData = currentIndex.data(Qt::EditRole).toString();
|
|
|
|
QString newData;
|
|
QDomDocument xmlDoc;
|
|
QString errorMsg;
|
|
int errorLine, errorColumn;
|
|
|
|
bool isValid = xmlDoc.setContent(sciEdit->text().toUtf8(), true, &errorMsg, &errorLine, &errorColumn);
|
|
bool proceed;
|
|
|
|
sciEdit->clearErrorIndicators();
|
|
if (!isValid) {
|
|
sciEdit->setErrorIndicator(errorLine-1, errorColumn-1, errorLine, 0);
|
|
newData = sciEdit->text();
|
|
proceed = (oldData != newData && promptInvalidData("XML", errorMsg));
|
|
} else {
|
|
if (mustIndentAndCompact)
|
|
// Compact the XML data before storing. If indent is -1, no whitespace at all is added.
|
|
newData = xmlDoc.toString(-1);
|
|
else
|
|
newData = sciEdit->text();
|
|
proceed = (oldData != newData);
|
|
}
|
|
if (proceed)
|
|
// The data is different, so commit it back to the database
|
|
emit recordTextUpdated(currentIndex, newData.toUtf8(), false);
|
|
}
|
|
break;
|
|
}
|
|
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;
|
|
}
|
|
|
|
if (!newTextData.isEmpty()) {
|
|
|
|
QString oldData = currentIndex.data(Qt::EditRole).toString();
|
|
// Check first for null case, otherwise empty strings cannot overwrite NULL values
|
|
if ((currentIndex.data(Qt::EditRole).isNull() && dataType != Null) || oldData != newTextData)
|
|
// The data is different, so commit it back to the database
|
|
emit recordTextUpdated(currentIndex, removedBom + newTextData.toUtf8(), false);
|
|
}
|
|
}
|
|
|
|
void EditDialog::setDataInBuffer(const QByteArray& bArrdata, DataSources source)
|
|
{
|
|
dataSource = source;
|
|
QString textData;
|
|
|
|
// 1) Perform validation and text formatting (if applicable).
|
|
// 2) Set the text in the corresponding editor widget (the text widget for the Image case).
|
|
// 3) Enable the widget.
|
|
switch (dataSource) {
|
|
case QtBuffer:
|
|
{
|
|
// Load the text into the text editor, remove BOM first if there is one
|
|
QByteArray dataWithoutBom = bArrdata;
|
|
removedBom = removeBom(dataWithoutBom);
|
|
|
|
textData = QString::fromUtf8(dataWithoutBom.constData(), dataWithoutBom.size());
|
|
ui->qtEdit->setPlainText(textData);
|
|
|
|
// Select all of the text by default (this is useful for simple text data that we usually edit as a whole).
|
|
// We don't want this when the QtBuffer has been automatically switched due to the insertion of RTL text,
|
|
// (detected through the state of the apply button) otherwise that would break the typing flow of the user.
|
|
if (!isReadOnly)
|
|
{
|
|
if (!ui->buttonApply->isEnabled())
|
|
ui->qtEdit->selectAll();
|
|
else
|
|
ui->qtEdit->moveCursor(QTextCursor::End);
|
|
}
|
|
ui->qtEdit->setEnabled(true);
|
|
break;
|
|
}
|
|
case SciBuffer:
|
|
switch (sciEdit->language()) {
|
|
case DockTextEdit::PlainText:
|
|
{
|
|
// Load the text into the text editor, remove BOM first if there is one
|
|
QByteArray dataWithoutBom = bArrdata;
|
|
removedBom = removeBom(dataWithoutBom);
|
|
|
|
textData = QString::fromUtf8(dataWithoutBom.constData(), dataWithoutBom.size());
|
|
sciEdit->setText(textData);
|
|
|
|
// Select all of the text by default (this is useful for simple text data that we usually edit as a whole)
|
|
if (!isReadOnly)
|
|
sciEdit->selectAll();
|
|
sciEdit->setEnabled(true);
|
|
break;
|
|
}
|
|
case DockTextEdit::JSON:
|
|
{
|
|
sciEdit->clearErrorIndicators();
|
|
|
|
json jsonDoc;
|
|
|
|
try {
|
|
jsonDoc = json::parse(std::string(bArrdata.constData(), static_cast<size_t>(bArrdata.size())));
|
|
} catch(json::parse_error& parseError) {
|
|
sciEdit->setErrorIndicator(static_cast<int>(parseError.byte - 1));
|
|
}
|
|
|
|
if (mustIndentAndCompact && !jsonDoc.is_null() && !jsonDoc.is_discarded()) {
|
|
// Load indented JSON into the JSON editor
|
|
textData = QString::fromStdString(jsonDoc.dump(4));
|
|
} else {
|
|
// Fallback case. The data is not yet valid JSON or no auto-formatting applied.
|
|
textData = QString::fromUtf8(bArrdata.constData(), bArrdata.size());
|
|
}
|
|
|
|
sciEdit->setText(textData);
|
|
sciEdit->setEnabled(true);
|
|
}
|
|
|
|
break;
|
|
|
|
case DockTextEdit::XML:
|
|
{
|
|
QString errorMsg;
|
|
int errorLine, errorColumn;
|
|
QDomDocument xmlDoc;
|
|
bool isValid = xmlDoc.setContent(bArrdata, true, &errorMsg, &errorLine, &errorColumn);
|
|
|
|
if (mustIndentAndCompact && isValid) {
|
|
// Load indented XML into the XML editor
|
|
textData = xmlDoc.toString(Settings::getValue("editor", "tabsize").toInt());
|
|
} else {
|
|
// Fallback case. The data is not yet valid JSON or no auto-formatting applied.
|
|
textData = QString::fromUtf8(bArrdata.constData(), bArrdata.size());
|
|
}
|
|
sciEdit->setText(textData);
|
|
|
|
sciEdit->clearErrorIndicators();
|
|
if (!isValid)
|
|
// Adjust line and column by one (Scintilla starts at 1 and QDomDocument at 0)
|
|
sciEdit->setErrorIndicator(errorLine-1, errorColumn-1, errorLine, 0);
|
|
sciEdit->setEnabled(true);
|
|
|
|
}
|
|
break;
|
|
}
|
|
break;
|
|
case HexBuffer:
|
|
hexEdit->setData(bArrdata);
|
|
hexEdit->setEnabled(true);
|
|
|
|
break;
|
|
}
|
|
|
|
}
|
|
|
|
// Called when the user manually changes the "Mode" drop down combobox
|
|
void EditDialog::editModeChanged(int newMode)
|
|
{
|
|
ui->actionIndent->setEnabled(newMode == JsonEditor || newMode == XmlEditor);
|
|
setStackCurrentIndex(newMode);
|
|
|
|
// * If the dataSource is the text buffer, the data is always text *
|
|
switch (dataSource) {
|
|
case QtBuffer:
|
|
switch (newMode) {
|
|
case RtlTextEditor: // Switching to the RTL text editor
|
|
// Nothing to do, as the text is already in the Qt buffer
|
|
break;
|
|
|
|
case TextEditor: // Switching to one of the Scintilla editor modes
|
|
case JsonEditor:
|
|
case XmlEditor:
|
|
|
|
setDataInBuffer(ui->qtEdit->toPlainText().toUtf8(), SciBuffer);
|
|
break;
|
|
|
|
case HexEditor: // Switching to the hex editor
|
|
// Convert the text widget buffer for the hex widget
|
|
// The hex widget buffer is now the main data source
|
|
setDataInBuffer(removedBom + ui->qtEdit->toPlainText().toUtf8(), HexBuffer);
|
|
break;
|
|
|
|
case ImageViewer:
|
|
// Clear any image from the image viewing widget
|
|
ui->editorImage->setPixmap(QPixmap(0,0));
|
|
break;
|
|
}
|
|
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 *
|
|
// Note that we have already set the editor, as loadData() relies on it
|
|
// being current
|
|
|
|
// Load the data into the appropriate widget, as done by loadData()
|
|
loadData(hexEdit->data());
|
|
break;
|
|
case SciBuffer:
|
|
switch (newMode) {
|
|
case RtlTextEditor: // Switching to the RTL text editor
|
|
// Convert the Scintilla widget buffer for the Qt widget
|
|
setDataInBuffer(sciEdit->text().toUtf8(), QtBuffer);
|
|
break;
|
|
case HexEditor: // Switching to the hex editor
|
|
// Convert the text widget buffer for the hex widget
|
|
setDataInBuffer(sciEdit->text().toUtf8(), HexBuffer);
|
|
break;
|
|
case ImageViewer:
|
|
{
|
|
// When SVG format, load the image, else clear it.
|
|
QByteArray bArrdata = sciEdit->text().toUtf8();
|
|
dataType = checkDataType(bArrdata);
|
|
if (dataType == SVG) {
|
|
QImage img;
|
|
|
|
if (img.loadFromData(bArrdata))
|
|
ui->editorImage->setPixmap(QPixmap::fromImage(img));
|
|
else
|
|
// Clear any image from the image viewing widget
|
|
ui->editorImage->setPixmap(QPixmap(0,0));
|
|
}
|
|
}
|
|
break;
|
|
|
|
case TextEditor: // Switching to the text editor
|
|
case JsonEditor: // Switching to the JSON editor
|
|
case XmlEditor: // Switching to the XML editor
|
|
// The text is already in the Sci buffer but we need to perform the necessary formatting.
|
|
setDataInBuffer(sciEdit->text().toUtf8(), SciBuffer);
|
|
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Called for every keystroke in the text editor (only)
|
|
void EditDialog::editTextChanged()
|
|
{
|
|
if (dataSource == SciBuffer || dataSource == QtBuffer) {
|
|
|
|
// Update the cell info in the bottom left manually. This is because
|
|
// updateCellInfoAndMode() only works with QByteArray's (for now)
|
|
int dataLength;
|
|
bool isModified;
|
|
|
|
if(dataSource == QtBuffer) {
|
|
// QtBuffer
|
|
dataLength = ui->qtEdit->toPlainText().length();
|
|
isModified = ui->qtEdit->document()->isModified();
|
|
} else {
|
|
// SciBuffer
|
|
dataLength = sciEdit->text().length();
|
|
isModified = sciEdit->isModified();
|
|
|
|
// Switch to the Qt Editor if we detect right-to-left text,
|
|
// since the QScintilla editor does not support it.
|
|
if (containsRightToLeft(sciEdit->text()))
|
|
ui->comboMode->setCurrentIndex(RtlTextEditor);
|
|
}
|
|
|
|
// If data has been entered in the text editor, it can't be a NULL
|
|
// any more. It hasn't been validated yet, so it cannot be JSON nor XML.
|
|
if (dataType == Null && isModified && dataLength != 0)
|
|
dataType = Text;
|
|
|
|
if (dataType != Null) {
|
|
sciEdit->clearTextInMargin();
|
|
ui->labelType->setText(tr("Type of data currently in cell: Text / Numeric"));
|
|
}
|
|
ui->labelSize->setText(tr("%n character(s)", "", dataLength));
|
|
}
|
|
}
|
|
|
|
void EditDialog::setMustIndentAndCompact(bool enable)
|
|
{
|
|
mustIndentAndCompact = enable;
|
|
|
|
// Indent or compact if necessary. If data has changed (button Apply indicates so), reload from the widget, else from the table.
|
|
if (ui->buttonApply->isEnabled()) {
|
|
setDataInBuffer(sciEdit->text().toUtf8(), SciBuffer);
|
|
} else
|
|
setCurrentIndex(currentIndex);
|
|
}
|
|
|
|
// Determine the type of data in the cell
|
|
int EditDialog::checkDataType(const QByteArray& bArrdata) const
|
|
{
|
|
QByteArray cellData = bArrdata;
|
|
|
|
// Check for NULL data type
|
|
if (cellData.isNull()) {
|
|
return Null;
|
|
}
|
|
|
|
// Check if it's an image
|
|
QString imageFormat = isImageData(cellData);
|
|
if(!imageFormat.isNull())
|
|
return imageFormat == "svg" ? SVG : Image;
|
|
|
|
// Check if it's text only
|
|
if(isTextOnly(cellData))
|
|
{
|
|
if (cellData.startsWith("<?xml"))
|
|
return XML;
|
|
|
|
auto json_parse_result = json::parse(cellData, nullptr, false);
|
|
if(!json_parse_result.is_discarded() && !json_parse_result.is_number())
|
|
return JSON;
|
|
else {
|
|
if (containsRightToLeft(QString::fromUtf8(cellData)))
|
|
return RtlText;
|
|
else
|
|
return Text;
|
|
}
|
|
}
|
|
|
|
// It's none of the above, so treat it as general binary data
|
|
return Binary;
|
|
}
|
|
|
|
void EditDialog::toggleOverwriteMode()
|
|
{
|
|
static bool currentMode = false;
|
|
currentMode = !currentMode;
|
|
|
|
hexEdit->setOverwriteMode(currentMode);
|
|
ui->qtEdit->setOverwriteMode(currentMode);
|
|
sciEdit->setOverwriteMode(currentMode);
|
|
}
|
|
|
|
void EditDialog::setFocus()
|
|
{
|
|
QDialog::setFocus();
|
|
|
|
// Set the focus to the editor widget. The idea here is that setting focus
|
|
// to the dock itself doesn't make much sense as it's just a frame; you'd
|
|
// have to tab to the editor which is what you most likely want to use. So
|
|
// in order to save the user from doing this we explicitly set the focus
|
|
// to the current editor.
|
|
int editMode = ui->editorStack->currentIndex();
|
|
|
|
switch (editMode) {
|
|
case TextEditor:
|
|
sciEdit->setFocus();
|
|
if (sciEdit->language() == DockTextEdit::PlainText && !isReadOnly)
|
|
sciEdit->selectAll();
|
|
break;
|
|
case RtlTextEditor:
|
|
ui->qtEdit->setFocus();
|
|
ui->qtEdit->selectAll();
|
|
break;
|
|
case HexEditor:
|
|
hexEdit->setFocus();
|
|
break;
|
|
case ImageViewer:
|
|
// Nothing to do
|
|
break;
|
|
}
|
|
|
|
}
|
|
|
|
// Enables or disables the Apply, Null, & Import buttons in the Edit Cell dock.
|
|
// Sets or unsets read-only properties for the editors.
|
|
void EditDialog::setReadOnly(bool ro)
|
|
{
|
|
isReadOnly = ro;
|
|
|
|
ui->buttonApply->setEnabled(!ro);
|
|
ui->actionNull->setEnabled(!ro);
|
|
ui->actionImport->setEnabled(!ro);
|
|
|
|
ui->qtEdit->setReadOnly(ro);
|
|
sciEdit->setReadOnly(ro);
|
|
hexEdit->setReadOnly(ro);
|
|
|
|
// This makes the caret being visible for selection, although the editor is read-only. The read-only state is hinted by the
|
|
// caret not blinking. The same is done in ExtendedScintilla.
|
|
Qt::TextInteractionFlags textFlags = ro? Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard : Qt::TextEditorInteraction;
|
|
ui->qtEdit->setTextInteractionFlags(textFlags);
|
|
}
|
|
|
|
void EditDialog::switchEditorMode(bool autoSwitchForType)
|
|
{
|
|
if (autoSwitchForType) {
|
|
// Switch automatically the editing mode according to the detected data.
|
|
switch (dataType) {
|
|
case Image:
|
|
ui->comboMode->setCurrentIndex(ImageViewer);
|
|
break;
|
|
case Binary:
|
|
ui->comboMode->setCurrentIndex(HexEditor);
|
|
break;
|
|
case Null:
|
|
case Text:
|
|
ui->comboMode->setCurrentIndex(TextEditor);
|
|
break;
|
|
case RtlText:
|
|
ui->comboMode->setCurrentIndex(RtlTextEditor);
|
|
break;
|
|
case JSON:
|
|
ui->comboMode->setCurrentIndex(JsonEditor);
|
|
break;
|
|
case SVG:
|
|
case XML:
|
|
ui->comboMode->setCurrentIndex(XmlEditor);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update the information labels in the bottom left corner of the dialog
|
|
// and switches the editor mode, if required, according to the detected data type.
|
|
void EditDialog::updateCellInfoAndMode(const QByteArray& bArrdata)
|
|
{
|
|
QByteArray cellData = bArrdata;
|
|
|
|
switchEditorMode(ui->buttonAutoSwitchMode->isChecked());
|
|
|
|
// Image data needs special treatment
|
|
if (dataType == Image || dataType == SVG) {
|
|
QBuffer imageBuffer(&cellData);
|
|
QImageReader imageReader(&imageBuffer);
|
|
|
|
// Display the image format
|
|
QString imageFormat = imageReader.format();
|
|
|
|
ui->labelType->setText(tr("Type of data currently in cell: %1 Image").arg(imageFormat.toUpper()));
|
|
|
|
// Display the image dimensions and size
|
|
QSize imageDimensions = imageReader.size();
|
|
unsigned int imageSize = static_cast<unsigned int>(cellData.size());
|
|
|
|
QString labelSizeText = tr("%1x%2 pixel(s)").arg(imageDimensions.width()).arg(imageDimensions.height()) + ", " + humanReadableSize(imageSize);
|
|
|
|
ui->labelSize->setText(labelSizeText);
|
|
|
|
return;
|
|
}
|
|
|
|
// Use a switch statement for the other data types to keep things neat :)
|
|
switch (dataType) {
|
|
case Null: {
|
|
// NULL data type
|
|
ui->labelType->setText(tr("Type of data currently in cell: NULL"));
|
|
ui->labelSize->setText(tr("%n byte(s)", "", 0));
|
|
|
|
// Use margin to set the NULL text.
|
|
sciEdit->setTextInMargin(Settings::getValue("databrowser", "null_text").toString());
|
|
break;
|
|
}
|
|
case XML:
|
|
case Text:
|
|
case RtlText: {
|
|
// Text only
|
|
// Determine the length of the cell text in characters (possibly different to number of bytes).
|
|
int textLength = QString(cellData).length();
|
|
ui->labelType->setText(tr("Type of data currently in cell: Text / Numeric"));
|
|
ui->labelSize->setText(tr("%n character(s)", "", textLength));
|
|
break;
|
|
}
|
|
case JSON: {
|
|
// Valid JSON
|
|
// Determine the length of the cell text in characters (possibly different to number of bytes).
|
|
int jsonLength = QString(cellData).length();
|
|
ui->labelType->setText(tr("Type of data currently in cell: Valid JSON"));
|
|
ui->labelSize->setText(tr("%n character(s)", "", jsonLength));
|
|
break;
|
|
}
|
|
default:
|
|
|
|
// Determine the length of the cell data
|
|
int dataLength = cellData.length();
|
|
// If none of the above data types, consider it general binary data
|
|
ui->labelType->setText(tr("Type of data currently in cell: Binary"));
|
|
ui->labelSize->setText(tr("%n byte(s)", "", dataLength));
|
|
break;
|
|
}
|
|
}
|
|
|
|
void EditDialog::reloadSettings()
|
|
{
|
|
// Set the (SQL) editor font for hex editor, since it needs a
|
|
// Monospace font and the databrowser font would be usually of
|
|
// variable width.
|
|
QFont hexFont(Settings::getValue("editor", "font").toString());
|
|
hexFont.setPointSize(Settings::getValue("databrowser", "fontsize").toInt());
|
|
hexEdit->setFont(hexFont);
|
|
|
|
ui->editCellToolbar->setToolButtonStyle(static_cast<Qt::ToolButtonStyle>
|
|
(Settings::getValue("General", "toolbarStyleEditCell").toInt()));
|
|
|
|
sciEdit->reloadSettings();
|
|
}
|
|
|
|
void EditDialog::setStackCurrentIndex(int editMode)
|
|
{
|
|
switch (editMode) {
|
|
case TextEditor:
|
|
// Scintilla case: switch to the single Scintilla editor and set language
|
|
ui->editorStack->setCurrentIndex(TextEditor);
|
|
sciEdit->setLanguage(DockTextEdit::PlainText);
|
|
break;
|
|
case HexEditor:
|
|
case ImageViewer:
|
|
case RtlTextEditor:
|
|
// General case: switch to the selected editor
|
|
ui->editorStack->setCurrentIndex(editMode);
|
|
break;
|
|
case JsonEditor:
|
|
// Scintilla case: switch to the single Scintilla editor and set language
|
|
ui->editorStack->setCurrentIndex(TextEditor);
|
|
sciEdit->setLanguage(DockTextEdit::JSON);
|
|
break;
|
|
case XmlEditor:
|
|
// Scintilla case: switch to the single Scintilla editor and set language
|
|
ui->editorStack->setCurrentIndex(TextEditor);
|
|
sciEdit->setLanguage(DockTextEdit::XML);
|
|
break;
|
|
}
|
|
}
|
|
|
|
void EditDialog::openPrintDialog()
|
|
{
|
|
int editMode = ui->editorStack->currentIndex();
|
|
if (editMode == ImageViewer) {
|
|
openPrintImageDialog();
|
|
return;
|
|
}
|
|
|
|
QPrinter printer;
|
|
QPrintPreviewDialog *dialog = new QPrintPreviewDialog(&printer);
|
|
|
|
connect(dialog, &QPrintPreviewDialog::paintRequested, [this](QPrinter *previewPrinter) {
|
|
QTextDocument document;
|
|
switch (dataSource) {
|
|
case SciBuffer:
|
|
// This case isn't really expected because the Scintilla widget has it's own printing slot
|
|
document.setPlainText(sciEdit->text());
|
|
break;
|
|
case HexBuffer:
|
|
document.setPlainText(hexEdit->toReadableString());
|
|
document.setDefaultFont(hexEdit->font());
|
|
break;
|
|
case QtBuffer:
|
|
document.setPlainText(ui->qtEdit->toPlainText());
|
|
break;
|
|
}
|
|
|
|
document.print(previewPrinter);
|
|
});
|
|
|
|
dialog->exec();
|
|
delete dialog;
|
|
|
|
}
|
|
|
|
void EditDialog::openPrintImageDialog()
|
|
{
|
|
QPrinter printer;
|
|
QPrintPreviewDialog *dialog = new QPrintPreviewDialog(&printer);
|
|
|
|
connect(dialog, &QPrintPreviewDialog::paintRequested, [&](QPrinter *previewPrinter) {
|
|
QPainter painter(previewPrinter);
|
|
QRect rect = painter.viewport();
|
|
QSize size = ui->editorImage->pixmap()->size();
|
|
size.scale(rect.size(), Qt::KeepAspectRatio);
|
|
painter.setViewport(rect.x(), rect.y(), size.width(), size.height());
|
|
painter.setWindow(ui->editorImage->pixmap()->rect());
|
|
painter.drawPixmap(0, 0, *ui->editorImage->pixmap());
|
|
});
|
|
|
|
dialog->exec();
|
|
|
|
delete dialog;
|
|
}
|
|
|
|
void EditDialog::copyHexAscii()
|
|
{
|
|
QApplication::clipboard()->setText(hexEdit->selectionToReadableString());
|
|
}
|
|
|
|
void EditDialog::setWordWrapping(bool value)
|
|
{
|
|
// Set wrap lines
|
|
sciEdit->setWrapMode(value ? QsciScintilla::WrapWord : QsciScintilla::WrapNone);
|
|
ui->qtEdit->setWordWrapMode(value ? QTextOption::WrapAtWordBoundaryOrAnywhere : QTextOption::NoWrap);
|
|
}
|