#include "AddRecordDialog.h" #include "ui_AddRecordDialog.h" #include "sqlitedb.h" #include "Settings.h" #include #include #include #include #include #include #include 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(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(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& _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 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(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"