mirror of
https://github.com/sqlitebrowser/sqlitebrowser.git
synced 2026-02-09 13:18:33 -06:00
This commit does a lot of refactoring. But most noticeably it changes
two things:
1) Instead of saving all objects (tables, views, indices, triggers) of a
schema in a common map, we now store tables/views, indices and
triggers in three separate maps. This has a number of benefits:
- It resembles more closely how SQLite stores its data internally and
therefore achieves greater compatability e.g. for databases with a
view and a trigger with the same name.
- It reduces the need for runtime polymorphism. This makes the code
run a bit faster.
- By working with explicit types more often more error checking can
be done at compile time, making the code less error prone.
- The code becomes a lot clearer to read.
2) By making View inherit form Table, views are now a sort of tables.
This has the following benefits:
- This is a again how SQLite stores views internally which again
should increase compatibility a bit.
- We mostly treat views and tables the same anyway and with these
changes we can unify the code for them even more.
373 lines
13 KiB
C++
373 lines
13 KiB
C++
#include "AddRecordDialog.h"
|
|
#include "ui_AddRecordDialog.h"
|
|
#include "sqlitedb.h"
|
|
#include "Settings.h"
|
|
|
|
#include <QMessageBox>
|
|
#include <QPushButton>
|
|
#include <QKeyEvent>
|
|
#include <QStyledItemDelegate>
|
|
#include <QWhatsThis>
|
|
#include <QLineEdit>
|
|
#include <QMenu>
|
|
|
|
namespace {
|
|
|
|
class NullLineEdit: public QLineEdit {
|
|
Q_OBJECT
|
|
private:
|
|
bool m_isNull;
|
|
|
|
public:
|
|
explicit NullLineEdit(QWidget* parent=nullptr): QLineEdit(parent), m_isNull (true) {}
|
|
|
|
bool isNull() {return m_isNull;}
|
|
void setNull(bool value) {
|
|
if (value) {
|
|
clear();
|
|
setStyleSheet("QLineEdit{ font-style: italic; }");
|
|
setPlaceholderText(Settings::getValue("databrowser", "null_text").toString());
|
|
setModified(false);
|
|
} else {
|
|
setStyleSheet("");
|
|
setPlaceholderText("");
|
|
}
|
|
m_isNull = value;
|
|
}
|
|
protected:
|
|
void contextMenuEvent(QContextMenuEvent *event) override
|
|
{
|
|
QMenu* editContextMenu = createStandardContextMenu();
|
|
|
|
QAction* nullAction = new QAction(QIcon(":/icons/set_to_null"), tr("Set to NULL"), editContextMenu);
|
|
connect(nullAction, &QAction::triggered, nullAction, [&]() {
|
|
setNull(true);
|
|
});
|
|
nullAction->setShortcut(QKeySequence(tr("Alt+Del")));
|
|
editContextMenu->addSeparator();
|
|
editContextMenu->addAction(nullAction);
|
|
|
|
editContextMenu->exec(event->globalPos());
|
|
delete editContextMenu;
|
|
}
|
|
|
|
void keyPressEvent(QKeyEvent *evt) override
|
|
{
|
|
// Alt+Del sets field to NULL
|
|
if((evt->modifiers() & Qt::AltModifier) && (evt->key() == Qt::Key_Delete))
|
|
setNull(true);
|
|
else {
|
|
// Remove any possible NULL mark when user starts typing
|
|
setStyleSheet("");
|
|
setPlaceholderText("");
|
|
QLineEdit::keyPressEvent(evt);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Styled Item Delegate for non-editable columns (all except Value)
|
|
class NoEditDelegate: public QStyledItemDelegate {
|
|
public:
|
|
explicit NoEditDelegate(QObject* parent=nullptr): QStyledItemDelegate(parent) {}
|
|
QWidget* createEditor(QWidget* /* parent */, const QStyleOptionViewItem& /* option */, const QModelIndex& /* index */) const override {
|
|
return nullptr;
|
|
}
|
|
};
|
|
|
|
// Styled Item Delegate for editable columns (Value)
|
|
class EditDelegate: public QStyledItemDelegate {
|
|
|
|
public:
|
|
explicit EditDelegate(QObject* parent=nullptr): QStyledItemDelegate(parent) {}
|
|
QWidget* createEditor(QWidget *parent, const QStyleOptionViewItem& /* option */, const QModelIndex& /* index */) const override {
|
|
return new NullLineEdit(parent);
|
|
}
|
|
|
|
void setEditorData(QWidget *editor, const QModelIndex &index) const override {
|
|
|
|
NullLineEdit* lineEditor = static_cast<NullLineEdit*>(editor);
|
|
// Set the editor in the null state (unless the user has actually written NULL)
|
|
if (index.model()->data(index, Qt::UserRole).isNull() &&
|
|
index.model()->data(index, Qt::DisplayRole) == Settings::getValue("databrowser", "null_text"))
|
|
lineEditor->setNull(true);
|
|
else {
|
|
QStyledItemDelegate::setEditorData(editor, index);
|
|
lineEditor->setNull(false);
|
|
}
|
|
}
|
|
void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override {
|
|
|
|
NullLineEdit* lineEditor = static_cast<NullLineEdit*>(editor);
|
|
// Restore NULL text (unless the user has already modified the value)
|
|
if (lineEditor->isNull() && !lineEditor->isModified()) {
|
|
model->setData(index, Settings::getValue("databrowser", "null_text"), Qt::DisplayRole);
|
|
model->setData(index, QVariant(), Qt::UserRole);
|
|
} else {
|
|
// Get isModified flag before calling setModelData
|
|
bool modified = lineEditor->isModified();
|
|
QStyledItemDelegate::setModelData(editor, model, index);
|
|
// Copy the just edited data to the user role, so it can be later used in the SQL insert statement.
|
|
if (modified) {
|
|
lineEditor->setNull(false);
|
|
model->setData(index, model->data(index, Qt::EditRole), Qt::UserRole);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
}
|
|
|
|
|
|
AddRecordDialog::AddRecordDialog(DBBrowserDB& db, const sqlb::ObjectIdentifier& tableName, QWidget* parent, const std::vector<std::string>& _pseudo_pk)
|
|
: QDialog(parent),
|
|
ui(new Ui::AddRecordDialog),
|
|
pdb(db),
|
|
curTable(tableName),
|
|
pseudo_pk(_pseudo_pk)
|
|
{
|
|
// Create UI
|
|
ui->setupUi(this);
|
|
connect(ui->treeWidget, &QTreeWidget::itemChanged, this, &AddRecordDialog::itemChanged);
|
|
|
|
populateFields();
|
|
|
|
ui->sqlTextEdit->setReadOnly(true);
|
|
|
|
// Update UI
|
|
ui->treeWidget->resizeColumnToContents(kName);
|
|
ui->treeWidget->resizeColumnToContents(kType);
|
|
ui->treeWidget->setFrameShape(QFrame::Box);
|
|
}
|
|
|
|
AddRecordDialog::~AddRecordDialog()
|
|
{
|
|
delete ui;
|
|
}
|
|
|
|
void AddRecordDialog::keyPressEvent(QKeyEvent *evt)
|
|
{
|
|
if((evt->modifiers() & Qt::ControlModifier)
|
|
&& (evt->key() == Qt::Key_Enter || evt->key() == Qt::Key_Return))
|
|
{
|
|
accept();
|
|
return;
|
|
}
|
|
if(evt->key() == Qt::Key_Enter || evt->key() == Qt::Key_Return)
|
|
return;
|
|
QDialog::keyPressEvent(evt);
|
|
}
|
|
|
|
void AddRecordDialog::setDefaultsStyle(QTreeWidgetItem* item)
|
|
{
|
|
// Default values are displayed with the style configured for NULL values in the Data Browser.
|
|
QFont font;
|
|
font.setItalic(true);
|
|
item->setData(kValue, Qt::FontRole, font);
|
|
item->setData(kValue, Qt::BackgroundRole, QColor(Settings::getValue("databrowser", "null_bg_colour").toString()));
|
|
item->setData(kValue, Qt::ForegroundRole, QColor(Settings::getValue("databrowser", "null_fg_colour").toString()));
|
|
}
|
|
|
|
void AddRecordDialog::populateFields()
|
|
{
|
|
// Block the itemChanged signal or the SQL text will be updated while filling the treewidget.
|
|
ui->treeWidget->blockSignals(true);
|
|
|
|
ui->treeWidget->clear();
|
|
|
|
// Allow all Edit Triggers, but they will only apply to the columns with
|
|
// editors (Value)
|
|
ui->treeWidget->setEditTriggers(QAbstractItemView::AllEditTriggers);
|
|
|
|
// Disallow edition of columns except Value
|
|
ui->treeWidget->setItemDelegateForColumn(kName, new NoEditDelegate(this));
|
|
ui->treeWidget->setItemDelegateForColumn(kType, new NoEditDelegate(this));
|
|
ui->treeWidget->setItemDelegateForColumn(kValue, new EditDelegate(this));
|
|
|
|
sqlb::FieldVector fields;
|
|
std::vector<sqlb::ConstraintPtr> fks;
|
|
sqlb::StringVector pk;
|
|
bool auto_increment = false;
|
|
|
|
// Initialize fields, fks and pk differently depending on whether it's a table or a view.
|
|
const sqlb::TablePtr table = pdb.getTableByName(curTable);
|
|
fields = table->fields;
|
|
if (!table->isView())
|
|
{
|
|
std::transform(fields.begin(), fields.end(), std::back_inserter(fks), [table](const auto& f) {
|
|
return table->constraint({f.name()}, sqlb::Constraint::ForeignKeyConstraintType);
|
|
});
|
|
|
|
const auto pk_constraint = table->primaryKey();
|
|
if(pk_constraint)
|
|
{
|
|
pk = pk_constraint->columnList();
|
|
auto_increment = pk_constraint->autoIncrement();
|
|
}
|
|
} else {
|
|
// It's a view
|
|
fks.resize(fields.size(), sqlb::ConstraintPtr(nullptr));
|
|
pk = pseudo_pk;
|
|
}
|
|
|
|
for(uint i = 0; i < fields.size(); i++)
|
|
{
|
|
const sqlb::Field& f = fields[i];
|
|
QTreeWidgetItem *tbitem = new QTreeWidgetItem(ui->treeWidget);
|
|
|
|
tbitem->setFlags(Qt::ItemIsEnabled | Qt::ItemIsEditable);
|
|
|
|
tbitem->setText(kName, QString::fromStdString(f.name()));
|
|
tbitem->setText(kType, QString::fromStdString(f.type()));
|
|
tbitem->setData(kType, Qt::UserRole, f.affinity());
|
|
|
|
// NOT NULL fields are indicated in bold.
|
|
if (f.notnull()) {
|
|
QFont font;
|
|
font.setBold(true);
|
|
tbitem->setData(kName, Qt::FontRole, font);
|
|
}
|
|
if (contains(pk, f.name()))
|
|
tbitem->setIcon(kName, QIcon(":/icons/field_key"));
|
|
else if (fks[i])
|
|
tbitem->setIcon(kName, QIcon(":/icons/field_fk"));
|
|
else
|
|
tbitem->setIcon(kName, QIcon(":/icons/field"));
|
|
|
|
QString toolTip;
|
|
|
|
if (auto_increment && contains(pk, f.name()))
|
|
toolTip.append(tr("Auto-increment\n"));
|
|
|
|
if (f.unique())
|
|
toolTip.append(tr("Unique constraint\n"));
|
|
|
|
if (!f.check().empty())
|
|
toolTip.append(tr("Check constraint:\t %1\n").arg(QString::fromStdString(f.check())));
|
|
|
|
auto fk = std::dynamic_pointer_cast<sqlb::ForeignKeyClause>(fks[i]);
|
|
if(fk)
|
|
toolTip.append(tr("Foreign key:\t %1\n").arg(QString::fromStdString(fk->toString())));
|
|
|
|
setDefaultsStyle(tbitem);
|
|
|
|
// Display Role is used for displaying the default values.
|
|
// Only when they are changed, the User Role is updated and then used in the INSERT query.
|
|
if (!f.defaultValue().empty()) {
|
|
QString defaultValue = QString::fromStdString(f.defaultValue());
|
|
tbitem->setData(kValue, Qt::DisplayRole, defaultValue);
|
|
toolTip.append(tr("Default value:\t %1\n").arg(defaultValue));
|
|
} else
|
|
tbitem->setData(kValue, Qt::DisplayRole, Settings::getValue("databrowser", "null_text"));
|
|
|
|
|
|
if (!toolTip.isEmpty()) {
|
|
// Chop last end-of-line
|
|
toolTip.chop(1);
|
|
tbitem->setToolTip(kValue, toolTip);
|
|
tbitem->setToolTip(kType, toolTip);
|
|
}
|
|
}
|
|
|
|
updateSqlText();
|
|
|
|
// Enable signals from tree widget
|
|
ui->treeWidget->blockSignals(false);
|
|
}
|
|
|
|
void AddRecordDialog::accept()
|
|
{
|
|
if(!pdb.executeSQL(ui->sqlTextEdit->text().toStdString()))
|
|
{
|
|
QMessageBox::warning(
|
|
this,
|
|
QApplication::applicationName(),
|
|
tr("Error adding record. Message from database engine:\n\n%1").arg(pdb.lastError()));
|
|
return;
|
|
}
|
|
|
|
QDialog::accept();
|
|
}
|
|
|
|
void AddRecordDialog::updateSqlText()
|
|
{
|
|
QString stmt = QString("INSERT INTO %1").arg(QString::fromStdString(curTable.toString()));
|
|
|
|
QStringList vals;
|
|
QStringList fields;
|
|
|
|
// If the User Role of the Value column is not null, the entered value is used
|
|
// in the INSERT statement. Otherwise, SQLite just uses the default value for the field.
|
|
for(int i = 0; i < ui->treeWidget->topLevelItemCount(); ++i)
|
|
{
|
|
QTreeWidgetItem *item = ui->treeWidget->topLevelItem(i);
|
|
// User role contains now values entered by the user, that we actually need to insert.
|
|
QVariant value = item->data(kValue, Qt::UserRole);
|
|
|
|
if (!value.isNull()) {
|
|
bool isNumeric;
|
|
fields << sqlb::escapeIdentifier(item->text(kName));
|
|
value.toDouble(&isNumeric);
|
|
// If it has a numeric format and has no text affinity, do not quote it.
|
|
if (isNumeric && item->data(kType, Qt::UserRole).toInt() != sqlb::Field::TextAffinity)
|
|
vals << value.toString();
|
|
else
|
|
vals << sqlb::escapeString(value.toString());
|
|
}
|
|
}
|
|
|
|
if(fields.empty())
|
|
{
|
|
stmt.append(" DEFAULT VALUES;");
|
|
} else {
|
|
stmt.append("\n(");
|
|
stmt.append(fields.join(", "));
|
|
stmt.append(")\nVALUES (");
|
|
stmt.append(vals.join(", "));
|
|
stmt.append(");");
|
|
}
|
|
|
|
ui->sqlTextEdit->setText(stmt);
|
|
}
|
|
|
|
void AddRecordDialog::itemChanged(QTreeWidgetItem *item, int column)
|
|
{
|
|
if (item->data(column, Qt::UserRole).isNull())
|
|
setDefaultsStyle(item);
|
|
else {
|
|
// Restore default fore/background for the value column,
|
|
// since the value has changed away from the default.
|
|
QFont font;
|
|
font.setItalic(false);
|
|
item->setData(column, Qt::FontRole, font);
|
|
item->setData(column, Qt::BackgroundRole, item->data(kName, Qt::BackgroundRole));
|
|
item->setData(column, Qt::ForegroundRole, item->data(kName, Qt::ForegroundRole));
|
|
}
|
|
|
|
updateSqlText();
|
|
}
|
|
|
|
void AddRecordDialog::help()
|
|
{
|
|
QWhatsThis::enterWhatsThisMode();
|
|
}
|
|
|
|
void AddRecordDialog::on_buttonBox_clicked(QAbstractButton* button)
|
|
{
|
|
if (button == ui->buttonBox->button(QDialogButtonBox::Cancel))
|
|
reject();
|
|
else if (button == ui->buttonBox->button(QDialogButtonBox::Save))
|
|
accept();
|
|
else if (button == ui->buttonBox->button(QDialogButtonBox::Help))
|
|
help();
|
|
else if (button == ui->buttonBox->button(QDialogButtonBox::RestoreDefaults)) {
|
|
if (QMessageBox::warning(this,
|
|
QApplication::applicationName(),
|
|
tr("Are you sure you want to restore all the entered values to their defaults?"),
|
|
QMessageBox::RestoreDefaults | QMessageBox::Cancel,
|
|
QMessageBox::Cancel) == QMessageBox::RestoreDefaults)
|
|
populateFields();
|
|
}
|
|
}
|
|
|
|
#include "AddRecordDialog.moc"
|