1 #include "MainWindow.h"
2 #include "ui_MainWindow.h"
3 
4 #include "Application.h"
5 #include "EditIndexDialog.h"
6 #include "AboutDialog.h"
7 #include "EditTableDialog.h"
8 #include "ImportCsvDialog.h"
9 #include "ExportDataDialog.h"
10 #include "Settings.h"
11 #include "PreferencesDialog.h"
12 #include "EditDialog.h"
13 #include "sqlitetablemodel.h"
14 #include "SqlExecutionArea.h"
15 #include "VacuumDialog.h"
16 #include "DbStructureModel.h"
17 #include "version.h"
18 #include "sqlite.h"
19 #include "CipherDialog.h"
20 #include "ExportSqlDialog.h"
21 #include "SqlUiLexer.h"
22 #include "FileDialog.h"
23 #include "FilterTableHeader.h"
24 #include "RemoteDock.h"
25 #include "FindReplaceDialog.h"
26 #include "RunSql.h"
27 #include "ExtendedTableWidget.h"
28 #include "Data.h"
29 
30 #include <chrono>
31 #include <QFile>
32 #include <QTextStream>
33 #include <QWhatsThis>
34 #include <QMessageBox>
35 #include <QPersistentModelIndex>
36 #include <QDragEnterEvent>
37 #include <QScrollBar>
38 #include <QMimeData>
39 #include <QDesktopServices>
40 #include <QXmlStreamReader>
41 #include <QXmlStreamWriter>
42 #include <QInputDialog>
43 #include <QProgressDialog>
44 #include <QTextEdit>
45 #include <QClipboard>
46 #include <QShortcut>
47 #include <QUrlQuery>
48 #include <QDataStream>      // This include seems to only be necessary for the Windows build
49 #include <QPrinter>
50 #include <QPrintPreviewDialog>
51 #include <QToolButton>
52 
53 #ifdef Q_OS_MACX //Needed only on macOS
54     #include <QOpenGLWidget>
55 #endif
56 
57 #include <limits>
58 
59 const int MainWindow::MaxRecentFiles;
60 
61 // These are needed for reading and writing object files
operator >>(QDataStream & ds,sqlb::ObjectIdentifier & objid)62 QDataStream& operator>>(QDataStream& ds, sqlb::ObjectIdentifier& objid)
63 {
64     // Read in the item
65     QVariant v;
66     ds >> v;
67 
68     // If it is a string list, we can treat it as an object identifier. If it isn't, we assume it's just a
69     // single string and use interpret it as the table name in the main schema. This is done for backwards
70     // compatability with old project file formats.
71     QStringList str = v.toStringList();
72     if(str.isEmpty())
73     {
74         objid = sqlb::ObjectIdentifier("main", v.toString().toStdString());
75     } else {
76         objid.setSchema(str.first().toStdString());
77         if(str.size() >= 2)
78             objid.setName(str.last().toStdString());
79     }
80     return ds;
81 }
82 
83 // This is a temporary helper function. Delete it once we clean up the project file loading.
toSortOrderVector(int index,Qt::SortOrder mode)84 static std::vector<sqlb::SortedColumn> toSortOrderVector(int index, Qt::SortOrder mode)
85 {
86     std::vector<sqlb::SortedColumn> vector;
87     vector.emplace_back(index, mode == Qt::AscendingOrder ? sqlb::Ascending : sqlb::Descending);
88     return vector;
89 }
90 
MainWindow(QWidget * parent)91 MainWindow::MainWindow(QWidget* parent)
92     : QMainWindow(parent),
93       ui(new Ui::MainWindow),
94       db(),
95       editDock(new EditDialog(this)),
96       plotDock(new PlotDock(this)),
97       remoteDock(new RemoteDock(this)),
98       findReplaceDialog(new FindReplaceDialog(this)),
99       execute_sql_worker(nullptr),
100       isProjectModified(false)
101 {
102     ui->setupUi(this);
103     init();
104 
105     activateFields(false);
106     updateRecentFileActions();
107 }
108 
~MainWindow()109 MainWindow::~MainWindow()
110 {
111     delete ui;
112 }
113 
init()114 void MainWindow::init()
115 {
116     // Load window settings
117     tabifyDockWidget(ui->dockLog, ui->dockPlot);
118     tabifyDockWidget(ui->dockLog, ui->dockSchema);
119     tabifyDockWidget(ui->dockLog, ui->dockRemote);
120 
121 #ifdef Q_OS_MACX
122     // Add OpenGL Context for macOS
123     QOpenGLWidget *ogl = new QOpenGLWidget(this);
124     ui->verticalLayout->addWidget(ogl);
125     ogl->setHidden(true);
126 #endif
127 
128     // Automatic update check
129 #ifdef CHECKNEWVERSION
130     connect(&RemoteNetwork::get(), &RemoteNetwork::networkReady, [this]() {
131         // Check for a new version if automatic update check aren't disabled in the settings dialog
132         if(Settings::getValue("checkversion", "enabled").toBool())
133         {
134             RemoteNetwork::get().fetch(QUrl("https://download.sqlitebrowser.org/currentrelease"), RemoteNetwork::RequestTypeCustom,
135                                        QString(), [this](const QByteArray& reply) {
136                 QList<QByteArray> info = reply.split('\n');
137                 if(info.size() >= 2)
138                 {
139                     QString version = info.at(0).trimmed();
140                     QString url = info.at(1).trimmed();
141                     checkNewVersion(version, url);
142                 }
143             });
144         }
145     });
146 #endif
147 
148     // Connect SQL logging and database state setting to main window
149     connect(&db, &DBBrowserDB::dbChanged, this, &MainWindow::dbState, Qt::QueuedConnection);
150     connect(&db, &DBBrowserDB::sqlExecuted, this, &MainWindow::logSql, Qt::QueuedConnection);
151     connect(&db, &DBBrowserDB::requestCollation, this, &MainWindow::requestCollation);
152 
153     // Initialise table browser first
154     ui->tableBrowser->init(&db);
155 
156     // Set project modified flag when the settings in the table browser were changed
157     connect(ui->tableBrowser, &TableBrowser::projectModified, this, [this]() {
158        isProjectModified = true;
159     });
160 
161     connect(ui->tableBrowser->model(), &SqliteTableModel::dataChanged, this, &MainWindow::dataTableSelectionChanged);
162     connect(ui->tableBrowser, &TableBrowser::selectionChanged, this, &MainWindow::dataTableSelectionChanged);
163     connect(ui->tableBrowser, &TableBrowser::selectionChangedByDoubleClick, this, &MainWindow::doubleClickTable);
164     connect(ui->tableBrowser, &TableBrowser::updatePlot, this, &MainWindow::attachPlot);
165     connect(ui->tableBrowser, &TableBrowser::createView, this, &MainWindow::saveAsView);
166     connect(ui->tableBrowser, &TableBrowser::requestFileOpen, this, [this](const QString& file) {
167         fileOpen(file);
168     });
169     connect(ui->tableBrowser, &TableBrowser::statusMessageRequested, ui->statusbar, [this](const QString& message) {
170         ui->statusbar->showMessage(message);
171     });
172 
173     m_currentTabTableModel = ui->tableBrowser->model();
174 
175     // Set up DB structure tab
176     dbStructureModel = new DbStructureModel(db, this);
177     connect(&db, &DBBrowserDB::structureUpdated, this, [this]() {
178         sqlb::ObjectIdentifier old_table = ui->tableBrowser->currentlyBrowsedTableName();
179         dbStructureModel->reloadData();
180         populateStructure(old_table);
181     });
182     ui->dbTreeWidget->setModel(dbStructureModel);
183     ui->dbTreeWidget->setColumnWidth(DbStructureModel::ColumnName, 300);
184     ui->dbTreeWidget->setColumnHidden(DbStructureModel::ColumnObjectType, true);
185     ui->dbTreeWidget->setColumnHidden(DbStructureModel::ColumnSchema, true);
186 
187     // Set up DB schema dock
188     ui->treeSchemaDock->setModel(dbStructureModel);
189     ui->treeSchemaDock->setColumnHidden(DbStructureModel::ColumnObjectType, true);
190     ui->treeSchemaDock->setColumnHidden(DbStructureModel::ColumnSchema, true);
191 
192     // Set up the table combo box in the Browse Data tab
193     ui->tableBrowser->setStructure(dbStructureModel);
194 
195     // Create docks
196     ui->dockEdit->setWidget(editDock);
197     ui->dockPlot->setWidget(plotDock);
198     ui->dockRemote->setWidget(remoteDock);
199 
200     // Set up edit dock
201     editDock->setReadOnly(true);
202 
203     // Restore window geometry
204     restoreGeometry(Settings::getValue("MainWindow", "geometry").toByteArray());
205 
206     // Save default and restore window state
207     defaultWindowState = saveState();
208     restoreState(Settings::getValue("MainWindow", "windowState").toByteArray());
209 
210     // Save default and restore open tab order if the openTabs setting is saved.
211     defaultOpenTabs = saveOpenTabs();
212     restoreOpenTabs(Settings::getValue("MainWindow", "openTabs").toString());
213 
214     // Restore dock state settings
215     ui->comboLogSubmittedBy->setCurrentIndex(ui->comboLogSubmittedBy->findText(Settings::getValue("SQLLogDock", "Log").toString()));
216 
217     // Add keyboard shortcuts
218     QShortcut* shortcutBrowseRefreshF5 = new QShortcut(QKeySequence("F5"), this);
219     connect(shortcutBrowseRefreshF5, &QShortcut::activated, this, &MainWindow::refresh);
220     QShortcut* shortcutBrowseRefreshCtrlR = new QShortcut(QKeySequence("Ctrl+R"), this);
221     connect(shortcutBrowseRefreshCtrlR, &QShortcut::activated, this, &MainWindow::refresh);
222 
223     // Add print shortcut for the DB Structure tab (dbTreeWidget) with context to the widget, so other print shortcuts aren't eclipsed.
224     QShortcut* shortcutPrint = new QShortcut(QKeySequence(QKeySequence::Print), ui->dbTreeWidget, nullptr, nullptr, Qt::WidgetShortcut);
225     connect(shortcutPrint, &QShortcut::activated, this, &MainWindow::printDbStructure);
226 
227     QShortcut* closeTabShortcut = new QShortcut(tr("Ctrl+W"), ui->tabSqlAreas, nullptr, nullptr, Qt::WidgetWithChildrenShortcut);
228     connect(closeTabShortcut, &QShortcut::activated, this, [this]() {
229         if(ui->tabSqlAreas->currentIndex() >= 0)
230           closeSqlTab(ui->tabSqlAreas->currentIndex());
231     });
232 
233     // Create the actions for the recently opened dbs list
234     for(int i = 0; i < MaxRecentFiles; ++i) {
235         recentFileActs[i] = new QAction(this);
236         recentFileActs[i]->setVisible(false);
237         connect(recentFileActs[i], &QAction::triggered, this, &MainWindow::openRecentFile);
238     }
239     for(int i = 0; i < MaxRecentFiles; ++i)
240         ui->fileMenu->insertAction(ui->fileExitAction, recentFileActs[i]);
241     recentSeparatorAct = ui->fileMenu->insertSeparator(ui->fileExitAction);
242 
243     // Create popup menus
244     popupTableMenu = new QMenu(this);
245     popupTableMenu->addAction(ui->actionEditBrowseTable);
246     popupTableMenu->addAction(ui->editModifyObjectAction);
247     popupTableMenu->addAction(ui->editDeleteObjectAction);
248     popupTableMenu->addSeparator();
249     popupTableMenu->addAction(ui->actionEditCopyCreateStatement);
250     popupTableMenu->addAction(ui->actionExportCsvPopup);
251 
252     popupSchemaDockMenu = new QMenu(this);
253     popupSchemaDockMenu->addAction(ui->actionPopupSchemaDockBrowseTable);
254     popupSchemaDockMenu->addSeparator();
255     popupSchemaDockMenu->addAction(ui->actionDropQualifiedCheck);
256     popupSchemaDockMenu->addAction(ui->actionEnquoteNamesCheck);
257 
258     popupOpenDbMenu = new QMenu(this);
259     popupOpenDbMenu->addAction(ui->fileOpenAction);
260     popupOpenDbMenu->addAction(ui->fileOpenReadOnlyAction);
261     ui->fileOpenActionPopup->setMenu(popupOpenDbMenu);
262 
263     popupSaveSqlFileMenu = new QMenu(this);
264     popupSaveSqlFileMenu->addAction(ui->actionSqlSaveFile);
265     popupSaveSqlFileMenu->addAction(ui->actionSqlSaveFileAs);
266     ui->actionSqlSaveFilePopup->setMenu(popupSaveSqlFileMenu);
267 
268     popupSaveSqlResultsMenu = new QMenu(this);
269     popupSaveSqlResultsMenu->addAction(ui->actionSqlResultsExportCsv);
270     popupSaveSqlResultsMenu->addAction(ui->actionSqlResultsSaveAsView);
271     ui->actionSqlResultsSave->setMenu(popupSaveSqlResultsMenu);
272     qobject_cast<QToolButton*>(ui->toolbarSql->widgetForAction(ui->actionSqlResultsSave))->setPopupMode(QToolButton::InstantPopup);
273 
274     // Add menu item for log dock
275     ui->viewMenu->insertAction(ui->viewDBToolbarAction, ui->dockLog->toggleViewAction());
276     ui->viewMenu->actions().at(0)->setShortcut(QKeySequence(tr("Ctrl+L")));
277     ui->viewMenu->actions().at(0)->setIcon(QIcon(":/icons/log_dock"));
278 
279     // Add menu item for plot dock
280     ui->viewMenu->insertAction(ui->viewDBToolbarAction, ui->dockPlot->toggleViewAction());
281     ui->viewMenu->actions().at(1)->setShortcut(QKeySequence(tr("Ctrl+D")));
282     ui->viewMenu->actions().at(1)->setIcon(QIcon(":/icons/log_dock"));
283 
284     // Add menu item for schema dock
285     ui->viewMenu->insertAction(ui->viewDBToolbarAction, ui->dockSchema->toggleViewAction());
286     ui->viewMenu->actions().at(2)->setShortcut(QKeySequence(tr("Ctrl+I")));
287     ui->viewMenu->actions().at(2)->setIcon(QIcon(":/icons/log_dock"));
288 
289     // Add menu item for edit dock
290     ui->viewMenu->insertAction(ui->viewDBToolbarAction, ui->dockEdit->toggleViewAction());
291     ui->viewMenu->actions().at(3)->setShortcut(QKeySequence(tr("Ctrl+E")));
292     ui->viewMenu->actions().at(3)->setIcon(QIcon(":/icons/log_dock"));
293 
294     // Add menu item for plot dock
295     ui->viewMenu->insertAction(ui->viewDBToolbarAction, ui->dockRemote->toggleViewAction());
296     ui->viewMenu->actions().at(4)->setIcon(QIcon(":/icons/log_dock"));
297 
298     // Set checked state if toolbar is visible
299     ui->viewDBToolbarAction->setChecked(!ui->toolbarDB->isHidden());
300     ui->viewExtraDBToolbarAction->setChecked(!ui->toolbarExtraDB->isHidden());
301     ui->viewProjectToolbarAction->setChecked(!ui->toolbarProject->isHidden());
302 
303     // Add separator between docks and toolbars
304     ui->viewMenu->insertSeparator(ui->viewDBToolbarAction);
305 
306     // Connect the tabCloseRequested to the actual closeTab function.
307     // This must be done before the connections for checking the actions in the View menu so
308     // they are updated accordingly.
309     connect(ui->mainTab, &QTabWidget::tabCloseRequested, this, &MainWindow::closeTab);
310 
311     // Add entries for toggling the visibility of main tabs
312     for (QWidget* widget : {ui->structure, ui->browser, ui->pragmas, ui->query}) {
313         QAction* action = ui->viewMenu->addAction(QIcon(":/icons/open_sql"), widget->accessibleName());
314         action->setObjectName(widget->accessibleName());
315         action->setCheckable(true);
316         action->setChecked(ui->mainTab->indexOf(widget) != -1);
317         connect(action, &QAction::toggled, [=](bool show) { toggleTabVisible(widget, show); });
318         // Connect tabCloseRequested for setting checked the appropiate menu entry.
319         // Note these are called after the actual tab is closed only because they are connected
320         // after connecting closeTab.
321         connect(ui->mainTab, &QTabWidget::tabCloseRequested, [=](int /*index*/) {
322                 action->setChecked(ui->mainTab->indexOf(widget) != -1);
323             });
324     }
325 
326     ui->viewMenu->addSeparator();
327 
328     QMenu* layoutMenu = new QMenu(tr("Window Layout"), this);
329     ui->viewMenu->addMenu(layoutMenu);
330 
331     QAction* resetLayoutAction = layoutMenu->addAction(tr("Reset Window Layout"));
332     resetLayoutAction->setShortcut(QKeySequence(tr("Alt+0")));
333     connect(resetLayoutAction, &QAction::triggered, [=]() {
334             restoreState(defaultWindowState);
335             restoreOpenTabs(defaultOpenTabs);
336             ui->viewDBToolbarAction->setChecked(!ui->toolbarDB->isHidden());
337             ui->viewExtraDBToolbarAction->setChecked(!ui->toolbarExtraDB->isHidden());
338             ui->viewProjectToolbarAction->setChecked(!ui->toolbarProject->isHidden());
339         });
340     QAction* simplifyLayoutAction = layoutMenu->addAction(tr("Simplify Window Layout"));
341     simplifyLayoutAction->setShortcut(QKeySequence(tr("Shift+Alt+0")));
342     connect(simplifyLayoutAction, &QAction::triggered, [=]() {
343             ui->viewMenu->findChild<QAction *>(ui->pragmas->accessibleName())->activate(QAction::Trigger);
344             ui->dockLog->hide();
345             ui->dockPlot->hide();
346             ui->dockSchema->hide();
347             ui->dockEdit->hide();
348             ui->dockRemote->hide();
349         });
350     QAction* atBottomLayoutAction = layoutMenu->addAction(tr("Dock Windows at Bottom"));
351     connect(atBottomLayoutAction, &QAction::triggered, [=]() {
352             moveDocksTo(Qt::BottomDockWidgetArea);
353         });
354     QAction* atLeftLayoutAction = layoutMenu->addAction(tr("Dock Windows at Left Side"));
355     connect(atLeftLayoutAction, &QAction::triggered, [=]() {
356             moveDocksTo(Qt::LeftDockWidgetArea);
357         });
358     QAction* atTopLayoutAction = layoutMenu->addAction(tr("Dock Windows at Top"));
359     connect(atTopLayoutAction, &QAction::triggered, [=]() {
360             moveDocksTo(Qt::TopDockWidgetArea);
361         });
362 
363     // Set Alt+[1-4] shortcuts for opening the corresponding tab in that position.
364     // Note that it is safe to call setCurrentIndex with a tab that is currently closed,
365     // since setCurrentIndex does nothing in that case.
366     QShortcut* setTab1Shortcut = new QShortcut(QKeySequence("Alt+1"), this);
367     connect(setTab1Shortcut, &QShortcut::activated, [this]() { ui->mainTab->setCurrentIndex(0); });
368     QShortcut* setTab2Shortcut = new QShortcut(QKeySequence("Alt+2"), this);
369     connect(setTab2Shortcut, &QShortcut::activated, [this]() { ui->mainTab->setCurrentIndex(1); });
370     QShortcut* setTab3Shortcut = new QShortcut(QKeySequence("Alt+3"), this);
371     connect(setTab3Shortcut, &QShortcut::activated, [this]() { ui->mainTab->setCurrentIndex(2); });
372     QShortcut* setTab4Shortcut = new QShortcut(QKeySequence("Alt+4"), this);
373     connect(setTab4Shortcut, &QShortcut::activated, [this]() { ui->mainTab->setCurrentIndex(3); });
374 
375     // If we're not compiling in SQLCipher, hide its FAQ link in the help menu
376 #ifndef ENABLE_SQLCIPHER
377     ui->actionSqlCipherFaq->setVisible(false);
378 #endif
379 
380     // Set statusbar fields
381     statusBusyLabel = new QLabel(ui->statusbar);
382     statusBusyLabel->setEnabled(false);
383     statusBusyLabel->setVisible(false);
384     statusBusyLabel->setToolTip(tr("The database is currenctly busy."));
385     ui->statusbar->addPermanentWidget(statusBusyLabel);
386 
387     statusStopButton = new QToolButton(ui->statusbar);
388     statusStopButton->setVisible(false);
389     statusStopButton->setIcon(QIcon(":icons/cancel"));
390     statusStopButton->setToolTip(tr("Click here to interrupt the currently running query."));
391     statusStopButton->setMaximumSize(ui->statusbar->geometry().height() - 6, ui->statusbar->geometry().height() - 6);
392     statusStopButton->setAutoRaise(true);
393     ui->statusbar->addPermanentWidget(statusStopButton);
394 
395     statusEncryptionLabel = new QLabel(ui->statusbar);
396     statusEncryptionLabel->setEnabled(false);
397     statusEncryptionLabel->setVisible(false);
398     statusEncryptionLabel->setText(tr("Encrypted"));
399     statusEncryptionLabel->setToolTip(tr("Database is encrypted using SQLCipher"));
400     ui->statusbar->addPermanentWidget(statusEncryptionLabel);
401 
402     statusReadOnlyLabel = new QLabel(ui->statusbar);
403     statusReadOnlyLabel->setEnabled(false);
404     statusReadOnlyLabel->setVisible(false);
405     statusReadOnlyLabel->setText(tr("Read only"));
406     statusReadOnlyLabel->setToolTip(tr("Database file is read only. Editing the database is disabled."));
407     ui->statusbar->addPermanentWidget(statusReadOnlyLabel);
408 
409     statusEncodingLabel = new QLabel(ui->statusbar);
410     statusEncodingLabel->setEnabled(false);
411     statusEncodingLabel->setText("UTF-8");
412     statusEncodingLabel->setToolTip(tr("Database encoding"));
413     ui->statusbar->addPermanentWidget(statusEncodingLabel);
414 
415     // When changing the text of the toolbar actions, also automatically change their icon text and their tooltip text
416     connect(ui->editModifyObjectAction, &QAction::changed, [=]() {
417         ui->editModifyObjectAction->setIconText(ui->editModifyObjectAction->text());
418         ui->editModifyObjectAction->setToolTip(ui->editModifyObjectAction->text());
419     });
420     connect(ui->editDeleteObjectAction, &QAction::changed, [=]() {
421         ui->editDeleteObjectAction->setIconText(ui->editDeleteObjectAction->text());
422         ui->editDeleteObjectAction->setToolTip(ui->editDeleteObjectAction->text());
423     });
424 
425     // When clicking the interrupt query button in the status bar, ask SQLite to interrupt the current query
426     connect(statusStopButton, &QToolButton::clicked, [this]() {
427        db.interruptQuery();
428     });
429 
430     // Connect some more signals and slots
431     connect(editDock, &EditDialog::recordTextUpdated, this, &MainWindow::updateRecordText);
432     connect(editDock, &EditDialog::requestUrlOrFileOpen, this, &MainWindow::openUrlOrFile);
433     connect(ui->dbTreeWidget->selectionModel(), &QItemSelectionModel::currentChanged, this, &MainWindow::changeTreeSelection);
434     connect(ui->dockEdit, &QDockWidget::visibilityChanged, this, &MainWindow::toggleEditDock);
435     connect(remoteDock, SIGNAL(openFile(QString)), this, SLOT(fileOpen(QString)));
436     connect(ui->actionDropQualifiedCheck, &QAction::toggled, dbStructureModel, &DbStructureModel::setDropQualifiedNames);
437     connect(ui->actionEnquoteNamesCheck, &QAction::toggled, dbStructureModel, &DbStructureModel::setDropEnquotedNames);
438     connect(&db, &DBBrowserDB::databaseInUseChanged, this, &MainWindow::updateDatabaseBusyStatus);
439 
440     ui->actionDropQualifiedCheck->setChecked(Settings::getValue("SchemaDock", "dropQualifiedNames").toBool());
441     ui->actionEnquoteNamesCheck->setChecked(Settings::getValue("SchemaDock", "dropEnquotedNames").toBool());
442 
443     connect(ui->tableBrowser->model(), &SqliteTableModel::finishedFetch, [this](){
444         auto& settings = ui->tableBrowser->settings(ui->tableBrowser->currentlyBrowsedTableName());
445         plotDock->updatePlot(ui->tableBrowser->model(), &settings, true, false);
446     });
447 
448     connect(ui->actionSqlStop, &QAction::triggered, [this]() {
449        if(execute_sql_worker && execute_sql_worker->isRunning())
450            execute_sql_worker->stop();
451     });
452 
453     // Connect tool pragmas
454     connect(ui->actionIntegrityCheck, &QAction::triggered, [this]() {
455             runSqlNewTab("PRAGMA integrity_check;", ui->actionIntegrityCheck->text(), "https://www.sqlite.org/pragma.html#pragma_integrity_check");
456     });
457     connect(ui->actionQuickCheck, &QAction::triggered, [this]() {
458             runSqlNewTab("PRAGMA quick_check;", ui->actionQuickCheck->text(), "https://www.sqlite.org/pragma.html#pragma_quick_check");
459     });
460     connect(ui->actionForeignKeyCheck, &QAction::triggered, [this]() {
461             runSqlNewTab("PRAGMA foreign_key_check;", ui->actionForeignKeyCheck->text(), "https://www.sqlite.org/pragma.html#pragma_foreign_key_check");
462     });
463     connect(ui->actionOptimize, &QAction::triggered, [this]() {
464             runSqlNewTab("PRAGMA optimize;", ui->actionOptimize->text(), "https://www.sqlite.org/pragma.html#pragma_optimize");
465     });
466 
467     // Action for switching the table via the Database Structure tab
468     connect(ui->actionPopupSchemaDockBrowseTable, &QAction::triggered, [this]() {
469             sqlb::ObjectIdentifier obj(ui->treeSchemaDock->model()->data(ui->treeSchemaDock->currentIndex().sibling(ui->treeSchemaDock->currentIndex().row(), DbStructureModel::ColumnSchema), Qt::EditRole).toString().toStdString(),
470                                        ui->treeSchemaDock->model()->data(ui->treeSchemaDock->currentIndex().sibling(ui->treeSchemaDock->currentIndex().row(), DbStructureModel::ColumnName), Qt::EditRole).toString().toStdString());
471             switchToBrowseDataTab(obj);
472             refresh();  // Required in case the Browse Data tab already was the active main tab
473     });
474 
475     // Set other window settings
476     setAcceptDrops(true);
477     setWindowTitle(QApplication::applicationName());
478 
479     // Add the documentation of shortcuts, which aren't otherwise visible in the user interface, to some buttons.
480     addShortcutsTooltip(ui->actionDbPrint);
481     addShortcutsTooltip(ui->actionSqlOpenTab);
482     addShortcutsTooltip(ui->actionSqlPrint);
483     addShortcutsTooltip(ui->actionExecuteSql, {shortcutBrowseRefreshF5->key(), shortcutBrowseRefreshCtrlR->key()});
484     addShortcutsTooltip(ui->actionSqlExecuteLine);
485     addShortcutsTooltip(ui->actionSqlFind);
486     addShortcutsTooltip(ui->actionSqlFindReplace);
487     addShortcutsTooltip(ui->actionSqlToggleComment);
488 
489     // Load all settings
490     reloadSettings();
491 
492 #ifndef ENABLE_SQLCIPHER
493     // Only show encryption menu action when SQLCipher support is enabled
494     ui->actionEncryption->setVisible(false);
495 #endif
496 
497     /* Remove all the '&' signs from the dock titles. On at least Windows and
498      * OSX, Qt doesn't seem to support them properly, so they end up being
499      * visible instead of creating a keyboard shortcut
500      */
501     ui->dockEdit->setWindowTitle(ui->dockEdit->windowTitle().remove('&'));
502     ui->dockLog->setWindowTitle(ui->dockLog->windowTitle().remove('&'));
503     ui->dockPlot->setWindowTitle(ui->dockPlot->windowTitle().remove('&'));
504     ui->dockSchema->setWindowTitle(ui->dockSchema->windowTitle().remove('&'));
505     ui->dockRemote->setWindowTitle(ui->dockRemote->windowTitle().remove('&'));
506 }
507 
fileOpen(const QString & fileName,bool openFromProject,bool readOnly)508 bool MainWindow::fileOpen(const QString& fileName, bool openFromProject, bool readOnly)
509 {
510     bool retval = false;
511 
512     QString wFile = fileName;
513     // QFile::exist will produce error message if passed empty string.
514     // Test string length before usage w/ QFile to silence warning
515     if (wFile.isEmpty() || !QFile::exists(wFile))
516     {
517         wFile = FileDialog::getOpenFileName(
518                     OpenDatabaseFile,
519                     this,
520                     tr("Choose a database file")
521 #ifndef Q_OS_MAC // Filters on OS X are buggy
522                     , FileDialog::getSqlDatabaseFileFilter()
523 #endif
524                     );
525     }
526     // catch situation where user has canceled file selection from dialog
527     if(!wFile.isEmpty() && QFile::exists(wFile) )
528     {
529         // Close the database. If the user didn't want to close it, though, stop here
530         if (db.isOpen())
531             if(!fileClose())
532                 return false;
533 
534         // Try opening it as a project file first
535         if(loadProject(wFile, readOnly))
536         {
537             retval = true;
538         } else {
539             // No project file; so it should be a database file
540             if(db.open(wFile, readOnly))
541             {
542                 // Close all open but empty SQL tabs
543                 for(int i=ui->tabSqlAreas->count()-1;i>=0;i--)
544                 {
545                     if(qobject_cast<SqlExecutionArea*>(ui->tabSqlAreas->widget(i))->getSql().trimmed().isEmpty())
546                         closeSqlTab(i, true);
547                 }
548 
549                 statusEncodingLabel->setText(db.getPragma("encoding"));
550                 statusEncryptionLabel->setVisible(db.encrypted());
551                 statusReadOnlyLabel->setVisible(db.readOnly());
552                 setCurrentFile(wFile);
553                 if(!openFromProject) {
554                     addToRecentFilesMenu(wFile, readOnly);
555                     // When a new DB file has been open while a project is open, set the project modified.
556                     if(!currentProjectFilename.isEmpty())
557                         isProjectModified = true;
558                 }
559                 if(ui->tabSqlAreas->count() == 0)
560                     openSqlTab(true);
561                 if(ui->mainTab->currentWidget() == ui->browser)
562                     populateTable();
563                 else if(ui->mainTab->currentWidget() == ui->pragmas)
564                     loadPragmas();
565 
566                 // Update remote dock
567                 remoteDock->fileOpened(wFile);
568 
569                 retval = true;
570             } else {
571                 QMessageBox::warning(this, qApp->applicationName(), tr("Could not open database file.\nReason: %1").arg(db.lastError()));
572                 return false;
573             }
574         }
575     }
576 
577     return retval;
578 }
579 
fileNew()580 void MainWindow::fileNew()
581 {
582     QString fileName = FileDialog::getSaveFileName(
583                            CreateDatabaseFile,
584                            this,
585                            tr("Choose a filename to save under"),
586                            FileDialog::getSqlDatabaseFileFilter());
587     if(!fileName.isEmpty())
588     {
589         if(QFile::exists(fileName))
590             QFile::remove(fileName);
591         db.create(fileName);
592         setCurrentFile(fileName);
593         addToRecentFilesMenu(fileName);
594         statusEncodingLabel->setText(db.getPragma("encoding"));
595         statusEncryptionLabel->setVisible(false);
596         statusReadOnlyLabel->setVisible(false);
597         populateTable();
598         if(ui->tabSqlAreas->count() == 0)
599             openSqlTab(true);
600         createTable();
601     }
602 }
603 
fileNewInMemoryDatabase()604 void MainWindow::fileNewInMemoryDatabase()
605 {
606     db.create(":memory:");
607     setCurrentFile(tr("In-Memory database"));
608     statusEncodingLabel->setText(db.getPragma("encoding"));
609     statusEncryptionLabel->setVisible(false);
610     statusReadOnlyLabel->setVisible(false);
611     remoteDock->fileOpened(":memory:");
612     populateTable();
613     if(ui->tabSqlAreas->count() == 0)
614         openSqlTab(true);
615     createTable();
616 }
617 
populateStructure(const sqlb::ObjectIdentifier & old_table)618 void MainWindow::populateStructure(const sqlb::ObjectIdentifier& old_table)
619 {
620     // Refresh the structure tab
621     ui->dbTreeWidget->setRootIndex(dbStructureModel->index(1, 0));      // Show the 'All' part of the db structure
622     ui->dbTreeWidget->expandToDepth(0);
623     ui->treeSchemaDock->setRootIndex(dbStructureModel->index(1, 0));    // Show the 'All' part of the db structure
624     ui->treeSchemaDock->expandToDepth(0);
625 
626     // Refresh the browse data tab
627     ui->tableBrowser->setStructure(dbStructureModel, old_table);
628 
629     // Cancel here if no database is opened
630     if(!db.isOpen())
631         return;
632 
633     // Update table and column names for syntax highlighting
634     SqlUiLexer::QualifiedTablesMap qualifiedTablesMap;
635     for(const auto& it : db.schemata)
636     {
637         SqlUiLexer::TablesAndColumnsMap tablesToColumnsMap;
638 
639         for(const auto& jt : it.second)
640         {
641             if(jt.second->type() == sqlb::Object::Types::Table || jt.second->type() == sqlb::Object::Types::View)
642             {
643                 QString objectname = QString::fromStdString(jt.second->name());
644 
645                 sqlb::FieldInfoList fi = jt.second->fieldInformation();
646                 for(const sqlb::FieldInfo& f : fi)
647                     tablesToColumnsMap[objectname].push_back(QString::fromStdString(f.name));
648             }
649         }
650 
651         qualifiedTablesMap[QString::fromStdString(it.first)] = tablesToColumnsMap;
652     }
653     SqlTextEdit::sqlLexer->setTableNames(qualifiedTablesMap);
654     ui->editLogApplication->reloadKeywords();
655     ui->editLogUser->reloadKeywords();
656     for(int i=0;i<ui->tabSqlAreas->count();i++)
657         qobject_cast<SqlExecutionArea*>(ui->tabSqlAreas->widget(i))->getEditor()->reloadKeywords();
658 
659     // Resize SQL column to fit contents
660     ui->dbTreeWidget->resizeColumnToContents(DbStructureModel::ColumnSQL);
661     ui->treeSchemaDock->resizeColumnToContents(DbStructureModel::ColumnSQL);
662     // Resize also the Name column in the Dock since it has usually
663     // short content and there is little space there.
664     ui->treeSchemaDock->resizeColumnToContents(DbStructureModel::ColumnName);
665 
666 
667 }
668 
populateTable()669 void MainWindow::populateTable()
670 {
671     // Early exit if the Browse Data tab isn't visible as there is no need to update it in this case
672     if(ui->mainTab->currentWidget() != ui->browser)
673         return;
674 
675     QApplication::setOverrideCursor(Qt::WaitCursor);
676     ui->tableBrowser->updateTable();
677     QApplication::restoreOverrideCursor();
678 }
679 
fileClose()680 bool MainWindow::fileClose()
681 {
682     // Stop any running SQL statements before closing the database
683     if(execute_sql_worker && execute_sql_worker->isRunning())
684     {
685         if(QMessageBox::warning(this, qApp->applicationName(),
686                                 tr("You are still executing SQL statements. Closing the database now will stop their execution, possibly "
687                                    "leaving the database in an inconsistent state. Are you sure you want to close the database?"),
688                                 QMessageBox::Yes, QMessageBox::Cancel | QMessageBox::Default | QMessageBox::Escape) == QMessageBox::Cancel)
689             return false;
690 
691         execute_sql_worker->stop();
692         execute_sql_worker->wait();
693     }
694 
695     // Close the database but stop the closing process here if the user pressed the cancel button in there
696     if(!db.close())
697         return false;
698 
699     setCurrentFile(QString());
700     loadPragmas();
701     statusEncryptionLabel->setVisible(false);
702     statusReadOnlyLabel->setVisible(false);
703 
704     // Reset the table browser of the Browse Data tab
705     ui->tableBrowser->reset();
706 
707     // Clear edit dock
708     editDock->setCurrentIndex(QModelIndex());
709 
710     // Clear the SQL Log
711     ui->editLogApplication->clear();
712     ui->editLogUser->clear();
713     ui->editLogErrorLog->clear();
714 
715     // Remove completion and highlighting for identifiers
716     SqlTextEdit::sqlLexer->setTableNames(SqlUiLexer::QualifiedTablesMap());
717     for(int i=0; i < ui->tabSqlAreas->count(); i++)
718         qobject_cast<SqlExecutionArea*>(ui->tabSqlAreas->widget(i))->getEditor()->reloadKeywords();
719 
720     // Clear remote dock
721     remoteDock->fileOpened(QString());
722 
723     return true;
724 }
725 
closeEvent(QCloseEvent * event)726 void MainWindow::closeEvent( QCloseEvent* event )
727 {
728     if(closeFiles())
729     {
730         Settings::setValue("MainWindow", "geometry", saveGeometry());
731         Settings::setValue("MainWindow", "windowState", saveState());
732         Settings::setValue("MainWindow", "openTabs", saveOpenTabs());
733 
734         Settings::setValue("SQLLogDock", "Log", ui->comboLogSubmittedBy->currentText());
735         Settings::setValue("SchemaDock", "dropQualifiedNames", ui->actionDropQualifiedCheck->isChecked());
736         Settings::setValue("SchemaDock", "dropEnquotedNames", ui->actionEnquoteNamesCheck->isChecked());
737 
738         SqlExecutionArea::saveState();
739 
740         QMainWindow::closeEvent(event);
741     } else {
742         event->ignore();
743     }
744 }
745 
closeFiles()746 bool MainWindow::closeFiles()
747 {
748     bool ignoreUnattachedBuffers = false;
749     // Ask for saving all modified open SQL files in their files and all the unattached tabs in a project file.
750     for(int i=0; i<ui->tabSqlAreas->count(); i++)
751         // Ask for saving and comply with cancel answer.
752         if(!askSaveSqlTab(i, ignoreUnattachedBuffers))
753             return false;
754     return closeProject();
755 }
756 
closeProject()757 bool MainWindow::closeProject()
758 {
759     if(!currentProjectFilename.isEmpty() && isProjectModified) {
760         QMessageBox::StandardButton reply = QMessageBox::question
761             (nullptr,
762              QApplication::applicationName(),
763              tr("Do you want to save the changes made to the project file '%1'?").
764              arg(QFileInfo(currentProjectFilename).fileName()),
765              QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel);
766         switch(reply) {
767         case QMessageBox::Save:
768             saveProject();
769             break;
770         case QMessageBox::Cancel:
771             return false;
772         default:
773             break;
774         }
775     }
776     currentProjectFilename.clear();
777     return db.close();
778 }
779 
attachPlot(ExtendedTableWidget * tableWidget,SqliteTableModel * model,BrowseDataTableSettings * settings,bool keepOrResetSelection)780 void MainWindow::attachPlot(ExtendedTableWidget* tableWidget, SqliteTableModel* model, BrowseDataTableSettings* settings, bool keepOrResetSelection)
781 {
782     plotDock->updatePlot(model, settings, true, keepOrResetSelection);
783     // Disconnect previous connection
784     disconnect(plotDock, SIGNAL(pointsSelected(int,int)), nullptr, nullptr);
785     if(tableWidget) {
786         // Connect plot selection to the current table results widget.
787         connect(plotDock, SIGNAL(pointsSelected(int,int)), tableWidget, SLOT(selectTableLines(int, int)));
788         connect(tableWidget, &ExtendedTableWidget::destroyed, plotDock, &PlotDock::resetPlot);
789         // Disconnect requestUrlOrFileOpen in order to make sure that there is only one connection. Otherwise we can open it several times.
790         disconnect(tableWidget, &ExtendedTableWidget::requestUrlOrFileOpen, this, &MainWindow::openUrlOrFile);
791         connect(tableWidget, &ExtendedTableWidget::requestUrlOrFileOpen, this, &MainWindow::openUrlOrFile);
792     }
793 }
794 
refresh()795 void MainWindow::refresh()
796 {
797     // What the Refresh function does depends on the currently active tab. This way the keyboard shortcuts (F5 and Ctrl+R)
798     // always perform some meaningful task; they just happen to be context dependent in the function they trigger.
799     QWidget* currentTab = ui->mainTab->currentWidget();
800     if (currentTab == ui->structure) {
801         // Refresh the schema
802         db.updateSchema();
803     } else if (currentTab == ui->browser) {
804         // Refresh the schema and reload the current table
805         populateTable();
806     } else if (currentTab == ui->pragmas) {
807         // Reload pragma values
808         loadPragmas();
809     } else if (currentTab == ui->query) {
810         // (Re-)Run the current SQL query
811         executeQuery();
812     }
813 }
814 
createTable()815 void MainWindow::createTable()
816 {
817     EditTableDialog dialog(db, sqlb::ObjectIdentifier(), true, this);
818     if(dialog.exec())
819     {
820         populateTable();
821     }
822 }
823 
createIndex()824 void MainWindow::createIndex()
825 {
826     EditIndexDialog dialog(db, sqlb::ObjectIdentifier(), true, this);
827     if(dialog.exec())
828         populateTable();
829 }
830 
compact()831 void MainWindow::compact()
832 {
833     VacuumDialog dialog(&db, this);
834     dialog.exec();
835 }
836 
deleteObject()837 void MainWindow::deleteObject()
838 {
839     // Get name and type of object to delete
840     sqlb::ObjectIdentifier name(ui->dbTreeWidget->model()->data(ui->dbTreeWidget->currentIndex().sibling(ui->dbTreeWidget->currentIndex().row(), DbStructureModel::ColumnSchema), Qt::EditRole).toString().toStdString(),
841                                 ui->dbTreeWidget->model()->data(ui->dbTreeWidget->currentIndex().sibling(ui->dbTreeWidget->currentIndex().row(), DbStructureModel::ColumnName), Qt::EditRole).toString().toStdString());
842     QString type = ui->dbTreeWidget->model()->data(ui->dbTreeWidget->currentIndex().sibling(ui->dbTreeWidget->currentIndex().row(), DbStructureModel::ColumnObjectType), Qt::EditRole).toString();
843 
844     // Due to different grammar in languages (e.g. gender or declension), each message must be given separately to translation.
845     QString message;
846     if (type == "table")
847         message = tr("Are you sure you want to delete the table '%1'?\nAll data associated with the table will be lost.");
848     else if (type == "view")
849         message = tr("Are you sure you want to delete the view '%1'?");
850     else if (type == "trigger")
851         message = tr("Are you sure you want to delete the trigger '%1'?");
852     else if (type == "index")
853         message = tr("Are you sure you want to delete the index '%1'?");
854 
855     // Ask user if he really wants to delete that table
856     if(QMessageBox::warning(this, QApplication::applicationName(), message.arg(QString::fromStdString(name.name())),
857                             QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes)
858     {
859         // Delete the table
860         QString statement = QString("DROP %1 %2;").arg(type.toUpper(), QString::fromStdString(name.toString()));
861         if(!db.executeSQL(statement.toStdString()))
862         {
863             if (type == "table")
864                 message = tr("Error: could not delete the table.");
865             else if (type == "view")
866                 message = tr("Error: could not delete the view.");
867             else if (type == "trigger")
868                 message = tr("Error: could not delete the trigger.");
869             else if (type == "index")
870                 message = tr("Error: could not delete the index.");
871 
872             QString error = tr("Message from database engine:\n%1").arg(db.lastError());
873             QMessageBox::warning(this, QApplication::applicationName(), message + " " + error);
874         } else {
875             populateTable();
876             changeTreeSelection();
877         }
878     }
879 }
880 
editObject()881 void MainWindow::editObject()
882 {
883     if(!ui->dbTreeWidget->selectionModel()->hasSelection())
884         return;
885 
886     // Get name and type of the object to edit
887     sqlb::ObjectIdentifier name(ui->dbTreeWidget->model()->data(ui->dbTreeWidget->currentIndex().sibling(ui->dbTreeWidget->currentIndex().row(), DbStructureModel::ColumnSchema), Qt::EditRole).toString().toStdString(),
888                                 ui->dbTreeWidget->model()->data(ui->dbTreeWidget->currentIndex().sibling(ui->dbTreeWidget->currentIndex().row(), DbStructureModel::ColumnName), Qt::EditRole).toString().toStdString());
889     QString type = ui->dbTreeWidget->model()->data(ui->dbTreeWidget->currentIndex().sibling(ui->dbTreeWidget->currentIndex().row(), DbStructureModel::ColumnObjectType), Qt::EditRole).toString();
890 
891     if(type == "table")
892     {
893         // For a safe and possibly complex table modification we must follow the steps documented in
894         // https://www.sqlite.org/lang_altertable.html
895         // Paragraph (first procedure): Making Other Kinds Of Table Schema Changes
896 
897         QString foreign_keys = db.getPragma("foreign_keys");
898         if (foreign_keys == "1") {
899             if(db.getDirty() && QMessageBox::question(this,
900                                      QApplication::applicationName(),
901                                      tr("Editing the table requires to save all pending changes now.\nAre you sure you want to save the database?"),
902                                      QMessageBox::Save | QMessageBox::Default,
903                                      QMessageBox::Cancel | QMessageBox::Escape) != QMessageBox::Save)
904                 return;
905             // Commit all changes so the foreign_keys can be effective.
906             fileSave();
907             db.setPragma("foreign_keys", "0");
908         }
909 
910         EditTableDialog dialog(db, name, false, this);
911         bool ok = dialog.exec();
912 
913         // If foreign_keys were enabled, we must commit or rollback the transaction so the foreign_keys pragma can be restored.
914         if (foreign_keys == "1") {
915             if (!db.querySingleValueFromDb("PRAGMA " + sqlb::escapeIdentifier(name.schema()) + ".foreign_key_check").isNull()) {
916                 // Raise warning for accepted modification. When rejected, warn user also since we know now that the table has problems,
917                 // but it wasn't our fault.
918                 if (ok)
919                     QMessageBox::warning(this, QApplication::applicationName(),
920                                          tr("Error checking foreign keys after table modification. The changes will be reverted."));
921                 else
922                     QMessageBox::warning(this, QApplication::applicationName(),
923                                          tr("This table did not pass a foreign-key check.<br/>"
924                                             "You should run 'Tools | Foreign-Key Check' and fix the reported issues."));
925                 db.revertAll();
926             } else {
927                 // Commit all changes so the foreign_keys can be effective.
928                 fileSave();
929             }
930             db.setPragma("foreign_keys", foreign_keys);
931         }
932         if(ok) {
933             ui->tableBrowser->clearFilters();
934             populateTable();
935         }
936     } else if(type == "index") {
937         EditIndexDialog dialog(db, name, false, this);
938         if(dialog.exec())
939             populateTable();
940     } else if(type == "view") {
941         sqlb::ViewPtr view = db.getObjectByName<sqlb::View>(name);
942         runSqlNewTab(QString("DROP VIEW %1;\n%2").arg(QString::fromStdString(name.toString())).arg(QString::fromStdString(view->sql())),
943                      tr("Edit View %1").arg(QString::fromStdString(name.toDisplayString())),
944                      "https://www.sqlite.org/lang_createview.html",
945                      /* autoRun */ false);
946     } else if(type == "trigger") {
947         sqlb::TriggerPtr trigger = db.getObjectByName<sqlb::Trigger>(name);
948         runSqlNewTab(QString("DROP TRIGGER %1;\n%2").arg(QString::fromStdString(name.toString())).arg(QString::fromStdString(trigger->sql())),
949                      tr("Edit Trigger %1").arg(QString::fromStdString(name.toDisplayString())),
950                      "https://www.sqlite.org/lang_createtrigger.html",
951                      /* autoRun */ false);
952     }
953 }
954 
helpWhatsThis()955 void MainWindow::helpWhatsThis()
956 {
957     QWhatsThis::enterWhatsThisMode ();
958 }
959 
helpAbout()960 void MainWindow::helpAbout()
961 {
962     AboutDialog dialog(this);
963     dialog.exec();
964 }
965 
updateRecordText(const QPersistentModelIndex & idx,const QByteArray & text,bool isBlob)966 void MainWindow::updateRecordText(const QPersistentModelIndex& idx, const QByteArray& text, bool isBlob)
967 {
968     m_currentTabTableModel->setTypedData(idx, isBlob, text);
969 }
970 
toggleEditDock(bool visible)971 void MainWindow::toggleEditDock(bool visible)
972 {
973     if (!visible) {
974         // Update main window
975         ui->tableBrowser->setFocus();
976     } else {
977         // fill edit dock with actual data, when the current index has changed while the dock was invisible.
978         // (note that this signal is also emitted when the widget is docked or undocked, so we have to avoid
979         // reloading data when the user is editing and (un)docks the editor).
980         if (editDock->currentIndex() != ui->tableBrowser->currentIndex())
981             editDock->setCurrentIndex(ui->tableBrowser->currentIndex());
982     }
983 }
984 
doubleClickTable(const QModelIndex & index)985 void MainWindow::doubleClickTable(const QModelIndex& index)
986 {
987     // Cancel on invalid index
988     if (!index.isValid()) {
989         return;
990     }
991 
992     // * Don't allow editing of other objects than tables and editable views
993     bool isEditingAllowed = !db.readOnly() && m_currentTabTableModel == ui->tableBrowser->model() &&
994             ui->tableBrowser->model()->isEditable(index);
995 
996     // Enable or disable the Apply, Null, & Import buttons in the Edit Cell
997     // dock depending on the value of the "isEditingAllowed" bool above
998     editDock->setReadOnly(!isEditingAllowed);
999 
1000     editDock->setCurrentIndex(index);
1001 
1002     // Show the edit dock
1003     ui->dockEdit->setVisible(true);
1004 
1005     // Set focus on the edit dock
1006     editDock->setFocus();
1007 }
1008 
dataTableSelectionChanged(const QModelIndex & index)1009 void MainWindow::dataTableSelectionChanged(const QModelIndex& index)
1010 {
1011     // Cancel on invalid index
1012     if(!index.isValid()) {
1013         editDock->setCurrentIndex(QModelIndex());
1014         return;
1015     }
1016 
1017     bool editingAllowed = !db.readOnly() && (m_currentTabTableModel == ui->tableBrowser->model()) &&
1018             ui->tableBrowser->model()->isEditable(index);
1019 
1020     // Don't allow editing of other objects than tables and editable views
1021     editDock->setReadOnly(!editingAllowed);
1022 
1023     // If the Edit Cell dock is visible, load the new value into it
1024     if (editDock->isVisible()) {
1025         editDock->setCurrentIndex(index);
1026     }
1027 }
1028 
1029 /*
1030  * I'm still not happy how the results are represented to the user
1031  * right now you only see the result of the last executed statement.
1032  * A better experience would be tabs on the bottom with query results
1033  * for all the executed statements.
1034  */
executeQuery()1035 void MainWindow::executeQuery()
1036 {
1037     // Make sure a database is opened. This is necessary because we allow opened SQL editor tabs even if no database is loaded. Hitting F5 or similar
1038     // then might call this function.
1039     if(!db.isOpen())
1040         return;
1041 
1042     // Check if other task is still running and stop it if necessary
1043     if(execute_sql_worker && execute_sql_worker->isRunning())
1044     {
1045         // Ask the user and do nothing if he/she doesn't want to interrupt the running query
1046         if(QMessageBox::warning(this, qApp->applicationName(),
1047                                 tr("You are already executing SQL statements. Do you want to stop them in order to execute the current "
1048                                    "statements instead? Note that this might leave the database in an inconsistent state."),
1049                                 QMessageBox::Yes, QMessageBox::Cancel | QMessageBox::Default | QMessageBox::Escape) == QMessageBox::Cancel)
1050             return;
1051 
1052         // Stop the running query
1053         execute_sql_worker->stop();
1054         execute_sql_worker->wait();
1055     }
1056 
1057     // Get current SQL tab and editor
1058     SqlExecutionArea* sqlWidget = qobject_cast<SqlExecutionArea*>(ui->tabSqlAreas->currentWidget());
1059     SqlTextEdit* editor = sqlWidget->getEditor();
1060     auto* current_tab = ui->tabSqlAreas->currentWidget();
1061     const QString tabName = ui->tabSqlAreas->tabText(ui->tabSqlAreas->currentIndex()).remove('&');
1062 
1063     // Remove any error indicators
1064     editor->clearErrorIndicators();
1065 
1066     // Determine execution mode: execute all, execute selection or execute current line
1067     enum executionMode
1068     {
1069         All,
1070         Selection,
1071         Line
1072     } mode;
1073     if(sender() && sender()->objectName() == "actionSqlExecuteLine")
1074         mode = Line;
1075     else if(!sqlWidget->getSelectedSql().isEmpty())
1076         mode = Selection;
1077     else
1078         mode = All;
1079 
1080     // Get SQL code to execute. This depends on the execution mode.
1081     int execute_from_position = 0;      // Where we want to start the execution in the query string
1082     int execute_to_position = 0;        // Where we roughly want to end the execution in the query string
1083 
1084     switch(mode)
1085     {
1086     case Selection:
1087         {
1088             // Start and end positions are start and end positions from the selection
1089             int execute_from_line, execute_from_index, execute_to_line, execute_to_index;
1090             editor->getSelection(&execute_from_line, &execute_from_index, &execute_to_line, &execute_to_index);
1091             execute_from_position = editor->positionFromLineIndex(execute_from_line, execute_from_index);
1092             execute_to_position = editor->positionFromLineIndex(execute_to_line, execute_to_index);
1093 
1094             db.logSQL(tr("-- EXECUTING SELECTION IN '%1'\n--").arg(tabName), kLogMsg_User);
1095         } break;
1096     case Line:
1097         {
1098             // Start position is the first character of the current line, except for those cases where we're in the middle of a
1099             // statement which started on one the previous line. In that case the start position is actually a bit earlier. For
1100             // the end position we set the last character of the current line. If the statement(s) continue(s) into the next line,
1101             // SQLite will execute it/them anyway and we'll stop afterwards.
1102             int execute_from_line, dummy;
1103             editor->getCursorPosition(&execute_from_line, &dummy);
1104             execute_from_position = editor->positionFromLineIndex(execute_from_line, 0);
1105 
1106             // Need to set the end position here before adjusting the start line
1107             int execute_to_line = execute_from_line;
1108             int execute_to_index = editor->text(execute_to_line).remove('\n').remove('\r').length();     // This chops the line break at the end of the line
1109             execute_to_position = editor->positionFromLineIndex(execute_to_line, execute_to_index);
1110 
1111             QByteArray firstPartEntireSQL = sqlWidget->getSql().toUtf8().left(execute_from_position);
1112             if(firstPartEntireSQL.lastIndexOf(';') != -1)
1113                 execute_from_position -= firstPartEntireSQL.length() - firstPartEntireSQL.lastIndexOf(';') - 1;
1114 
1115             db.logSQL(tr("-- EXECUTING LINE IN '%1'\n--").arg(tabName), kLogMsg_User);
1116         } break;
1117     case All:
1118         {
1119             // Start position is the first byte, end position the last.
1120             // Note that we use byte positions that might differ from character positions.
1121             execute_to_position = editor->length();
1122 
1123             db.logSQL(tr("-- EXECUTING ALL IN '%1'\n--").arg(tabName), kLogMsg_User);
1124         } break;
1125     }
1126 
1127     // Prepare a lambda function for logging the results of a query
1128     auto query_logger = [this, sqlWidget, editor](bool ok, const QString& status_message, int from_position, int to_position) {
1129         int execute_from_line, execute_from_index;
1130         editor->lineIndexFromPosition(from_position, &execute_from_line, &execute_from_index);
1131 
1132         // Special case: if the start position is at the end of a line, then move to the beginning of next line.
1133         // Otherwise for the typical case, the line reference is one less than expected.
1134         // Note that execute_from_index uses character positions and not byte positions, so at() can be used.
1135         QChar char_at_index = editor->text(execute_from_line).at(execute_from_index);
1136         if (char_at_index == '\r' || char_at_index == '\n') {
1137             execute_from_line++;
1138             // The next lines could be empty, so skip all of them too.
1139             while(editor->text(execute_from_line).trimmed().isEmpty())
1140                 execute_from_line++;
1141             execute_from_index = 0;
1142         }
1143 
1144         // If there was an error highlight the erroneous SQL statement
1145         if(!ok)
1146         {
1147             int end_of_current_statement_line, end_of_current_statement_index;
1148             editor->lineIndexFromPosition(to_position, &end_of_current_statement_line, &end_of_current_statement_index);
1149             editor->setErrorIndicator(execute_from_line, execute_from_index, end_of_current_statement_line, end_of_current_statement_index);
1150 
1151             editor->setCursorPosition(execute_from_line, execute_from_index);
1152         }
1153 
1154         // Log the query and the result message.
1155         // The query takes the last placeholder as it may itself contain the sequence '%' + number.
1156         QString query = editor->text(from_position, to_position);
1157         QString log_message = "-- " + tr("At line %1:").arg(execute_from_line+1) + "\n" + query.trimmed() + "\n-- " + tr("Result: %1").arg(status_message);
1158         db.logSQL(log_message, kLogMsg_User);
1159 
1160         log_message = tr("Result: %2").arg(status_message) + "\n" + tr("At line %1:").arg(execute_from_line+1) + "\n" + query.trimmed();
1161         // Update the execution area
1162         sqlWidget->finishExecution(log_message, ok);
1163     };
1164 
1165     // Get the statement(s) to execute. When in selection mode crop the query string at exactly the end of the selection to make sure SQLite has
1166     // no chance to execute any further.
1167     QString sql = sqlWidget->getSql();
1168     if(mode == Selection)
1169         sql = sql.toUtf8().left(execute_to_position);   // We have to convert to a QByteArray here because QScintilla gives us the position in bytes, not in characters.
1170 
1171     // Prepare the SQL worker to run the query. We set the context of each signal-slot connection to the current SQL execution area.
1172     // This means that if the tab is closed all these signals are automatically disconnected so the lambdas won't be called for a not
1173     // existing execution area.
1174     execute_sql_worker.reset(new RunSql(db, sql, execute_from_position, execute_to_position, true));
1175 
1176     connect(execute_sql_worker.get(), &RunSql::structureUpdated, sqlWidget, [this]() {
1177         db.updateSchema();
1178     }, Qt::QueuedConnection);
1179     connect(execute_sql_worker.get(), &RunSql::statementErrored, sqlWidget, [query_logger, this, sqlWidget](const QString& status_message, int from_position, int to_position) {
1180         sqlWidget->getModel()->reset();
1181         ui->actionSqlResultsSave->setEnabled(false);
1182         ui->actionSqlResultsSaveAsView->setEnabled(false);
1183         attachPlot(sqlWidget->getTableResult(), sqlWidget->getModel());
1184 
1185         query_logger(false, status_message, from_position, to_position);
1186     }, Qt::QueuedConnection);
1187     connect(execute_sql_worker.get(), &RunSql::statementExecuted, sqlWidget, [query_logger, this, sqlWidget](const QString& status_message, int from_position, int to_position) {
1188         sqlWidget->getModel()->reset();
1189         ui->actionSqlResultsSave->setEnabled(false);
1190         ui->actionSqlResultsSaveAsView->setEnabled(false);
1191         attachPlot(sqlWidget->getTableResult(), sqlWidget->getModel());
1192 
1193         query_logger(true, status_message, from_position, to_position);
1194         execute_sql_worker->startNextStatement();
1195     }, Qt::QueuedConnection);
1196     connect(execute_sql_worker.get(), &RunSql::statementReturnsRows, sqlWidget, [query_logger, this, sqlWidget](const QString& query, int from_position, int to_position, qint64 time_in_ms_so_far) {
1197         auto time_start = std::chrono::high_resolution_clock::now();
1198 
1199         ui->actionSqlResultsSave->setEnabled(true);
1200         ui->actionSqlResultsSaveAsView->setEnabled(!db.readOnly());
1201 
1202         auto * model = sqlWidget->getModel();
1203         model->setQuery(query);
1204 
1205         // Wait until the initial loading of data (= first chunk and row count) has been performed
1206         auto conn = std::make_shared<QMetaObject::Connection>();
1207         *conn = connect(model, &SqliteTableModel::finishedFetch, [=](int fetched_row_begin, int fetched_row_end) {
1208             // Avoid attaching the plot when the signal is notifying the row count, since the
1209             // data wouldn't be available yet.
1210             if(fetched_row_begin != fetched_row_end && fetched_row_begin != model->rowCount()) {
1211                 // Disconnect this connection right now. This avoids calling this slot multiple times
1212                 disconnect(*conn);
1213 
1214                 attachPlot(sqlWidget->getTableResult(), sqlWidget->getModel());
1215             } else {
1216                 connect(sqlWidget->getTableResult()->selectionModel(), &QItemSelectionModel::currentChanged, this, &MainWindow::dataTableSelectionChanged);
1217                 connect(sqlWidget->getTableResult(), &QTableView::doubleClicked, this, &MainWindow::doubleClickTable);
1218 
1219                 auto time_end = std::chrono::high_resolution_clock::now();
1220                 auto time_in_ms = std::chrono::duration_cast<std::chrono::milliseconds>(time_end-time_start);
1221                 query_logger(true, tr("%1 rows returned in %2ms").arg(model->rowCount()).arg(time_in_ms.count()+time_in_ms_so_far), from_position, to_position);
1222                 execute_sql_worker->startNextStatement();
1223             }
1224         });
1225     }, Qt::QueuedConnection);
1226     connect(execute_sql_worker.get(), &RunSql::confirmSaveBeforePragmaOrVacuum, sqlWidget, [this]() {
1227         if(QMessageBox::question(nullptr, QApplication::applicationName(),
1228                                  tr("Setting PRAGMA values or vacuuming will commit your current transaction.\nAre you sure?"),
1229                                  QMessageBox::Yes | QMessageBox::Default,
1230                                  QMessageBox::No | QMessageBox::Escape) == QMessageBox::No)
1231             execute_sql_worker->stop();
1232 
1233     }, Qt::BlockingQueuedConnection);
1234     connect(execute_sql_worker.get(), &RunSql::finished, sqlWidget, [this, current_tab, sqlWidget]() {
1235         // We work with a pointer to the current tab here instead of its index because the user might reorder the tabs in the meantime.
1236         // We set different icons for general tabs, which are either new or loaded from the project file, and for tabs loaded from a file.
1237         if(sqlWidget->fileName().isEmpty())
1238             ui->tabSqlAreas->setTabIcon(ui->tabSqlAreas->indexOf(current_tab), QIcon(":/icons/open_sql"));
1239         else
1240             ui->tabSqlAreas->setTabIcon(ui->tabSqlAreas->indexOf(current_tab), QIcon(":/icons/document_open"));
1241 
1242         // Set no-running-query state
1243         ui->tabSqlAreas->tabBar()->setTabData(ui->tabSqlAreas->indexOf(current_tab), QVariant(false));
1244 
1245         // We don't need to check for the current SQL tab here because two concurrently running queries are not allowed
1246         ui->actionSqlExecuteLine->setEnabled(true);
1247         ui->actionExecuteSql->setEnabled(true);
1248         ui->actionSqlStop->setEnabled(false);
1249         sqlWidget->getEditor()->setReadOnly(false);
1250 
1251         // Show Done message
1252         if(sqlWidget->inErrorState())
1253             sqlWidget->getStatusEdit()->setPlainText(tr("Execution finished with errors.") + "\n" + sqlWidget->getStatusEdit()->toPlainText());
1254         else
1255             sqlWidget->getStatusEdit()->setPlainText(tr("Execution finished without errors.") + "\n" + sqlWidget->getStatusEdit()->toPlainText());
1256     });
1257 
1258     // Add an hourglass icon to the current tab to indicate that there's a running execution in there.
1259     ui->tabSqlAreas->setTabIcon(ui->tabSqlAreas->currentIndex(), QIcon(":icons/hourglass"));
1260     // We use the tab data to check whether a specific SQL tab is currently running a query or not.
1261     ui->tabSqlAreas->tabBar()->setTabData(ui->tabSqlAreas->currentIndex(), QVariant(true));
1262 
1263     // Deactivate the buttons to start a query and activate the button to stop the query
1264     ui->actionSqlExecuteLine->setEnabled(false);
1265     ui->actionExecuteSql->setEnabled(false);
1266     ui->actionSqlStop->setEnabled(true);
1267 
1268     // Make the SQL editor widget read-only. We do this because the error indicators would be misplaced if the user changed the SQL text during execution
1269     sqlWidget->getEditor()->setReadOnly(true);
1270 
1271     // Start the execution
1272     execute_sql_worker->start();
1273 }
1274 
mainTabSelected(int)1275 void MainWindow::mainTabSelected(int /*tabindex*/)
1276 {
1277     editDock->setReadOnly(true);
1278 
1279     if(ui->mainTab->currentWidget() == ui->browser)
1280     {
1281         m_currentTabTableModel = ui->tableBrowser->model();
1282         populateTable();
1283     } else if(ui->mainTab->currentWidget() == ui->pragmas) {
1284         loadPragmas();
1285     } else if(ui->mainTab->currentWidget() == ui->query) {
1286         SqlExecutionArea* sqlWidget = qobject_cast<SqlExecutionArea*>(ui->tabSqlAreas->currentWidget());
1287 
1288         if (sqlWidget) {
1289             m_currentTabTableModel = sqlWidget->getModel();
1290 
1291             dataTableSelectionChanged(sqlWidget->getTableResult()->currentIndex());
1292         }
1293     }
1294 }
1295 
importTableFromCSV()1296 void MainWindow::importTableFromCSV()
1297 {
1298     QStringList file_filter;
1299     file_filter << FILE_FILTER_CSV
1300                 << FILE_FILTER_TSV
1301                 << FILE_FILTER_DSV
1302                 << FILE_FILTER_TXT
1303                 << FILE_FILTER_DAT
1304                 << FILE_FILTER_ALL;
1305 
1306     QStringList wFiles = FileDialog::getOpenFileNames(
1307                              OpenCSVFile,
1308                              this,
1309                              tr("Choose text files"),
1310                              file_filter.join(";;"));
1311 
1312     std::vector<QString> validFiles;
1313     for(const auto& file : wFiles) {
1314         if (QFile::exists(file))
1315             validFiles.push_back(file);
1316     }
1317 
1318     if (!validFiles.empty())
1319     {
1320         ImportCsvDialog dialog(validFiles, &db, this);
1321         if (dialog.exec())
1322             populateTable();
1323     }
1324 }
1325 
exportTableToCSV()1326 void MainWindow::exportTableToCSV()
1327 {
1328     // Get the current table name if we are in the Browse Data tab
1329     sqlb::ObjectIdentifier current_table;
1330     if(ui->mainTab->currentWidget() == ui->structure)
1331     {
1332         QString type = ui->dbTreeWidget->model()->data(ui->dbTreeWidget->currentIndex().sibling(ui->dbTreeWidget->currentIndex().row(), DbStructureModel::ColumnObjectType)).toString();
1333         if(type == "table" || type == "view")
1334         {
1335             QString schema = ui->dbTreeWidget->model()->data(ui->dbTreeWidget->currentIndex().sibling(ui->dbTreeWidget->currentIndex().row(), DbStructureModel::ColumnSchema)).toString();
1336             QString name = ui->dbTreeWidget->model()->data(ui->dbTreeWidget->currentIndex().sibling(ui->dbTreeWidget->currentIndex().row(), DbStructureModel::ColumnName)).toString();
1337             current_table = sqlb::ObjectIdentifier(schema.toStdString(), name.toStdString());
1338         }
1339     } else if(ui->mainTab->currentWidget() == ui->browser) {
1340         current_table = ui->tableBrowser->currentlyBrowsedTableName();
1341     }
1342 
1343     // Open dialog
1344     ExportDataDialog dialog(db, ExportDataDialog::ExportFormatCsv, this, "", current_table);
1345     dialog.exec();
1346 }
1347 
exportTableToJson()1348 void MainWindow::exportTableToJson()
1349 {
1350     // Get the current table name if we are in the Browse Data tab
1351     sqlb::ObjectIdentifier current_table;
1352     if(ui->mainTab->currentWidget() == ui->structure)
1353     {
1354         QString type = ui->dbTreeWidget->model()->data(ui->dbTreeWidget->currentIndex().sibling(ui->dbTreeWidget->currentIndex().row(), DbStructureModel::ColumnObjectType)).toString();
1355         if(type == "table" || type == "view")
1356         {
1357             QString schema = ui->dbTreeWidget->model()->data(ui->dbTreeWidget->currentIndex().sibling(ui->dbTreeWidget->currentIndex().row(), DbStructureModel::ColumnSchema)).toString();
1358             QString name = ui->dbTreeWidget->model()->data(ui->dbTreeWidget->currentIndex().sibling(ui->dbTreeWidget->currentIndex().row(), DbStructureModel::ColumnName)).toString();
1359             current_table = sqlb::ObjectIdentifier(schema.toStdString(), name.toStdString());
1360         }
1361     } else if(ui->mainTab->currentWidget() == ui->browser) {
1362         current_table = ui->tableBrowser->currentlyBrowsedTableName();
1363     }
1364 
1365     // Open dialog
1366     ExportDataDialog dialog(db, ExportDataDialog::ExportFormatJson, this, "", current_table);
1367     dialog.exec();
1368 }
1369 
dbState(bool dirty)1370 void MainWindow::dbState(bool dirty)
1371 {
1372     ui->fileSaveAction->setEnabled(dirty);
1373     ui->fileRevertAction->setEnabled(dirty);
1374     ui->fileAttachAction->setEnabled(db.isOpen() && !dirty);
1375 }
1376 
fileSave()1377 void MainWindow::fileSave()
1378 {
1379     if(db.isOpen())
1380     {
1381         if(!db.releaseAllSavepoints())
1382         {
1383             QMessageBox::warning(this, QApplication::applicationName(), tr("Error while saving the database file. This means that not all changes to the database were "
1384                                                                            "saved. You need to resolve the following error first.\n\n%1").arg(db.lastError()));
1385         }
1386     }
1387 }
1388 
fileRevert()1389 void MainWindow::fileRevert()
1390 {
1391     if (db.isOpen()){
1392         QString msg = tr("Are you sure you want to undo all changes made to the database file '%1' since the last save?").arg(db.currentFile());
1393         if(QMessageBox::question(this, QApplication::applicationName(), msg, QMessageBox::Yes | QMessageBox::Default, QMessageBox::No | QMessageBox::Escape) == QMessageBox::Yes)
1394         {
1395             db.revertAll();
1396             populateTable();
1397         }
1398     }
1399 }
1400 
exportDatabaseToSQL()1401 void MainWindow::exportDatabaseToSQL()
1402 {
1403     QString current_table;
1404     if(ui->mainTab->currentWidget() == ui->browser)
1405         current_table = QString::fromStdString(ui->tableBrowser->currentlyBrowsedTableName().name());
1406 
1407     ExportSqlDialog dialog(&db, this, current_table);
1408     dialog.exec();
1409 }
1410 
importDatabaseFromSQL()1411 void MainWindow::importDatabaseFromSQL()
1412 {
1413     QStringList file_filter;
1414     file_filter << FILE_FILTER_SQL
1415                 << FILE_FILTER_TXT
1416                 << FILE_FILTER_ALL;
1417 
1418     // Get file name to import
1419     QString fileName = FileDialog::getOpenFileName(
1420                 OpenSQLFile,
1421                 this,
1422                 tr("Choose a file to import"),
1423                 file_filter.join(";;"));
1424 
1425     // Cancel when file doesn't exist
1426     if(!QFile::exists(fileName))
1427         return;
1428 
1429     // If there is already a database file opened ask the user whether to import into
1430     // this one or a new one. If no DB is opened just ask for a DB name directly
1431     QString newDbFile;
1432     if((db.isOpen() && QMessageBox::question(this,
1433                                             QApplication::applicationName(),
1434                                             tr("Do you want to create a new database file to hold the imported data?\n"
1435                                                "If you answer no we will attempt to import the data in the SQL file to the current database."),
1436                                             QMessageBox::Yes, QMessageBox::No) == QMessageBox::Yes) || !db.isOpen())
1437     {
1438         newDbFile = FileDialog::getSaveFileName(
1439                     CreateDatabaseFile,
1440                     this,
1441                     tr("Choose a filename to save under"),
1442                     FileDialog::getSqlDatabaseFileFilter());
1443         if(QFile::exists(newDbFile))
1444         {
1445             QMessageBox::information(this, QApplication::applicationName(), tr("File %1 already exists. Please choose a different name.").arg(newDbFile));
1446             return;
1447         } else if(newDbFile.size() == 0) {
1448             return;
1449         }
1450 
1451         // Create the new file and open it in the browser
1452         db.create(newDbFile);
1453         db.close();
1454         fileOpen(newDbFile);
1455     }
1456 
1457     // Defer foreign keys. Just deferring them instead of disabling them should work fine because in the import we only expect CREATE and INSERT
1458     // statements which unlike in the Edit Table dialog shouldn't trigger any problems.
1459     QString foreignKeysOldSettings = db.getPragma("defer_foreign_keys");
1460     db.setPragma("defer_foreign_keys", "1");
1461 
1462     // Open, read, execute and close file
1463     QApplication::setOverrideCursor(Qt::WaitCursor);
1464     QFile f(fileName);
1465     f.open(QIODevice::ReadOnly);
1466     QByteArray data = f.readAll();
1467     removeBom(data);
1468     bool ok = db.executeMultiSQL(data, newDbFile.size() == 0);
1469     // Restore cursor before asking the user to accept the message
1470     QApplication::restoreOverrideCursor();
1471     if(!ok)
1472         QMessageBox::warning(this, QApplication::applicationName(), tr("Error importing data: %1").arg(db.lastError()));
1473     else if(db.getPragma("foreign_keys") == "1" && !db.querySingleValueFromDb("PRAGMA foreign_key_check").isNull())
1474         QMessageBox::warning(this, QApplication::applicationName(), tr("Import completed. Some foreign key constraints are violated. Please fix them before saving."));
1475     else
1476         QMessageBox::information(this, QApplication::applicationName(), tr("Import completed."));
1477     f.close();
1478 
1479     // Restore the former foreign key settings
1480     db.setPragma("defer_foreign_keys", foreignKeysOldSettings);
1481 
1482     // Refresh views
1483     db.updateSchema();
1484     populateTable();
1485 }
1486 
openPreferences()1487 void MainWindow::openPreferences()
1488 {
1489     PreferencesDialog dialog(this);
1490     if(dialog.exec())
1491         reloadSettings();
1492 }
1493 
1494 //** Db Tree Context Menu
createTreeContextMenu(const QPoint & qPoint)1495 void MainWindow::createTreeContextMenu(const QPoint &qPoint)
1496 {
1497     if(!ui->dbTreeWidget->selectionModel()->hasSelection())
1498         return;
1499 
1500     QString type = ui->dbTreeWidget->model()->data(ui->dbTreeWidget->currentIndex().sibling(ui->dbTreeWidget->currentIndex().row(), 1)).toString();
1501 
1502     if(type == "table" || type == "view" || type == "trigger" || type == "index")
1503         popupTableMenu->exec(ui->dbTreeWidget->mapToGlobal(qPoint));
1504 }
1505 
1506 //** DB Schema Dock Context Menu
createSchemaDockContextMenu(const QPoint & qPoint)1507 void MainWindow::createSchemaDockContextMenu(const QPoint &qPoint)
1508 {
1509     bool enable_browse_table = false;
1510     if(ui->treeSchemaDock->selectionModel()->hasSelection())
1511     {
1512         QString type = ui->treeSchemaDock->model()->data(ui->treeSchemaDock->currentIndex().sibling(ui->treeSchemaDock->currentIndex().row(), DbStructureModel::ColumnObjectType), Qt::EditRole).toString();
1513         if(type == "table" || type == "view")
1514             enable_browse_table = true;
1515     }
1516     ui->actionPopupSchemaDockBrowseTable->setEnabled(enable_browse_table);
1517 
1518     popupSchemaDockMenu->exec(ui->treeSchemaDock->mapToGlobal(qPoint));
1519 }
1520 
changeTreeSelection()1521 void MainWindow::changeTreeSelection()
1522 {
1523     // Just assume first that something's selected that can not be edited at all
1524     ui->editDeleteObjectAction->setEnabled(false);
1525     ui->editModifyObjectAction->setEnabled(false);
1526     ui->actionEditBrowseTable->setEnabled(false);
1527 
1528     if(!ui->dbTreeWidget->currentIndex().isValid())
1529         return;
1530 
1531     // Change the text and tooltips of the actions
1532     QString type = ui->dbTreeWidget->model()->data(ui->dbTreeWidget->currentIndex().sibling(ui->dbTreeWidget->currentIndex().row(), 1)).toString();
1533 
1534     if (type.isEmpty())
1535     {
1536         ui->editDeleteObjectAction->setIcon(QIcon(":icons/table_delete"));
1537         ui->editModifyObjectAction->setIcon(QIcon(":icons/table_modify"));
1538     } else {
1539         ui->editDeleteObjectAction->setIcon(QIcon(QString(":icons/%1_delete").arg(type)));
1540         ui->editModifyObjectAction->setIcon(QIcon(QString(":icons/%1_modify").arg(type)));
1541     }
1542 
1543     if (type == "view") {
1544         ui->editDeleteObjectAction->setText(tr("Delete View"));
1545         ui->editModifyObjectAction->setText(tr("Modify View"));
1546     } else if(type == "trigger") {
1547         ui->editDeleteObjectAction->setText(tr("Delete Trigger"));
1548         ui->editModifyObjectAction->setText(tr("Modify Trigger"));
1549     } else if(type == "index") {
1550         ui->editDeleteObjectAction->setText(tr("Delete Index"));
1551         ui->editModifyObjectAction->setText(tr("Modify Index"));
1552     } else if(type == "table") {
1553         ui->editDeleteObjectAction->setText(tr("Delete Table"));
1554         ui->editModifyObjectAction->setText(tr("Modify Table"));
1555     } else {
1556         // Nothing to do for other types. Set the buttons not visible and return.
1557         ui->editDeleteObjectAction->setVisible(false);
1558         ui->editModifyObjectAction->setVisible(false);
1559         return;
1560     }
1561 
1562     ui->editDeleteObjectAction->setVisible(true);
1563     ui->editModifyObjectAction->setVisible(true);
1564 
1565     // Activate actions
1566     ui->editDeleteObjectAction->setEnabled(!db.readOnly());
1567     ui->editModifyObjectAction->setEnabled(!db.readOnly());
1568 
1569     if(type == "table" || type == "view")
1570     {
1571         ui->actionEditBrowseTable->setEnabled(true);
1572         ui->actionExportCsvPopup->setEnabled(true);
1573     }
1574 }
1575 
openRecentFile()1576 void MainWindow::openRecentFile()
1577 {
1578     QAction *action = qobject_cast<QAction *>(sender());
1579     if (action)
1580     {
1581         QString file = action->data().toString();
1582         bool read_only = false;
1583         if(file.startsWith("[ro]"))     // Check if file is in read-only
1584         {
1585             file = file.mid(4);
1586             read_only = true;
1587         }
1588 
1589         if(fileOpen(file, false, read_only))
1590             if(read_only)
1591                 ui->statusbar->showMessage(tr("Opened '%1' in read-only mode from recent file list").arg(file));
1592             else
1593                 ui->statusbar->showMessage(tr("Opened '%1' from recent file list").arg(file));
1594     }
1595 }
1596 
updateRecentFileActions()1597 void MainWindow::updateRecentFileActions()
1598 {
1599     // Get recent files list from settings
1600     QStringList files = Settings::getValue("General", "recentFileList").toStringList();
1601 
1602     // Check if files still exist and remove any non-existent file
1603     for(int i=0;i<files.size();i++)
1604     {
1605         // Remove preceding "[ro]" mark for check
1606         QString file = files.at(i);
1607         if(file.startsWith("[ro]"))
1608             file = file.mid(4);
1609 
1610         if(!QFileInfo::exists(file))
1611         {
1612             files.removeAt(i);
1613             i--;
1614         }
1615     }
1616 
1617     // Store updated list
1618     Settings::setValue("General", "recentFileList", files);
1619 
1620     int numRecentFiles = qMin(files.size(), MaxRecentFiles);
1621 
1622     for (int i = 0; i < numRecentFiles; ++i) {
1623         QString file = files[i];
1624         bool read_only = false;
1625         if(file.startsWith("[ro]"))     // Check if file is in read-only
1626         {
1627             file = file.mid(4);
1628             read_only = true;
1629         }
1630 
1631         QString text = tr("&%1 %2%3").arg(i + 1).arg(QDir::toNativeSeparators(file), read_only ? tr(" (read only)") : "");
1632         recentFileActs[i]->setText(text);
1633         recentFileActs[i]->setData(files[i]);
1634         recentFileActs[i]->setVisible(true);
1635 
1636         // Add shortcut for opening the file using the keyboard. However, if the application is configured to store
1637         // more than nine recently opened files don't set shortcuts for the later ones which wouldn't be single digit anymore.
1638         if(i < 9)
1639             recentFileActs[i]->setShortcut(QKeySequence(static_cast<int>(Qt::CTRL + (Qt::Key_1+static_cast<unsigned int>(i)))));
1640     }
1641     for (int j = numRecentFiles; j < MaxRecentFiles; ++j)
1642         recentFileActs[j]->setVisible(false);
1643 
1644     recentSeparatorAct->setVisible(numRecentFiles > 0);
1645 }
1646 
setCurrentFile(const QString & fileName)1647 void MainWindow::setCurrentFile(const QString &fileName)
1648 {
1649     setWindowFilePath(fileName);
1650     if(currentProjectFilename.isEmpty() && fileName.isEmpty())
1651         setWindowTitle(QApplication::applicationName());
1652     else if(currentProjectFilename.isEmpty())
1653         setWindowTitle(QApplication::applicationName() + " - " + QDir::toNativeSeparators(fileName));
1654     else {
1655         QFileInfo projectFileInfo(currentProjectFilename);
1656         QFileInfo dbFileInfo(fileName);
1657         QString dbFileName;
1658         if(dbFileInfo.path() == projectFileInfo.path())
1659             dbFileName = dbFileInfo.fileName();
1660         else
1661             dbFileName = QDir::toNativeSeparators(fileName);
1662         setWindowTitle(QApplication::applicationName() + " - " + QDir::toNativeSeparators(currentProjectFilename) + " [" + dbFileName + "]");
1663     }
1664     activateFields(!fileName.isEmpty());
1665     if(!fileName.isEmpty())
1666         dbState(db.getDirty());
1667 }
1668 
addToRecentFilesMenu(const QString & filename,bool read_only)1669 void MainWindow::addToRecentFilesMenu(const QString& filename, bool read_only)
1670 {
1671     QFileInfo info(filename);
1672     QString path = info.absoluteFilePath();
1673     if(read_only)
1674         path = "[ro]" + path;
1675 
1676     QStringList files = Settings::getValue("General", "recentFileList").toStringList();
1677 
1678     files.removeAll(path);
1679     files.prepend(path);
1680     while (files.size() > MaxRecentFiles)
1681         files.removeLast();
1682 
1683     Settings::setValue("General", "recentFileList", files);
1684 
1685     for(QWidget* widget : QApplication::topLevelWidgets()) {
1686         MainWindow *mainWin = qobject_cast<MainWindow *>(widget);
1687         if (mainWin)
1688             mainWin->updateRecentFileActions();
1689     }
1690 }
1691 
dragEnterEvent(QDragEnterEvent * event)1692 void MainWindow::dragEnterEvent(QDragEnterEvent *event)
1693 {
1694     if( event->mimeData()->hasFormat("text/uri-list") )
1695         event->acceptProposedAction();
1696 }
1697 
dropEvent(QDropEvent * event)1698 void MainWindow::dropEvent(QDropEvent *event)
1699 {
1700     QList<QUrl> urls = event->mimeData()->urls();
1701 
1702     if( urls.isEmpty() )
1703         return;
1704 
1705     QString fileName = urls.first().toLocalFile();
1706 
1707     if(!fileName.isEmpty()) {
1708 
1709         // If there is no open database, the only possible option is to open the file.
1710         if (!db.isOpen()) {
1711             fileOpen(fileName);
1712             return;
1713         }
1714         bool ok;
1715         const QString open = tr("Open Database or Project");
1716         const QString attach = tr("Attach Database...");
1717         const QString import = tr("Import CSV file(s)...");
1718         QString action = QInputDialog::getItem(this,
1719                                    qApp->applicationName(),
1720                                    tr("Select the action to apply to the dropped file(s). <br/>"
1721                                       "Note: only 'Import' will process more than one file.", "", urls.count()),
1722                                    {open, attach, import},
1723                                    0,
1724                                    false,
1725                                    &ok);
1726         if(ok) {
1727             if (action == open) {
1728                 fileOpen(fileName);
1729             } else if (action == attach) {
1730                 fileAttach(fileName);
1731             } else if (action == import) {
1732 
1733                 std::vector<QString> validFiles;
1734                 for(const auto& url : urls) {
1735                     if (QFile::exists(url.toLocalFile()))
1736                         validFiles.push_back(url.toLocalFile());
1737                 }
1738                 ImportCsvDialog dialog(validFiles, &db, this);
1739                 if (dialog.exec())
1740                     populateTable();
1741             }
1742         }
1743     }
1744 }
1745 
activateFields(bool enable)1746 void MainWindow::activateFields(bool enable)
1747 {
1748     bool write = !db.readOnly();
1749     bool tempDb = db.currentFile() == ":memory:";
1750 
1751     ui->tableBrowser->setEnabled(enable);
1752     ui->fileCloseAction->setEnabled(enable);
1753     ui->fileAttachAction->setEnabled(enable);
1754     ui->fileCompactAction->setEnabled(enable && write);
1755     ui->fileExportJsonAction->setEnabled(enable);
1756     ui->fileExportCSVAction->setEnabled(enable);
1757     ui->fileExportSQLAction->setEnabled(enable);
1758     ui->fileImportCSVAction->setEnabled(enable && write);
1759     ui->editCreateTableAction->setEnabled(enable && write);
1760     ui->editCreateIndexAction->setEnabled(enable && write);
1761     ui->actionDbPrint->setEnabled(enable);
1762     ui->scrollAreaWidgetContents->setEnabled(enable);
1763     ui->buttonBoxPragmas->setEnabled(enable && write);
1764     ui->actionExecuteSql->setEnabled(enable);
1765     ui->actionLoadExtension->setEnabled(enable);
1766     ui->actionSqlExecuteLine->setEnabled(enable);
1767     ui->actionSaveProject->setEnabled(enable && !tempDb);
1768     ui->actionSaveProjectAs->setEnabled(enable && !tempDb);
1769     ui->actionSaveAll->setEnabled(enable && !tempDb);
1770     ui->actionEncryption->setEnabled(enable && write && !tempDb);
1771     ui->actionIntegrityCheck->setEnabled(enable);
1772     ui->actionQuickCheck->setEnabled(enable);
1773     ui->actionForeignKeyCheck->setEnabled(enable);
1774     ui->actionOptimize->setEnabled(enable);
1775     ui->dockEdit->setEnabled(enable);
1776     ui->dockPlot->setEnabled(enable);
1777 
1778     if(!enable)
1779         ui->actionSqlResultsSave->setEnabled(false);
1780 
1781     remoteDock->enableButtons();
1782 }
1783 
resizeEvent(QResizeEvent *)1784 void MainWindow::resizeEvent(QResizeEvent*)
1785 {
1786     ui->tableBrowser->updateRecordsetLabel();
1787 }
1788 
loadPragmas()1789 void MainWindow::loadPragmas()
1790 {
1791     pragmaValues.autovacuum = db.getPragma("auto_vacuum").toInt();
1792     pragmaValues.automatic_index = db.getPragma("automatic_index").toInt();
1793     pragmaValues.checkpoint_fullsync = db.getPragma("checkpoint_fullfsync").toInt();
1794     pragmaValues.foreign_keys = db.getPragma("foreign_keys").toInt();
1795     pragmaValues.fullfsync = db.getPragma("fullfsync").toInt();
1796     pragmaValues.ignore_check_constraints = db.getPragma("ignore_check_constraints").toInt();
1797     pragmaValues.journal_mode = db.getPragma("journal_mode").toUpper();
1798     pragmaValues.journal_size_limit = db.getPragma("journal_size_limit").toInt();
1799     pragmaValues.locking_mode = db.getPragma("locking_mode").toUpper();
1800     pragmaValues.max_page_count = db.getPragma("max_page_count").toInt();
1801     pragmaValues.page_size = db.getPragma("page_size").toInt();
1802     pragmaValues.recursive_triggers = db.getPragma("recursive_triggers").toInt();
1803     pragmaValues.secure_delete = db.getPragma("secure_delete").toInt();
1804     pragmaValues.synchronous = db.getPragma("synchronous").toInt();
1805     pragmaValues.temp_store = db.getPragma("temp_store").toInt();
1806     pragmaValues.user_version = db.getPragma("user_version").toInt();
1807     pragmaValues.wal_autocheckpoint = db.getPragma("wal_autocheckpoint").toInt();
1808     pragmaValues.case_sensitive_like = db.getPragma("case_sensitive_like").toInt();
1809 
1810     updatePragmaUi();
1811 }
1812 
updatePragmaUi()1813 void MainWindow::updatePragmaUi()
1814 {
1815     ui->comboboxPragmaAutoVacuum->setCurrentIndex(pragmaValues.autovacuum);
1816     ui->checkboxPragmaAutomaticIndex->setChecked(pragmaValues.automatic_index);
1817     ui->checkboxPragmaCheckpointFullFsync->setChecked(pragmaValues.checkpoint_fullsync);
1818     ui->checkboxPragmaForeignKeys->setChecked(pragmaValues.foreign_keys);
1819     ui->checkboxPragmaFullFsync->setChecked(pragmaValues.fullfsync);
1820     ui->checkboxPragmaIgnoreCheckConstraints->setChecked(pragmaValues.ignore_check_constraints);
1821     ui->comboboxPragmaJournalMode->setCurrentIndex(ui->comboboxPragmaJournalMode->findText(pragmaValues.journal_mode, Qt::MatchFixedString));
1822     ui->spinPragmaJournalSizeLimit->setValue(pragmaValues.journal_size_limit);
1823     ui->comboboxPragmaLockingMode->setCurrentIndex(ui->comboboxPragmaLockingMode->findText(pragmaValues.locking_mode, Qt::MatchFixedString));
1824     ui->spinPragmaMaxPageCount->setValue(pragmaValues.max_page_count);
1825     ui->comboPragmaPageSize->setCurrentIndex(ui->comboPragmaPageSize->findText(QString::number(pragmaValues.page_size), Qt::MatchFixedString));
1826     ui->checkboxPragmaRecursiveTriggers->setChecked(pragmaValues.recursive_triggers);
1827     ui->checkboxPragmaSecureDelete->setChecked(pragmaValues.secure_delete);
1828     ui->comboboxPragmaSynchronous->setCurrentIndex(pragmaValues.synchronous);
1829     ui->comboboxPragmaTempStore->setCurrentIndex(pragmaValues.temp_store);
1830     ui->spinPragmaUserVersion->setValue(pragmaValues.user_version);
1831     ui->spinPragmaWalAutoCheckpoint->setValue(pragmaValues.wal_autocheckpoint);
1832     ui->checkboxPragmaCaseSensitiveLike->setChecked(pragmaValues.case_sensitive_like);
1833 }
1834 
savePragmas()1835 void MainWindow::savePragmas()
1836 {
1837     if( db.getDirty() )
1838     {
1839         QString msg = tr("Setting PRAGMA values will commit your current transaction.\nAre you sure?");
1840         if(QMessageBox::question(this, QApplication::applicationName(), msg, QMessageBox::Yes | QMessageBox::Default, QMessageBox::No | QMessageBox::Escape) == QMessageBox::No)
1841         {
1842             return; // abort
1843         }
1844     }
1845     db.setPragma("auto_vacuum", ui->comboboxPragmaAutoVacuum->currentIndex(), pragmaValues.autovacuum);
1846     db.setPragma("automatic_index", ui->checkboxPragmaAutomaticIndex->isChecked(), pragmaValues.automatic_index);
1847     db.setPragma("checkpoint_fullfsync", ui->checkboxPragmaCheckpointFullFsync->isChecked(), pragmaValues.checkpoint_fullsync);
1848     db.setPragma("foreign_keys", ui->checkboxPragmaForeignKeys->isChecked(), pragmaValues.foreign_keys);
1849     db.setPragma("fullfsync", ui->checkboxPragmaFullFsync->isChecked(), pragmaValues.fullfsync);
1850     db.setPragma("ignore_check_constraints", ui->checkboxPragmaIgnoreCheckConstraints->isChecked(), pragmaValues.ignore_check_constraints);
1851     db.setPragma("journal_mode", ui->comboboxPragmaJournalMode->currentText().toUpper(), pragmaValues.journal_mode);
1852     db.setPragma("journal_size_limit", ui->spinPragmaJournalSizeLimit->value(), pragmaValues.journal_size_limit);
1853     db.setPragma("locking_mode", ui->comboboxPragmaLockingMode->currentText().toUpper(), pragmaValues.locking_mode);
1854     db.setPragma("max_page_count", ui->spinPragmaMaxPageCount->value(), pragmaValues.max_page_count);
1855     db.setPragma("page_size", ui->comboPragmaPageSize->currentText().toInt(), pragmaValues.page_size);
1856     db.setPragma("recursive_triggers", ui->checkboxPragmaRecursiveTriggers->isChecked(), pragmaValues.recursive_triggers);
1857     db.setPragma("secure_delete", ui->checkboxPragmaSecureDelete->isChecked(), pragmaValues.secure_delete);
1858     db.setPragma("synchronous", ui->comboboxPragmaSynchronous->currentIndex(), pragmaValues.synchronous);
1859     db.setPragma("temp_store", ui->comboboxPragmaTempStore->currentIndex(), pragmaValues.temp_store);
1860     db.setPragma("user_version", ui->spinPragmaUserVersion->value(), pragmaValues.user_version);
1861     db.setPragma("wal_autocheckpoint", ui->spinPragmaWalAutoCheckpoint->value(), pragmaValues.wal_autocheckpoint);
1862     db.setPragma("case_sensitive_like", ui->checkboxPragmaCaseSensitiveLike->isChecked(), pragmaValues.case_sensitive_like);
1863     isProjectModified = true;
1864 
1865     updatePragmaUi();
1866 }
1867 
logSql(const QString & sql,int msgtype)1868 void MainWindow::logSql(const QString& sql, int msgtype)
1869 {
1870     if(msgtype == kLogMsg_User)
1871     {
1872         ui->editLogUser->append(sql + "\n");
1873         ui->editLogUser->verticalScrollBar()->setValue(ui->editLogUser->verticalScrollBar()->maximum());
1874     } else if(msgtype == kLogMsg_App) {
1875         ui->editLogApplication->append(sql + "\n");
1876         ui->editLogApplication->verticalScrollBar()->setValue(ui->editLogApplication->verticalScrollBar()->maximum());
1877     } else if(msgtype == kLogMsg_ErrorLog) {
1878         ui->editLogErrorLog->append(sql + "\n");
1879         ui->editLogErrorLog->verticalScrollBar()->setValue(ui->editLogErrorLog->verticalScrollBar()->maximum());
1880     }
1881 }
1882 
1883 // Ask user to save the buffer in the specified tab index.
1884 // ignoreUnattachedBuffers is used to store answer about buffers not linked to files, so user is only asked once about them.
1885 // Return true unless user wants to cancel the invoking action.
askSaveSqlTab(int index,bool & ignoreUnattachedBuffers)1886 bool MainWindow::askSaveSqlTab(int index, bool& ignoreUnattachedBuffers)
1887 {
1888     SqlExecutionArea* sqlExecArea = qobject_cast<SqlExecutionArea*>(ui->tabSqlAreas->widget(index));
1889 
1890     if(sqlExecArea->getEditor()->isModified()) {
1891         if(sqlExecArea->fileName().isEmpty() && !ignoreUnattachedBuffers) {
1892             // Once the project is saved, remaining SQL tabs will not be modified, so this is only expected to be asked once.
1893             QString message = currentProjectFilename.isEmpty() ?
1894                 tr("Do you want to save the changes made to SQL tabs in a new project file?") :
1895                 tr("Do you want to save the changes made to SQL tabs in the project file '%1'?").
1896                 arg(QFileInfo(currentProjectFilename).fileName());
1897             QMessageBox::StandardButton reply = QMessageBox::question(nullptr,
1898                                                                       QApplication::applicationName(),
1899                                                                       message,
1900                                                                       QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel);
1901             switch(reply) {
1902             case QMessageBox::Save:
1903                 saveProject();
1904                 break;
1905             case QMessageBox::Cancel:
1906                 return false;
1907             default:
1908                 ignoreUnattachedBuffers = true;
1909                 break;
1910             }
1911         } else if(!sqlExecArea->fileName().isEmpty()) {
1912             QMessageBox::StandardButton reply =
1913                 QMessageBox::question(nullptr,
1914                                       QApplication::applicationName(),
1915                                       tr("Do you want to save the changes made to the SQL file %1?").
1916                                       arg(QFileInfo(sqlExecArea->fileName()).fileName()),
1917                                       QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel);
1918             switch(reply) {
1919             case QMessageBox::Save:
1920                 saveSqlFile(index);
1921                 break;
1922             case QMessageBox::Cancel:
1923                 return false;
1924             default:
1925                 break;
1926             }
1927         }
1928     }
1929     return true;
1930 }
1931 
closeSqlTab(int index,bool force)1932 void MainWindow::closeSqlTab(int index, bool force)
1933 {
1934     // Check if we're still executing statements from this tab and stop them before proceeding
1935     if(ui->tabSqlAreas->tabBar()->tabData(index).toBool())
1936     {
1937         if(QMessageBox::warning(this, qApp->applicationName(), tr("The statements in this tab are still executing. Closing the tab will stop the "
1938                                                                   "execution. This might leave the database in an inconsistent state. Are you sure "
1939                                                                   "you want to close the tab?"),
1940                                 QMessageBox::Yes,
1941                                 QMessageBox::Cancel | QMessageBox::Default | QMessageBox::Escape) == QMessageBox::Cancel)
1942             return;
1943 
1944         execute_sql_worker->stop();
1945         execute_sql_worker->wait();
1946     }
1947     // Ask for saving and comply with cancel answer.
1948     bool ignoreUnattachedBuffers = false;
1949     if (!askSaveSqlTab(index, ignoreUnattachedBuffers))
1950         return;
1951     // Remove the tab and delete the widget
1952     QWidget* w = ui->tabSqlAreas->widget(index);
1953     ui->tabSqlAreas->removeTab(index);
1954     delete w;
1955 
1956     // Don't let an empty tab widget
1957     if(ui->tabSqlAreas->count() == 0 && !force)
1958         openSqlTab(true);
1959 
1960     // Set focus to the currently selected editor tab.
1961     SqlExecutionArea* sqlarea = qobject_cast<SqlExecutionArea*>(ui->tabSqlAreas->currentWidget());
1962     if(sqlarea)
1963         sqlarea->getEditor()->setFocus();
1964 }
1965 
openSqlTab(bool resetCounter)1966 int MainWindow::openSqlTab(bool resetCounter)
1967 {
1968     static int tabNumber = 0;
1969 
1970     if(resetCounter)
1971         tabNumber = 0;
1972 
1973     // Create new tab, add it to the tab widget and select it
1974     SqlExecutionArea* w = new SqlExecutionArea(db, this);
1975     int index = ui->tabSqlAreas->addTab(w, QString("SQL %1").arg(++tabNumber));
1976     ui->tabSqlAreas->setCurrentIndex(index);
1977     w->setFindFrameVisibility(ui->actionSqlFind->isChecked());
1978     // Disable the find dialog in the SQL tabs, since the shortcut
1979     // would interfere with the search bar and it'd be anyway redundant.
1980     w->getEditor()->setEnabledFindDialog(false);
1981     w->getEditor()->setFocus();
1982     connect(w, &SqlExecutionArea::findFrameVisibilityChanged, ui->actionSqlFind, &QAction::setChecked);
1983 
1984     // Connect now the find shortcut to the editor with widget context, so it isn't ambiguous with other Scintilla Widgets.
1985     QShortcut* shortcutFind = new QShortcut(ui->actionSqlFind->shortcut(), w->getEditor(), nullptr, nullptr, Qt::WidgetShortcut);
1986     connect(shortcutFind, &QShortcut::activated, ui->actionSqlFind, &QAction::toggle);
1987     ui->tabSqlAreas->setTabIcon(index, QIcon(":icons/open_sql"));
1988     // The new tab is not currently running a query
1989     ui->tabSqlAreas->tabBar()->setTabData(index, false);
1990 
1991     return index;
1992 }
1993 
changeSqlTab(int index)1994 void MainWindow::changeSqlTab(int index)
1995 {
1996     // Instead of figuring out if there are some execution results in the new tab and which statement was used to generate them,
1997     // we just disable the export buttons in the toolbar.
1998     ui->actionSqlResultsSave->setEnabled(false);
1999 
2000     // Check if the new tab is currently running a query or not
2001     if(!ui->tabSqlAreas->tabBar()->tabData(index).toBool())
2002     {
2003         // Not running a query
2004 
2005         ui->actionSqlExecuteLine->setEnabled(db.isOpen());
2006         ui->actionExecuteSql->setEnabled(db.isOpen());
2007         ui->actionSqlStop->setEnabled(false);
2008     } else {
2009         // Running a query
2010 
2011         ui->actionSqlExecuteLine->setEnabled(false);
2012         ui->actionExecuteSql->setEnabled(false);
2013         ui->actionSqlStop->setEnabled(true);
2014     }
2015 }
2016 
openSqlFile()2017 void MainWindow::openSqlFile()
2018 {
2019     QStringList wfiles = FileDialog::getOpenFileNames(
2020                 OpenSQLFile,
2021                 this,
2022                 tr("Select SQL file to open"),
2023                 tr("Text files(*.sql *.txt);;All files(*)"));
2024 
2025     for(QString file: wfiles)
2026     {
2027         if(QFile::exists(file))
2028         {
2029             // Decide whether to open a new tab or take the current one
2030             int index;
2031             SqlExecutionArea* current_tab = qobject_cast<SqlExecutionArea*>(ui->tabSqlAreas->currentWidget());
2032             if(current_tab && current_tab->getSql().isEmpty() && current_tab->getModel()->rowCount() == 0)
2033                 index = ui->tabSqlAreas->currentIndex();
2034             else
2035                 index = openSqlTab();
2036 
2037             SqlExecutionArea* sqlarea = qobject_cast<SqlExecutionArea*>(ui->tabSqlAreas->widget(index));
2038             sqlarea->openFile(file);
2039 
2040             QFileInfo fileinfo(file);
2041             ui->tabSqlAreas->setTabText(index, fileinfo.fileName());
2042             ui->tabSqlAreas->setTabIcon(index, QIcon(":/icons/document_open"));
2043         }
2044     }
2045 }
2046 
saveSqlFile(int tabIndex)2047 void MainWindow::saveSqlFile(int tabIndex)
2048 {
2049     SqlExecutionArea* sqlarea = qobject_cast<SqlExecutionArea*>(ui->tabSqlAreas->widget(tabIndex));
2050     if(!sqlarea)
2051         return;
2052 
2053     // If this SQL file hasn't been saved before open the Save As dialog. Otherwise just use the old file name for saving
2054     if(sqlarea->fileName().isEmpty())
2055     {
2056         saveSqlFileAs();
2057     } else {
2058         sqlarea->saveFile(sqlarea->fileName());
2059     }
2060 }
2061 
saveSqlFile()2062 void MainWindow::saveSqlFile()
2063 {
2064     saveSqlFile(ui->tabSqlAreas->currentIndex());
2065 }
2066 
saveSqlFileAs()2067 void MainWindow::saveSqlFileAs()
2068 {
2069     SqlExecutionArea* sqlarea = qobject_cast<SqlExecutionArea*>(ui->tabSqlAreas->currentWidget());
2070     if(!sqlarea)
2071         return;
2072 
2073     QStringList file_filter;
2074     file_filter << FILE_FILTER_SQL
2075                 << FILE_FILTER_TXT
2076                 << FILE_FILTER_ALL;
2077     QString file = FileDialog::getSaveFileName(
2078                 CreateSQLFile,
2079                 this,
2080                 tr("Select file name"),
2081                 file_filter.join(";;"));
2082 
2083     if(!file.isEmpty())
2084     {
2085         sqlarea->saveFile(file);
2086 
2087         QFileInfo fileinfo(file);
2088         ui->tabSqlAreas->setTabText(ui->tabSqlAreas->currentIndex(), fileinfo.fileName());
2089         ui->tabSqlAreas->setTabIcon(ui->tabSqlAreas->currentIndex(), QIcon(":/icons/document_open"));
2090     }
2091 }
2092 
saveSqlResultsAsCsv()2093 void MainWindow::saveSqlResultsAsCsv()
2094 {
2095     qobject_cast<SqlExecutionArea*>(ui->tabSqlAreas->currentWidget())->saveAsCsv();
2096 }
2097 
saveSqlResultsAsView()2098 void MainWindow::saveSqlResultsAsView()
2099 {
2100     saveAsView(qobject_cast<SqlExecutionArea*>(ui->tabSqlAreas->currentWidget())->getModel()->query());
2101 }
2102 
loadExtension()2103 void MainWindow::loadExtension()
2104 {
2105     QStringList file_filter;
2106     file_filter << FILE_FILTER_DYN
2107                 << FILE_FILTER_ALL;
2108 
2109     QString file = FileDialog::getOpenFileName(
2110                 OpenExtensionFile,
2111                 this,
2112                 tr("Select extension file"),
2113                 file_filter.join(";;"));
2114 
2115     if(file.isEmpty())
2116         return;
2117 
2118     if(db.loadExtension(file))
2119         QMessageBox::information(this, QApplication::applicationName(), tr("Extension successfully loaded."));
2120     else
2121         QMessageBox::warning(this, QApplication::applicationName(), tr("Error loading extension: %1").arg(db.lastError()));
2122 }
2123 
reloadSettings()2124 void MainWindow::reloadSettings()
2125 {
2126     // Set default application font size
2127     qobject_cast<Application*>(qApp)->reloadSettings();
2128 
2129     // Set data browser font
2130     ui->tableBrowser->reloadSettings();
2131 
2132     switch (static_cast<Settings::AppStyle>(Settings::getValue("General", "appStyle").toInt())) {
2133     case Settings::FollowDesktopStyle :
2134         qApp->setStyleSheet("");
2135 
2136         break;
2137     case Settings::DarkStyle :
2138         QFile f(":qdarkstyle/style.qss");
2139         if (!f.exists()) {
2140             QMessageBox::warning(this, qApp->applicationName(),
2141                                tr("Could not find resource file: %1").arg(f.fileName()));
2142         } else {
2143             f.open(QFile::ReadOnly | QFile::Text);
2144             QTextStream ts(&f);
2145             qApp->setStyleSheet(ts.readAll());
2146         }
2147         break;
2148     }
2149 
2150     setToolButtonStyle(static_cast<Qt::ToolButtonStyle>(Settings::getValue("General", "toolbarStyle").toInt()));
2151     ui->dbToolbar->setToolButtonStyle(static_cast<Qt::ToolButtonStyle>(Settings::getValue("General", "toolbarStyleStructure").toInt()));
2152     ui->toolbarSql->setToolButtonStyle(static_cast<Qt::ToolButtonStyle>(Settings::getValue("General", "toolbarStyleSql").toInt()));
2153 
2154     // Set prefetch sizes for lazy population of table models
2155     for(int i=0;i<ui->tabSqlAreas->count();++i)
2156         qobject_cast<SqlExecutionArea*>(ui->tabSqlAreas->widget(i))->reloadSettings();
2157 
2158     // Prepare log font
2159     QFont logfont("Monospace");
2160     logfont.setStyleHint(QFont::TypeWriter);
2161     logfont.setPointSize(Settings::getValue("log", "fontsize").toInt());
2162 
2163     // Set font for SQL logs and edit dialog
2164     ui->editLogApplication->reloadSettings();
2165     ui->editLogUser->reloadSettings();
2166     ui->editLogErrorLog->reloadSettings();
2167     ui->editLogApplication->setFont(logfont);
2168     ui->editLogUser->setFont(logfont);
2169     ui->editLogErrorLog->setFont(logfont);
2170     editDock->reloadSettings();
2171 
2172     // Set font for database structure views
2173     QFont structure_font = ui->dbTreeWidget->font();
2174     structure_font.setPointSize(Settings::getValue("db", "fontsize").toInt());
2175     ui->dbTreeWidget->setFont(structure_font);
2176     ui->treeSchemaDock->setFont(structure_font);
2177 
2178     // Load extensions
2179     db.loadExtensionsFromSettings();
2180 
2181     // Refresh view
2182     dbStructureModel->reloadData();
2183     populateStructure();
2184     populateTable();
2185 
2186     // Hide or show the remote dock as needed
2187     bool showRemoteActions = Settings::getValue("remote", "active").toBool();
2188     ui->viewMenu->actions().at(4)->setVisible(showRemoteActions);
2189     if(!showRemoteActions)
2190         ui->dockRemote->setHidden(true);
2191 
2192     // Reload remote dock settings
2193     remoteDock->reloadSettings();
2194 
2195     sqlb::setIdentifierQuoting(static_cast<sqlb::escapeQuoting>(Settings::getValue("editor", "identifier_quotes").toInt()));
2196 
2197     ui->tabSqlAreas->setTabsClosable(Settings::getValue("editor", "close_button_on_tabs").toBool());
2198 }
2199 
checkNewVersion(const QString & versionstring,const QString & url)2200 void MainWindow::checkNewVersion(const QString& versionstring, const QString& url)
2201 {
2202     // versionstring contains a major.minor.patch version string
2203     QStringList versiontokens = versionstring.split(".");
2204     if(versiontokens.size() < 3)
2205         return;
2206 
2207     int major = versiontokens[0].toInt();
2208     int minor = versiontokens[1].toInt();
2209     int patch = versiontokens[2].toInt();
2210 
2211     bool newversion = false;
2212     if(major > MAJOR_VERSION)
2213         newversion = true;
2214     else if(major == MAJOR_VERSION)
2215     {
2216         if(minor > MINOR_VERSION)
2217             newversion = true;
2218         else if(minor == MINOR_VERSION)
2219         {
2220             if(patch > PATCH_VERSION)
2221                 newversion = true;
2222         }
2223     }
2224 
2225     if(newversion)
2226     {
2227         int ignmajor = Settings::getValue("checkversion", "ignmajor").toInt();
2228         int ignminor = Settings::getValue("checkversion", "ignminor").toInt();
2229         int ignpatch = Settings::getValue("checkversion", "ignpatch").toInt();
2230 
2231         // check if the user doesn't care about the current update
2232         if(!(ignmajor == major && ignminor == minor && ignpatch == patch))
2233         {
2234             QMessageBox msgBox;
2235             QPushButton *idontcarebutton = msgBox.addButton(tr("Don't show again"), QMessageBox::ActionRole);
2236             msgBox.addButton(QMessageBox::Ok);
2237             msgBox.setTextFormat(Qt::RichText);
2238             msgBox.setWindowTitle(tr("New version available."));
2239             msgBox.setText(tr("A new DB Browser for SQLite version is available (%1.%2.%3).<br/><br/>"
2240                               "Please download at <a href='%4'>%4</a>.").arg(major).arg(minor).arg(patch).
2241                                 arg(url));
2242             msgBox.exec();
2243 
2244             if(msgBox.clickedButton() == idontcarebutton)
2245             {
2246                 // save that the user don't want to get bothered about this update
2247                 Settings::setValue("checkversion", "ignmajor", major);
2248                 Settings::setValue("checkversion", "ignminor", minor);
2249                 Settings::setValue("checkversion", "ignpatch", patch);
2250             }
2251         }
2252     }
2253 }
2254 
on_actionWiki_triggered() const2255 void MainWindow::on_actionWiki_triggered() const
2256 {
2257     QDesktopServices::openUrl(QUrl("https://github.com/sqlitebrowser/sqlitebrowser/wiki"));
2258 }
2259 
2260 // 'Help | Bug Report...' link will set an appropiate body, add the system information and set the label 'bug' automatically to the issue
on_actionBug_report_triggered() const2261 void MainWindow::on_actionBug_report_triggered() const
2262 {
2263     const QString version = Application::versionString();
2264     const QString os = QSysInfo::prettyProductName();
2265     const QString kernelType = QSysInfo::kernelType();
2266     const QString kernelVersion = QSysInfo::kernelVersion();
2267     const QString arch = QSysInfo::currentCpuArchitecture();
2268     const QString built_for = QSysInfo::buildAbi();
2269 
2270     QString sqlite_version, sqlcipher_version;
2271     DBBrowserDB::getSqliteVersion(sqlite_version, sqlcipher_version);
2272     if(sqlcipher_version.isNull())
2273         sqlite_version = QString("SQLite Version ") + sqlite_version;
2274     else
2275         sqlite_version = QString("SQLCipher Version ") + sqlcipher_version + QString(" (based on SQLite %1)").arg(sqlite_version);
2276 
2277     const QString body =
2278       QString("Details for the issue\n"
2279               "--------------------\n\n"
2280               "#### What did you do?\n\n\n"
2281               "#### What did you expect to see?\n\n\n"
2282               "#### What did you see instead?\n\n\n"
2283               "Useful extra information\n"
2284               "-------------------------\n"
2285               "> DB4S v%1 [built for %2] on %3 (%4/%5) [%6]\n"
2286               "> using %7\n"
2287               "> and Qt %8")
2288             .arg(version, built_for, os, kernelType, kernelVersion, arch, sqlite_version, QT_VERSION_STR);
2289 
2290     QUrlQuery query;
2291     query.addQueryItem("labels", "bug");
2292     query.addQueryItem("body", body);
2293 
2294     QUrl url("https://github.com/sqlitebrowser/sqlitebrowser/issues/new");
2295     url.setQuery(query);
2296     QDesktopServices::openUrl(url);
2297 }
2298 
2299 // 'Help | Feature Request...' link will set an appropiate body and add the label 'enhancement' automatically to the issue
on_actionFeature_Request_triggered() const2300 void MainWindow::on_actionFeature_Request_triggered() const
2301 {
2302     QUrlQuery query;
2303 
2304     // Add the label enhancement and use the Feature request template that
2305     // we have in GitHub.
2306     query.addQueryItem("labels", "enhancement");
2307     query.addQueryItem("template", "Feature_request.md");
2308 
2309     QUrl url("https://github.com/sqlitebrowser/sqlitebrowser/issues/new");
2310     url.setQuery(query);
2311     QDesktopServices::openUrl(url);
2312 }
2313 
on_actionSqlCipherFaq_triggered() const2314 void MainWindow::on_actionSqlCipherFaq_triggered() const
2315 {
2316     QDesktopServices::openUrl(QUrl("https://discuss.zetetic.net/c/sqlcipher/sqlcipher-faq"));
2317 }
2318 
on_actionWebsite_triggered() const2319 void MainWindow::on_actionWebsite_triggered() const
2320 {
2321     QDesktopServices::openUrl(QUrl("https://sqlitebrowser.org"));
2322 }
2323 
on_actionDonatePatreon_triggered() const2324 void MainWindow::on_actionDonatePatreon_triggered() const
2325 {
2326     QDesktopServices::openUrl(QUrl("https://www.patreon.com/bePatron?u=11578749"));
2327 }
2328 
loadCondFormatMap(BrowseDataTableSettings::CondFormatMap & condFormats,QXmlStreamReader & xml,const QString & encoding)2329 static void loadCondFormatMap(BrowseDataTableSettings::CondFormatMap& condFormats, QXmlStreamReader& xml, const QString& encoding)
2330 {
2331     const QStringRef name = xml.name();
2332 
2333     while(xml.readNext() != QXmlStreamReader::EndElement && xml.name() != name) {
2334         if (xml.name() == "column") {
2335             size_t index = xml.attributes().value("index").toUInt();
2336             while(xml.readNext() != QXmlStreamReader::EndElement && xml.name() != "column") {
2337                 if(xml.name() == "format") {
2338                     QFont font;
2339                     if (xml.attributes().hasAttribute("font"))
2340                         font.fromString(xml.attributes().value("font").toString());
2341                     else
2342                         Settings::getValue("databrowser", "font").toString();
2343 
2344                     CondFormat::Alignment align;
2345                     if (xml.attributes().hasAttribute("align"))
2346                         align = static_cast<CondFormat::Alignment>(xml.attributes().value("align").toInt());
2347                     else
2348                         align = CondFormat::AlignLeft;
2349 
2350                     condFormats[index].emplace_back(xml.attributes().value("condition").toString(),
2351                                                     QColor(xml.attributes().value("foreground").toString()),
2352                                                     QColor(xml.attributes().value("background").toString()),
2353                                                     font, align, encoding);
2354                     xml.skipCurrentElement();
2355                 }
2356             }
2357         }
2358     }
2359 }
2360 
loadBrowseDataTableSettings(BrowseDataTableSettings & settings,QXmlStreamReader & xml)2361 static void loadBrowseDataTableSettings(BrowseDataTableSettings& settings, QXmlStreamReader& xml)
2362 {
2363     // TODO Remove this in the near future. This file format was only created temporarily by the nightlies from the late 3.11 development period.
2364     if(xml.attributes().hasAttribute("sort_order_index"))
2365     {
2366         int sortOrderIndex = xml.attributes().value("sort_order_index").toInt();
2367         Qt::SortOrder sortOrderMode = static_cast<Qt::SortOrder>(xml.attributes().value("sort_order_mode").toInt());
2368         settings.query.setOrderBy(toSortOrderVector(sortOrderIndex, sortOrderMode));
2369     }
2370 
2371     settings.showRowid = xml.attributes().value("show_row_id").toInt();
2372     settings.encoding = xml.attributes().value("encoding").toString();
2373     settings.plotXAxis = xml.attributes().value("plot_x_axis").toString();
2374     settings.unlockViewPk = xml.attributes().value("unlock_view_pk").toString();
2375 
2376     while(xml.readNext() != QXmlStreamReader::EndElement && xml.name() != "table") {
2377         if(xml.name() == "sort")
2378         {
2379             while(xml.readNext() != QXmlStreamReader::EndElement && xml.name() != "sort")
2380             {
2381                 if(xml.name() == "column")
2382                 {
2383                     int index = xml.attributes().value("index").toInt();
2384                     int mode = xml.attributes().value("mode").toInt();
2385                     settings.query.orderBy().emplace_back(index, mode == Qt::AscendingOrder ? sqlb::Ascending : sqlb::Descending);
2386                     xml.skipCurrentElement();
2387                 }
2388             }
2389         } else if(xml.name() == "column_widths") {
2390             while(xml.readNext() != QXmlStreamReader::EndElement && xml.name() != "column_widths") {
2391                 if (xml.name() == "column") {
2392                     int index = xml.attributes().value("index").toInt();
2393                     settings.columnWidths[index] = xml.attributes().value("value").toInt();
2394                     xml.skipCurrentElement();
2395                 }
2396             }
2397         } else if(xml.name() == "filter_values") {
2398             while(xml.readNext() != QXmlStreamReader::EndElement && xml.name() != "filter_values") {
2399                 if (xml.name() == "column") {
2400                     int index = xml.attributes().value("index").toInt();
2401                     QString value = xml.attributes().value("value").toString();
2402                     if(!value.isEmpty())
2403                         settings.filterValues[index] = value;
2404 
2405                     xml.skipCurrentElement();
2406                 }
2407             }
2408         } else if(xml.name() == "conditional_formats") {
2409             loadCondFormatMap(settings.condFormats, xml, settings.encoding);
2410         } else if(xml.name() == "row_id_formats") {
2411             loadCondFormatMap(settings.rowIdFormats, xml, settings.encoding);
2412         } else if(xml.name() == "display_formats") {
2413             while(xml.readNext() != QXmlStreamReader::EndElement && xml.name() != "display_formats") {
2414                 if (xml.name() == "column") {
2415                     int index = xml.attributes().value("index").toInt();
2416                     settings.displayFormats[index] = xml.attributes().value("value").toString();
2417                     xml.skipCurrentElement();
2418                 }
2419             }
2420         } else if(xml.name() == "hidden_columns") {
2421             while(xml.readNext() != QXmlStreamReader::EndElement && xml.name() != "hidden_columns") {
2422                 if (xml.name() == "column") {
2423                     int index = xml.attributes().value("index").toInt();
2424                     settings.hiddenColumns[index] = xml.attributes().value("value").toInt();
2425                     xml.skipCurrentElement();
2426                 }
2427             }
2428         } else if(xml.name() == "plot_y_axes") {
2429             while(xml.readNext() != QXmlStreamReader::EndElement && xml.name() != "plot_y_axes") {
2430                 QString y1AxisName;
2431                 QString y2AxisName;
2432                 PlotDock::PlotSettings y1AxisSettings;
2433                 PlotDock::PlotSettings y2AxisSettings;
2434                 if (xml.name() == "y_axis") {
2435                     y1AxisName = xml.attributes().value("name").toString();
2436                     y1AxisSettings.lineStyle = xml.attributes().value("line_style").toInt();
2437                     y1AxisSettings.pointShape = xml.attributes().value("point_shape").toInt();
2438                     y1AxisSettings.colour = QColor (xml.attributes().value("colour").toString());
2439                     y1AxisSettings.active = xml.attributes().value("active").toInt();
2440                     xml.skipCurrentElement();
2441                 }
2442                 settings.plotYAxes[0][y1AxisName] = y1AxisSettings;
2443                 if (xml.name() == "y2_axis") {
2444                   y2AxisName = xml.attributes().value("name").toString();
2445                   y2AxisSettings.lineStyle = xml.attributes().value("line_style").toInt();
2446                   y2AxisSettings.pointShape = xml.attributes().value("point_shape").toInt();
2447                   y2AxisSettings.colour = QColor (xml.attributes().value("colour").toString());
2448                   y2AxisSettings.active = xml.attributes().value("active").toInt();
2449                   xml.skipCurrentElement();
2450                 }
2451                 settings.plotYAxes[1][y2AxisName] = y2AxisSettings;
2452             }
2453         } else if(xml.name() == "global_filter") {
2454             while(xml.readNext() != QXmlStreamReader::EndElement && xml.name() != "global_filter")
2455             {
2456                 if(xml.name() == "filter")
2457                 {
2458                     QString value = xml.attributes().value("value").toString();
2459                     settings.globalFilters.push_back(value);
2460                     xml.skipCurrentElement();
2461                 }
2462             }
2463         }
2464     }
2465 }
loadProject(QString filename,bool readOnly)2466 bool MainWindow::loadProject(QString filename, bool readOnly)
2467 {
2468     // Show the open file dialog when no filename was passed as parameter
2469     if(filename.isEmpty())
2470     {
2471         filename = FileDialog::getOpenFileName(
2472                        OpenProjectFile,
2473                        this,
2474                        tr("Choose a project file to open"),
2475                        tr("DB Browser for SQLite project file (*.sqbpro)"));
2476     }
2477 
2478     if(!filename.isEmpty())
2479     {
2480         QFile file(filename);
2481         file.open(QFile::ReadOnly | QFile::Text);
2482 
2483         QXmlStreamReader xml(&file);
2484         xml.readNext();     // token == QXmlStreamReader::StartDocument
2485         xml.readNext();     // name == sqlb_project
2486         if(xml.name() != "sqlb_project")
2487             return false;
2488 
2489         // We are going to open a new project, so close the possible current one before opening another.
2490         // Stop the opening process here if the user pressed the cancel button in there.
2491         if(!closeProject())
2492             return false;
2493 
2494         addToRecentFilesMenu(filename, readOnly);
2495         currentProjectFilename = filename;
2496 
2497         QString currentTable;
2498         while(!xml.atEnd() && !xml.hasError())
2499         {
2500             // Read next token
2501             QXmlStreamReader::TokenType token = xml.readNext();
2502 
2503             // Handle element start
2504             if(token == QXmlStreamReader::StartElement)
2505             {
2506                 if(xml.name() == "db")
2507                 {
2508                     // Read only?
2509                     if(xml.attributes().hasAttribute("readonly") && xml.attributes().value("readonly").toInt())
2510                         readOnly = true;
2511 
2512                     // DB file
2513                     QString dbfilename = xml.attributes().value("path").toString();
2514                     if(!QFile::exists(dbfilename)) {
2515                         dbfilename = QFileInfo(filename).absolutePath() + QDir::separator() + dbfilename;
2516                         // New DB filename is pending to be saved
2517                         isProjectModified = true;
2518                     }
2519                     fileOpen(dbfilename, true, readOnly);
2520                     ui->dbTreeWidget->collapseAll();
2521 
2522                     // PRAGMAs
2523                     if(xml.attributes().hasAttribute("foreign_keys"))
2524                         db.setPragma("foreign_keys", xml.attributes().value("foreign_keys").toString());
2525                     if(xml.attributes().hasAttribute("case_sensitive_like"))
2526                         db.setPragma("case_sensitive_like", xml.attributes().value("case_sensitive_like").toString());
2527                     if(xml.attributes().hasAttribute("temp_store"))
2528                         db.setPragma("temp_store", xml.attributes().value("temp_store").toString());
2529                     if(xml.attributes().hasAttribute("wal_autocheckpoint"))
2530                         db.setPragma("wal_autocheckpoint", xml.attributes().value("wal_autocheckpoint").toString());
2531                     if(xml.attributes().hasAttribute("synchronous"))
2532                         db.setPragma("synchronous", xml.attributes().value("synchronous").toString());
2533                     loadPragmas();
2534                 } else if(xml.name() == "attached") {
2535                     while(xml.readNext() != QXmlStreamReader::EndElement && xml.name() != "attached")
2536                     {
2537                         if(xml.name() == "db")
2538                         {
2539                             db.attach(xml.attributes().value("path").toString(), xml.attributes().value("schema").toString());
2540                             xml.skipCurrentElement();
2541                         }
2542                     }
2543                 } else if(xml.name() == "window") {
2544                     // Window settings
2545                     while(xml.readNext() != QXmlStreamReader::EndElement && xml.name() != "window")
2546                     {
2547                         if(xml.name() == "main_tabs") {
2548                             // Currently open tabs
2549                             restoreOpenTabs(xml.attributes().value("open").toString());
2550                             // Currently selected open tab
2551                             ui->mainTab->setCurrentIndex(xml.attributes().value("current").toString().toInt());
2552                             xml.skipCurrentElement();
2553                         } else if(xml.name() == "current_tab") {
2554                             // Currently selected tab (3.11 or older format, first restore default open tabs)
2555                             restoreOpenTabs(defaultOpenTabs);
2556                             ui->mainTab->setCurrentIndex(xml.attributes().value("id").toString().toInt());
2557                             xml.skipCurrentElement();
2558                         }
2559                     }
2560                 } else if(xml.name() == "tab_structure") {
2561                     // Database Structure tab settings
2562                     while(xml.readNext() != QXmlStreamReader::EndElement && xml.name() != "tab_structure")
2563                     {
2564                         if(xml.name() == "column_width")
2565                         {
2566                             // Tree view column widths
2567                             ui->dbTreeWidget->setColumnWidth(xml.attributes().value("id").toString().toInt(),
2568                                                              xml.attributes().value("width").toString().toInt());
2569                             xml.skipCurrentElement();
2570                         } else if(xml.name() == "expanded_item") {
2571                             // Tree view expanded items
2572                             int parent = xml.attributes().value("parent").toString().toInt();
2573                             QModelIndex idx;
2574                             if(parent == -1)
2575                                 idx = ui->dbTreeWidget->model()->index(xml.attributes().value("id").toString().toInt(), 0);
2576                             else
2577                                 idx = ui->dbTreeWidget->model()->index(xml.attributes().value("id").toString().toInt(), 0, ui->dbTreeWidget->model()->index(parent, 0));
2578                             ui->dbTreeWidget->expand(idx);
2579                             xml.skipCurrentElement();
2580                         }
2581                     }
2582                 } else if(xml.name() == "tab_browse") {
2583                     // Browse Data tab settings
2584                     while(xml.readNext() != QXmlStreamReader::EndElement && xml.name() != "tab_browse")
2585                     {
2586                         if(xml.name() == "current_table")
2587                         {
2588                             // Currently selected table
2589                             currentTable = xml.attributes().value("name").toString();
2590                             xml.skipCurrentElement();
2591                         } else if(xml.name() == "default_encoding") {
2592                             // Default text encoding
2593                             ui->tableBrowser->setDefaultEncoding(xml.attributes().value("codec").toString());
2594                             xml.skipCurrentElement();
2595                         } else if(xml.name() == "browsetable_info") {
2596                             // This tag is only found in old project files. In newer versions (>= 3.11) it is replaced by a new implementation.
2597                             // We still support loading it though we might decide to drop that support later. But for now we show a warning to the
2598                             // user when loading an old file.
2599                             if(!Settings::getValue("idontcare", "projectBrowseTable").toBool())
2600                             {
2601                                 QMessageBox msgBox;
2602                                 QPushButton* idontcarebutton = msgBox.addButton(tr("Don't show again"), QMessageBox::ActionRole);
2603                                 msgBox.addButton(QMessageBox::Ok);
2604                                 msgBox.setTextFormat(Qt::RichText);
2605                                 msgBox.setWindowTitle(qApp->applicationName());
2606                                 msgBox.setText(tr("This project file is using an old file format because it was created using DB Browser for SQLite "
2607                                                   "version 3.10 or lower. Loading this file format is still fully supported but we advice you to convert "
2608                                                   "all your project files to the new file format because support for older formats might be dropped "
2609                                                   "at some point in the future. You can convert your files by simply opening and re-saving them."));
2610                                 msgBox.exec();
2611                                 if(msgBox.clickedButton() == idontcarebutton)
2612                                     Settings::setValue("idontcare", "projectBrowseTable", true);
2613                             }
2614 
2615                             QString attrData = xml.attributes().value("data").toString();
2616                             QByteArray temp = QByteArray::fromBase64(attrData.toUtf8());
2617                             QDataStream stream(temp);
2618                             QMap<sqlb::ObjectIdentifier, BrowseDataTableSettings> settings;
2619                             stream >> settings;
2620                             for(auto it=settings.begin();it!=settings.end();++it)
2621                                 ui->tableBrowser->setSettings(it.key(), it.value());
2622 
2623                             xml.skipCurrentElement();
2624                         } else if(xml.name() == "browse_table_settings") {
2625 
2626                             while(xml.readNext() != QXmlStreamReader::EndElement && xml.name() != "browse_table_settings") {
2627                                 if (xml.name() == "table") {
2628 
2629                                     sqlb::ObjectIdentifier tableIdentifier =
2630                                         sqlb::ObjectIdentifier (xml.attributes().value("schema").toString().toStdString(),
2631                                                                 xml.attributes().value("name").toString().toStdString());
2632                                     BrowseDataTableSettings settings;
2633                                     loadBrowseDataTableSettings(settings, xml);
2634                                     ui->tableBrowser->setSettings(tableIdentifier, settings);
2635                                 }
2636                             }
2637                         }
2638 
2639                     }
2640                 } else if(xml.name() == "tab_sql") {
2641                     // Close all open tabs first
2642                     for(int i=ui->tabSqlAreas->count()-1;i>=0;i--)
2643                         closeSqlTab(i, true);
2644 
2645                     // Execute SQL tab data
2646                     while(xml.readNext() != QXmlStreamReader::EndElement && xml.name() != "tab_sql")
2647                     {
2648                         if(xml.name() == "sql")
2649                         {
2650                             // SQL editor tab
2651                             int index = openSqlTab();
2652                             ui->tabSqlAreas->setTabText(index, xml.attributes().value("name").toString());
2653                             SqlTextEdit* sqlEditor = qobject_cast<SqlExecutionArea*>(ui->tabSqlAreas->widget(index))->getEditor();
2654                             sqlEditor->setText(xml.readElementText());
2655                             sqlEditor->setModified(false);
2656                         } else if(xml.name() == "current_tab") {
2657                             // Currently selected tab
2658                             ui->tabSqlAreas->setCurrentIndex(xml.attributes().value("id").toString().toInt());
2659                             xml.skipCurrentElement();
2660                         }
2661                     }
2662                 }
2663             }
2664         }
2665 
2666         file.close();
2667 
2668         if(ui->mainTab->currentWidget() == ui->browser) {
2669             if (!currentTable.isEmpty())
2670             {
2671                 sqlb::ObjectIdentifier obj;
2672                 if(!obj.fromSerialised(currentTable.toStdString()))
2673                 {
2674                     // This is an old project file format which doesn't yet contain serialised table identifiers. This means
2675                     // we have to try our best to unserialise this one manually. The only problem is when the name of an
2676                     // attached database or of a table contains a dot character. In that case the name becomes ambigious and
2677                     // we just try to split it at the first dot. I don't think it affects many (if any) project files. But if
2678                     // it turn out to be wrong, we can always add a loop here which checks for any possible combination of schema
2679                     // and table name whether an object with that combination exists.
2680                     // TODO: Delete this code in the future when we don't expect there to be any project files in the old format anymore.
2681                     if(currentTable.contains('.'))
2682                     {
2683                         obj.setSchema(currentTable.left(currentTable.indexOf('.')).toStdString());
2684                         obj.setName(currentTable.mid(currentTable.indexOf('.')+1).toStdString());
2685                     } else {
2686                         obj.setName(currentTable.toStdString());
2687                     }
2688                 }
2689                 switchToBrowseDataTab(obj);
2690             }
2691             populateTable();     // Refresh view
2692         }
2693 
2694         isProjectModified = false;
2695 
2696         return !xml.hasError();
2697     } else {
2698         // No project was opened
2699         return false;
2700     }
2701 }
2702 
saveDbTreeState(const QTreeView * tree,QXmlStreamWriter & xml,QModelIndex index=QModelIndex (),int parent_row=-1)2703 static void saveDbTreeState(const QTreeView* tree, QXmlStreamWriter& xml, QModelIndex index = QModelIndex(), int parent_row = -1)
2704 {
2705     for(int i=0;i<tree->model()->rowCount(index);i++)
2706     {
2707         if(tree->isExpanded(tree->model()->index(i, 0, index)))
2708         {
2709             xml.writeStartElement("expanded_item");
2710             xml.writeAttribute("id", QString::number(i));
2711             xml.writeAttribute("parent", QString::number(parent_row));
2712             xml.writeEndElement();
2713         }
2714 
2715         saveDbTreeState(tree, xml, tree->model()->index(i, 0, index), i);
2716     }
2717 }
2718 
saveCondFormatMap(const QString & elementName,const BrowseDataTableSettings::CondFormatMap & condFormats,QXmlStreamWriter & xml)2719 static void saveCondFormatMap(const QString& elementName, const BrowseDataTableSettings::CondFormatMap& condFormats, QXmlStreamWriter& xml)
2720 {
2721     xml.writeStartElement(elementName);
2722     for(auto iter=condFormats.constBegin(); iter!=condFormats.constEnd(); ++iter) {
2723         xml.writeStartElement("column");
2724         xml.writeAttribute("index", QString::number(iter.key()));
2725         for(auto format : iter.value()) {
2726             xml.writeStartElement("format");
2727             xml.writeAttribute("condition", format.filter());
2728             xml.writeAttribute("background", format.backgroundColor().name());
2729             xml.writeAttribute("foreground", format.foregroundColor().name());
2730             xml.writeAttribute("font", format.font().toString());
2731             xml.writeAttribute("align", QString().setNum(format.alignment()));
2732             xml.writeEndElement();
2733         }
2734         xml.writeEndElement();
2735     }
2736     xml.writeEndElement();
2737 }
2738 
saveBrowseDataTableSettings(const BrowseDataTableSettings & object,QXmlStreamWriter & xml)2739 static void saveBrowseDataTableSettings(const BrowseDataTableSettings& object, QXmlStreamWriter& xml)
2740 {
2741     xml.writeAttribute("show_row_id", QString::number(object.showRowid));
2742     xml.writeAttribute("encoding", object.encoding);
2743     xml.writeAttribute("plot_x_axis", object.plotXAxis);
2744     xml.writeAttribute("unlock_view_pk", object.unlockViewPk);
2745 
2746     xml.writeStartElement("sort");
2747     for(const auto& column : object.query.orderBy())
2748     {
2749         xml.writeStartElement("column");
2750         xml.writeAttribute("index", QString::number(column.column));
2751         xml.writeAttribute("mode", QString::number(column.direction));
2752         xml.writeEndElement();
2753     }
2754     xml.writeEndElement();
2755 
2756     xml.writeStartElement("column_widths");
2757     for(auto iter=object.columnWidths.constBegin(); iter!=object.columnWidths.constEnd(); ++iter) {
2758         xml.writeStartElement("column");
2759         xml.writeAttribute("index", QString::number(iter.key()));
2760         xml.writeAttribute("value", QString::number(iter.value()));
2761         xml.writeEndElement();
2762     }
2763     xml.writeEndElement();
2764     xml.writeStartElement("filter_values");
2765     for(auto iter=object.filterValues.constBegin(); iter!=object.filterValues.constEnd(); ++iter) {
2766         xml.writeStartElement("column");
2767         xml.writeAttribute("index", QString::number(iter.key()));
2768         xml.writeAttribute("value", iter.value());
2769         xml.writeEndElement();
2770     }
2771     xml.writeEndElement();
2772     saveCondFormatMap("conditional_formats", object.condFormats, xml);
2773     saveCondFormatMap("row_id_formats", object.rowIdFormats, xml);
2774     xml.writeStartElement("display_formats");
2775     for(auto iter=object.displayFormats.constBegin(); iter!=object.displayFormats.constEnd(); ++iter) {
2776         xml.writeStartElement("column");
2777         xml.writeAttribute("index", QString::number(iter.key()));
2778         xml.writeAttribute("value", iter.value());
2779         xml.writeEndElement();
2780     }
2781     xml.writeEndElement();
2782     xml.writeStartElement("hidden_columns");
2783     for(auto iter=object.hiddenColumns.constBegin(); iter!=object.hiddenColumns.constEnd(); ++iter) {
2784         xml.writeStartElement("column");
2785         xml.writeAttribute("index", QString::number(iter.key()));
2786         xml.writeAttribute("value", QString::number(iter.value()));
2787         xml.writeEndElement();
2788     }
2789     xml.writeEndElement();
2790     xml.writeStartElement("plot_y_axes");
2791     for(auto iter=object.plotYAxes[0].constBegin(); iter!=object.plotYAxes[0].constEnd(); ++iter) {
2792         PlotDock::PlotSettings plotSettings = iter.value();
2793         xml.writeStartElement("y_axis");
2794         xml.writeAttribute("name", iter.key());
2795         xml.writeAttribute("line_style", QString::number(plotSettings.lineStyle));
2796         xml.writeAttribute("point_shape", QString::number(plotSettings.pointShape));
2797         xml.writeAttribute("colour", plotSettings.colour.name());
2798         xml.writeAttribute("active", QString::number(plotSettings.active));
2799         xml.writeEndElement();
2800     }
2801     for(auto iter=object.plotYAxes[1].constBegin(); iter!=object.plotYAxes[1].constEnd(); ++iter) {
2802       PlotDock::PlotSettings plotSettings = iter.value();
2803       xml.writeStartElement("y2_axis");
2804       xml.writeAttribute("name", iter.key());
2805       xml.writeAttribute("line_style", QString::number(plotSettings.lineStyle));
2806       xml.writeAttribute("point_shape", QString::number(plotSettings.pointShape));
2807       xml.writeAttribute("colour", plotSettings.colour.name());
2808       xml.writeAttribute("active", QString::number(plotSettings.active));
2809       xml.writeEndElement();
2810     }
2811     xml.writeEndElement();
2812     xml.writeStartElement("global_filter");
2813     for(const auto& v : object.globalFilters)
2814     {
2815         xml.writeStartElement("filter");
2816         xml.writeAttribute("value", v);
2817         xml.writeEndElement();
2818     }
2819     xml.writeEndElement();
2820 }
2821 
saveProject(const QString & currentFilename)2822 void MainWindow::saveProject(const QString& currentFilename)
2823 {
2824     QString filename;
2825     if(currentFilename.isEmpty()) {
2826         QString basePathName = db.currentFile();
2827         // Remove database suffix
2828         basePathName.chop(QFileInfo(basePathName).suffix().size()+1);
2829         filename = FileDialog::getSaveFileName(
2830                            CreateProjectFile,
2831                            this,
2832                            tr("Choose a filename to save under"),
2833                            FILE_FILTER_SQLPRJ,
2834                            basePathName);
2835     } else
2836         filename = currentFilename;
2837 
2838     if(!filename.isEmpty())
2839     {
2840         // Make sure the file has got a .sqbpro ending
2841         if(!filename.endsWith(FILE_EXT_SQLPRJ_DEFAULT, Qt::CaseInsensitive))
2842             filename.append(FILE_EXT_SQLPRJ_DEFAULT);
2843 
2844         QFile file(filename);
2845         bool opened = file.open(QFile::WriteOnly | QFile::Text);
2846         if(!opened) {
2847             QMessageBox::warning(this, qApp->applicationName(),
2848                                tr("Could not open project file for writing.\nReason: %1").arg(file.errorString()));
2849             currentProjectFilename.clear();
2850             return;
2851         }
2852         currentProjectFilename = filename;
2853         QApplication::setOverrideCursor(Qt::WaitCursor);
2854 
2855         QXmlStreamWriter xml(&file);
2856         xml.writeStartDocument();
2857         xml.writeStartElement("sqlb_project");
2858 
2859         // Database file name
2860         xml.writeStartElement("db");
2861         xml.writeAttribute("path", db.currentFile());
2862         xml.writeAttribute("readonly", QString::number(db.readOnly()));
2863         xml.writeAttribute("foreign_keys", db.getPragma("foreign_keys"));
2864         xml.writeAttribute("case_sensitive_like", db.getPragma("case_sensitive_like"));
2865         xml.writeAttribute("temp_store", db.getPragma("temp_store"));
2866         xml.writeAttribute("wal_autocheckpoint", db.getPragma("wal_autocheckpoint"));
2867         xml.writeAttribute("synchronous", db.getPragma("synchronous"));
2868         xml.writeEndElement();
2869 
2870         // Attached databases
2871         xml.writeStartElement("attached");
2872         db.executeSQL("PRAGMA database_list;", false, true, [&xml](int, std::vector<QByteArray> values, std::vector<QByteArray>) -> bool {
2873             auto schema = values.at(1);
2874             if(schema != "main" && schema != "temp")
2875             {
2876                 auto path = values.at(2);
2877                 xml.writeStartElement("db");
2878                 xml.writeAttribute("schema", schema);
2879                 xml.writeAttribute("path", path);
2880                 xml.writeEndElement();
2881             }
2882             return false;
2883         });
2884         xml.writeEndElement();
2885 
2886         // Window settings
2887         xml.writeStartElement("window");
2888         xml.writeStartElement("main_tabs");   // Currently open tabs
2889         xml.writeAttribute("open", saveOpenTabs());
2890         xml.writeAttribute("current", QString::number(ui->mainTab->currentIndex()));
2891         xml.writeEndElement();
2892         xml.writeEndElement();
2893 
2894         // Database Structure tab settings
2895         xml.writeStartElement("tab_structure");
2896         for(int i=0;i<ui->dbTreeWidget->model()->columnCount();i++) // Widths of tree view columns
2897         {
2898             xml.writeStartElement("column_width");
2899             xml.writeAttribute("id", QString::number(i));
2900             xml.writeAttribute("width", QString::number(ui->dbTreeWidget->columnWidth(i)));
2901             xml.writeEndElement();
2902         }
2903         saveDbTreeState(ui->dbTreeWidget, xml);                     // Expanded tree items
2904         xml.writeEndElement();
2905 
2906         // Browse Data tab settings
2907         xml.writeStartElement("tab_browse");
2908         xml.writeStartElement("current_table");     // Currently selected table
2909         xml.writeAttribute("name", QString::fromStdString(ui->tableBrowser->currentlyBrowsedTableName().toSerialised()));
2910         xml.writeEndElement();
2911         xml.writeStartElement("default_encoding");  // Default encoding for text stored in tables
2912         xml.writeAttribute("codec", ui->tableBrowser->defaultEncoding());
2913         xml.writeEndElement();
2914 
2915         xml.writeStartElement("browse_table_settings");
2916         const auto settings = ui->tableBrowser->allSettings();
2917         for(auto tableIt=settings.constBegin(); tableIt!=settings.constEnd(); ++tableIt) {
2918 
2919             xml.writeStartElement("table");
2920             xml.writeAttribute("schema", QString::fromStdString(tableIt.key().schema()));
2921             xml.writeAttribute("name", QString::fromStdString(tableIt.key().name()));
2922             saveBrowseDataTableSettings(tableIt.value(), xml);
2923             xml.writeEndElement();
2924         }
2925         // </browse_table_settings>
2926         xml.writeEndElement();
2927         // </tab_browse>
2928         xml.writeEndElement();
2929 
2930         // Execute SQL tab data
2931         xml.writeStartElement("tab_sql");
2932         for(int i=0;i<ui->tabSqlAreas->count();i++)                                     // All SQL tabs content
2933         {
2934             SqlExecutionArea* sqlArea = qobject_cast<SqlExecutionArea*>(ui->tabSqlAreas->widget(i));
2935             xml.writeStartElement("sql");
2936             xml.writeAttribute("name", ui->tabSqlAreas->tabText(i));
2937             xml.writeCharacters(sqlArea->getSql());
2938             sqlArea->getEditor()->setModified(false);
2939             xml.writeEndElement();
2940         }
2941         xml.writeStartElement("current_tab");                                           // Currently selected tab
2942         xml.writeAttribute("id", QString::number(ui->tabSqlAreas->currentIndex()));
2943         xml.writeEndElement();
2944         xml.writeEndElement();
2945 
2946         xml.writeEndElement();
2947         xml.writeEndDocument();
2948         file.close();
2949 
2950         addToRecentFilesMenu(filename);
2951         setCurrentFile(db.currentFile());
2952         isProjectModified = false;
2953         showStatusMessage5s(tr("Project saved to file '%1'").arg(currentProjectFilename));
2954         QApplication::restoreOverrideCursor();
2955     }
2956 }
2957 
saveProject()2958 void MainWindow::saveProject()
2959 {
2960     saveProject(currentProjectFilename);
2961 }
2962 
saveProjectAs()2963 void MainWindow::saveProjectAs()
2964 {
2965     saveProject(QString());
2966 }
2967 
fileAttach(const QString & fileName)2968 void MainWindow::fileAttach(const QString& fileName)
2969 {
2970     QString file;
2971     if (fileName.isEmpty()) {
2972 
2973         // Get file name of database to attach
2974         file = FileDialog::getOpenFileName(
2975             OpenDatabaseFile,
2976             this,
2977             tr("Choose a database file"),
2978             FileDialog::getSqlDatabaseFileFilter());
2979     } else
2980         file = fileName;
2981 
2982     if(!QFile::exists(file))
2983         return;
2984 
2985     // Attach it
2986     db.attach(file);
2987     isProjectModified = true;
2988 }
2989 
editEncryption()2990 void MainWindow::editEncryption()
2991 {
2992 #ifdef ENABLE_SQLCIPHER
2993     CipherDialog cipherDialog(this, true);
2994     if(cipherDialog.exec())
2995     {
2996         // Show progress dialog even though we can't provide any detailed progress information but this
2997         // process might take some time.
2998         QProgressDialog progress(this);
2999         progress.setCancelButton(nullptr);
3000         progress.setWindowModality(Qt::ApplicationModal);
3001         progress.show();
3002         qApp->processEvents();
3003 
3004         // Apply all unsaved changes
3005         bool ok = db.releaseAllSavepoints();
3006         qApp->processEvents();
3007 
3008         // Create the new file first or it won't work
3009         if(ok)
3010         {
3011             QFile file(db.currentFile() + ".enctemp");
3012             file.open(QFile::WriteOnly);
3013             file.close();
3014         }
3015 
3016         CipherSettings cipherSettings = cipherDialog.getCipherSettings();
3017 
3018         // Attach a new database using the new settings
3019         qApp->processEvents();
3020         if(ok)
3021             ok = db.executeSQL("ATTACH DATABASE '" + db.currentFile().toStdString() + ".enctemp' AS sqlitebrowser_edit_encryption KEY " + cipherSettings.getPassword() + ";",
3022                                false, false);
3023         qApp->processEvents();
3024         if(ok)
3025             ok = db.executeSQL("PRAGMA sqlitebrowser_edit_encryption.cipher_page_size = " + std::to_string(cipherSettings.getPageSize()), false, false);
3026         if(ok)
3027             ok = db.executeSQL("PRAGMA sqlitebrowser_edit_encryption.cipher_hmac_algorithm = " + cipherSettings.getHmacAlgorithm(), false, false);
3028         if(ok)
3029             ok = db.executeSQL("PRAGMA sqlitebrowser_edit_encryption.cipher_kdf_algorithm = " + cipherSettings.getKdfAlgorithm(), false, false);
3030         if(ok)
3031             ok = db.executeSQL("PRAGMA sqlitebrowser_edit_encryption.kdf_iter = " + std::to_string(cipherSettings.getKdfIterations()), false, false);
3032         if (ok)
3033             ok = db.executeSQL("PRAGMA sqlitebrowser_edit_encryption.cipher_plaintext_header_size = " + std::to_string(cipherSettings.getPlaintextHeaderSize()), false, false);
3034 
3035         // Export the current database to the new one
3036         qApp->processEvents();
3037         if(ok)
3038             ok = db.executeSQL("SELECT sqlcipher_export('sqlitebrowser_edit_encryption');", false, false);
3039 
3040         // Set user version of the new database
3041         qApp->processEvents();
3042         if (ok)
3043             ok = db.executeSQL("PRAGMA sqlitebrowser_edit_encryption.user_version = " + std::to_string(db.getPragma("user_version").toInt()) + ";", false, false);
3044 
3045         // We need to detach the database before proceeding
3046         qApp->processEvents();
3047         if (ok)
3048             ok = db.executeSQL("DETACH sqlitebrowser_edit_encryption;", false, false);
3049 
3050         // Check for errors
3051         qApp->processEvents();
3052         if(ok)
3053         {
3054             // No errors: Then close the current database, switch names, open the new one and if that succeeded delete the old one
3055 
3056             fileClose();
3057             QFile::rename(db.currentFile(), db.currentFile() + ".enctempold");
3058             QFile::rename(db.currentFile() + ".enctemp", db.currentFile());
3059             if(fileOpen(db.currentFile()))
3060                 QFile::remove(db.currentFile() + ".enctempold");
3061         } else {
3062             QMessageBox::warning(this, qApp->applicationName(), db.lastError());
3063         }
3064     }
3065 #endif
3066 }
3067 
switchToBrowseDataTab(sqlb::ObjectIdentifier tableToBrowse)3068 void MainWindow::switchToBrowseDataTab(sqlb::ObjectIdentifier tableToBrowse)
3069 {
3070     // If no table name was provided get the currently selected table fromt he structure tab
3071     if(tableToBrowse.isEmpty())
3072     {
3073         // Cancel here if there is no selection
3074         if(!ui->dbTreeWidget->selectionModel()->hasSelection())
3075             return;
3076 
3077         tableToBrowse.setSchema(ui->dbTreeWidget->model()->data(ui->dbTreeWidget->currentIndex().sibling(ui->dbTreeWidget->currentIndex().row(), DbStructureModel::ColumnSchema), Qt::EditRole).toString().toStdString());
3078         tableToBrowse.setName(ui->dbTreeWidget->model()->data(ui->dbTreeWidget->currentIndex().sibling(ui->dbTreeWidget->currentIndex().row(), DbStructureModel::ColumnName), Qt::EditRole).toString().toStdString());
3079     }
3080 
3081     ui->tableBrowser->setCurrentTable(tableToBrowse);
3082     if (ui->mainTab->indexOf(ui->browser) == -1)
3083         ui->mainTab->addTab(ui->browser, ui->browser->accessibleName());
3084     ui->mainTab->setCurrentWidget(ui->browser);
3085 }
3086 
copyCurrentCreateStatement()3087 void MainWindow::copyCurrentCreateStatement()
3088 {
3089     // Cancel if no field is currently selected
3090     if(!ui->dbTreeWidget->selectionModel()->hasSelection())
3091         return;
3092 
3093     // Get the CREATE statement from the Schema column
3094     QString stmt = ui->dbTreeWidget->model()->data(ui->dbTreeWidget->currentIndex().sibling(ui->dbTreeWidget->currentIndex().row(), 3), Qt::EditRole).toString();
3095 
3096     // Copy the statement to the global application clipboard
3097     QApplication::clipboard()->setText(stmt);
3098 }
3099 
fileOpenReadOnly()3100 void MainWindow::fileOpenReadOnly()
3101 {
3102     // Redirect to 'standard' fileOpen(), with the read only flag set
3103     fileOpen(QString(), false, true);
3104 }
3105 
requestCollation(const QString & name,int eTextRep)3106 void MainWindow::requestCollation(const QString& name, int eTextRep)
3107 {
3108     QMessageBox::StandardButton reply = QMessageBox::question(
3109                 this,
3110                 tr("Collation needed! Proceed?"),
3111                 tr("A table in this database requires a special collation function '%1' "
3112                    "that this application can't provide without further knowledge.\n"
3113                    "If you choose to proceed, be aware bad things can happen to your database.\n"
3114                    "Create a backup!").arg(name), QMessageBox::Yes | QMessageBox::No);
3115     if(reply == QMessageBox::Yes) {
3116         auto pDb = db.get(tr("creating collation"));
3117         sqlite3_create_collation(pDb.get(), name.toUtf8(), eTextRep, nullptr, collCompare);
3118     }
3119 }
3120 
renameSqlTab(int index)3121 void MainWindow::renameSqlTab(int index)
3122 {
3123     QString new_name = QInputDialog::getText(this,
3124                                              qApp->applicationName(),
3125                                              tr("Set a new name for the SQL tab. Use the '&&' character to allow using the following character as a keyboard shortcut."),
3126                                              QLineEdit::EchoMode::Normal,
3127                                              ui->tabSqlAreas->tabText(index));
3128 
3129     if(!new_name.isNull())      // Don't do anything if the Cancel button was clicked
3130         ui->tabSqlAreas->setTabText(index, new_name);
3131 }
3132 
setFindFrameVisibility(bool show)3133 void MainWindow::setFindFrameVisibility(bool show)
3134 {
3135     // Set the find frame visibility for all tabs, but leave the
3136     // current as the last, to retain there the focus.
3137     for(int i=0;i<ui->tabSqlAreas->count();i++)
3138         if (i != ui->tabSqlAreas->currentIndex())
3139             qobject_cast<SqlExecutionArea*>(ui->tabSqlAreas->widget(i))->setFindFrameVisibility(show);
3140     if (ui->tabSqlAreas->count()>0)
3141         qobject_cast<SqlExecutionArea*>(ui->tabSqlAreas->currentWidget())->setFindFrameVisibility(show);
3142 }
3143 
openFindReplaceDialog()3144 void MainWindow::openFindReplaceDialog()
3145 {
3146     // The slot for the shortcut must discover which sqltexedit widget has the focus and then open its dialog.
3147     SqlExecutionArea* sqlWidget = qobject_cast<SqlExecutionArea*>(ui->tabSqlAreas->currentWidget());
3148 
3149     if (sqlWidget)
3150         sqlWidget->getEditor()->openFindReplaceDialog();
3151 }
3152 
toggleSqlBlockComment()3153 void MainWindow::toggleSqlBlockComment()
3154 {
3155     // The slot for the shortcut must discover which sqltexedit widget has the focus
3156     SqlExecutionArea* sqlWidget = qobject_cast<SqlExecutionArea*>(ui->tabSqlAreas->currentWidget());
3157 
3158     if (sqlWidget)
3159         sqlWidget->getEditor()->toggleBlockComment();
3160 }
3161 
openSqlPrintDialog()3162 void MainWindow::openSqlPrintDialog()
3163 {
3164     // The slot for the shortcut must discover which sqltexedit widget has the focus and then open its dialog.
3165     SqlExecutionArea* sqlWidget = qobject_cast<SqlExecutionArea*>(ui->tabSqlAreas->currentWidget());
3166 
3167     if (sqlWidget)
3168         sqlWidget->getEditor()->openPrintDialog();
3169 }
3170 
saveAsView(const std::string & query)3171 void MainWindow::saveAsView(const std::string& query)
3172 {
3173     // Let the user select a name for the new view and make sure it doesn't already exist
3174     QString name;
3175     while(true)
3176     {
3177         name = QInputDialog::getText(this, qApp->applicationName(), tr("Please specify the view name")).trimmed();
3178         if(name.isNull())
3179             return;
3180         if(db.getObjectByName(sqlb::ObjectIdentifier("main", name.toStdString())) != nullptr)
3181             QMessageBox::warning(this, qApp->applicationName(), tr("There is already an object with that name. Please choose a different name."));
3182         else
3183             break;
3184     }
3185 
3186     // Create the view
3187     if(db.executeSQL("CREATE VIEW " + sqlb::escapeIdentifier(name.toStdString()) + " AS " + query + ";"))
3188         QMessageBox::information(this, qApp->applicationName(), tr("View successfully created."));
3189     else
3190         QMessageBox::warning(this, qApp->applicationName(), tr("Error creating view: %1").arg(db.lastError()));
3191 }
3192 
runSqlNewTab(const QString & query,const QString & title,const QString & helpUrl,const bool autoRun)3193 void MainWindow::runSqlNewTab(const QString& query, const QString& title, const QString& helpUrl, const bool autoRun)
3194 {
3195     QString message;
3196 
3197     if(autoRun)
3198         message = tr("This action will open a new SQL tab for running:");
3199     else
3200         message = tr("This action will open a new SQL tab with the following statements for you to edit and run:");
3201 
3202     message += QString("<blockquote><tt>%1</tt></blockquote>").arg(query) +
3203                tr("Press Help for opening the corresponding SQLite reference page.");
3204 
3205     QString windowTitle = title;
3206     windowTitle.remove('&');
3207 
3208     switch (QMessageBox::information(this, windowTitle, message, QMessageBox::Ok | QMessageBox::Default, QMessageBox::Cancel | QMessageBox::Escape, QMessageBox::Help))
3209     {
3210     case QMessageBox::Ok: {
3211         if (ui->mainTab->indexOf(ui->query) == -1)
3212             ui->mainTab->addTab(ui->query, ui->query->accessibleName());
3213         ui->mainTab->setCurrentWidget(ui->query);
3214         int index = openSqlTab();
3215         ui->tabSqlAreas->setTabText(index, title);
3216         qobject_cast<SqlExecutionArea*>(ui->tabSqlAreas->widget(index))->getEditor()->setText(query);
3217         if(autoRun)
3218             executeQuery();
3219         break;
3220     }
3221     case QMessageBox::Help: {
3222         QDesktopServices::openUrl(QUrl(helpUrl));
3223         break;
3224     }
3225     default:
3226         return;
3227     }
3228 }
3229 
printDbStructure()3230 void MainWindow::printDbStructure ()
3231 {
3232     const QTreeView* treeView = ui->dbTreeWidget;
3233     const QAbstractItemModel* model = treeView->model();
3234 
3235     const int rowCount = model->rowCount(treeView->rootIndex());
3236     const int columnCount = model->columnCount(treeView->rootIndex());
3237 
3238     QString strStream;
3239     QTextStream out(&strStream);
3240 
3241     out << "<html><meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">"
3242         << QString("<title>%1</title>").arg(treeView->windowTitle())
3243         << "<style type=\"text/css\">pre {white-space: pre-wrap;}</style></head><body bgcolor=\"#FFFFFF\">";
3244 
3245     for (int row = 0; row < rowCount; row++) {
3246 
3247         QModelIndex headerIndex = model->index(row, 0, treeView->rootIndex());
3248         QString strData = model->data(headerIndex).toString().toHtmlEscaped();
3249         out << QString("<h1>%1</h1>").arg(strData);
3250 
3251         // Open a new table for each group of objects
3252         out << "<table border=1 cellspacing=0 cellpadding=2><thead><tr bgcolor=\"#F0F0F0\">";
3253 
3254         for (int column = 0; column < columnCount; column++) {
3255             // Headers
3256             if (!treeView->isColumnHidden(column))
3257                 out << QString("<th>%1</th>").arg(model->headerData(column, Qt::Horizontal).toString().toHtmlEscaped());
3258         }
3259         out << "</tr></thead>";
3260 
3261         for (int column = 0; column < columnCount; column++) {
3262             QModelIndex groupIndex = model->index(row, column, treeView->rootIndex());
3263 
3264             // A row for the object name
3265             for (int rowChild = 0; rowChild < model->rowCount(groupIndex); rowChild++) {
3266                 QModelIndex objectIndex = model->index(rowChild, column, groupIndex);
3267                 out << "<tr>";
3268                 for (int column2 = 0; column2 < columnCount; column2++) {
3269                     if (!treeView->isColumnHidden(column2)) {
3270                         QModelIndex cellIndex = model->index(rowChild, column2, groupIndex);
3271                         QString header_data = model->data(cellIndex).toString().toHtmlEscaped();
3272                         if (column2 != DbStructureModel::ColumnSQL)
3273                             out << QString("<td><h2>%1</h2></td>").arg((!header_data.isEmpty()) ? header_data : QString("&nbsp;"));
3274                         else
3275                             out << QString("<td><pre>%1</pre></td>").arg((!header_data.isEmpty()) ? header_data : QString("&nbsp;"));
3276                     }
3277                 }
3278                 out << "</tr>";
3279 
3280                 // One row for each object's fields
3281                 for (int rowChild2 = 0; rowChild2 < model->rowCount(objectIndex); rowChild2++) {
3282                     out << "<tr>";
3283                     for (int column2 = 0; column2 < columnCount; column2++) {
3284                         if (!treeView->isColumnHidden(column2)) {
3285                             QModelIndex fieldIndex = model->index(rowChild2, column2, objectIndex);
3286                             QString field_data = model->data(fieldIndex).toString().toHtmlEscaped();
3287                             out << QString("<td>%1</td>").arg((!field_data.isEmpty()) ? field_data : QString("&nbsp;"));
3288                         }
3289                     }
3290                     out << "</tr>";
3291                 }
3292             }
3293         }
3294         out << "</table>";
3295     }
3296     out << "</body></html>";
3297 
3298     QPrinter printer;
3299     printer.setDocName(treeView->windowTitle());
3300 
3301     QPrintPreviewDialog *dialog = new QPrintPreviewDialog(&printer);
3302     connect(dialog, &QPrintPreviewDialog::paintRequested, [strStream](QPrinter *previewPrinter) {
3303         QTextDocument document;
3304         document.setHtml(strStream);
3305         document.print(previewPrinter);
3306     });
3307 
3308     dialog->exec();
3309     delete dialog;
3310 
3311 }
3312 
updateDatabaseBusyStatus(bool busy,const QString & user)3313 void MainWindow::updateDatabaseBusyStatus(bool busy, const QString& user)
3314 {
3315     statusBusyLabel->setText(tr("Busy (%1)").arg(user));
3316     statusBusyLabel->setVisible(busy);
3317     statusStopButton->setVisible(busy);
3318 }
3319 
3320 
closeTab(int index)3321 void MainWindow::closeTab(int index)
3322 {
3323     ui->mainTab->removeTab(index);
3324 }
3325 
toggleTabVisible(QWidget * tabWidget,bool show)3326 void MainWindow::toggleTabVisible(QWidget* tabWidget, bool show)
3327 {
3328     if (show)
3329         ui->mainTab->addTab(tabWidget, tabWidget->accessibleName());
3330     else
3331         ui->mainTab->removeTab(ui->mainTab->indexOf(tabWidget));
3332 }
3333 
restoreOpenTabs(QString tabs)3334 void MainWindow::restoreOpenTabs(QString tabs)
3335 {
3336     // Split the tab list, skiping the empty parts so the empty string turns to an empty list
3337     // and not a list of one empty string.
3338     QStringList tabList = tabs.split(' ', QString::SkipEmptyParts);
3339 
3340     // Clear the tabs and then add them in the order specified by the setting.
3341     // Use the accessibleName attribute for restoring the tab label.
3342     if (!tabList.isEmpty()) {
3343         // Avoid flickering while clearing and adding tabs.
3344         ui->mainTab->setUpdatesEnabled(false);
3345         ui->mainTab->clear();
3346         for (const auto& objectName : tabList) {
3347             for (QWidget* widget : {ui->structure, ui->browser, ui->pragmas, ui->query})
3348                 if (widget->objectName() == objectName) {
3349                     ui->mainTab->addTab(widget, widget->accessibleName());
3350                     break;
3351                 }
3352         }
3353         ui->mainTab->setUpdatesEnabled(true);
3354         // Force the update of the View menu toggable entries
3355         // (it doesn't seem to be a better way)
3356         emit ui->mainTab->tabCloseRequested(-1);
3357     }
3358 }
3359 
saveOpenTabs()3360 QString MainWindow::saveOpenTabs()
3361 {
3362     QString openTabs;
3363     for (int i=0; i < ui->mainTab->count(); i++)
3364         openTabs.append(ui->mainTab->widget(i)->objectName() + ' ');
3365     openTabs.chop(1);
3366     return openTabs;
3367 }
3368 
showStatusMessage5s(QString message)3369 void MainWindow::showStatusMessage5s(QString message)
3370 {
3371     ui->statusbar->showMessage(message, 5000);
3372 }
3373 
saveAll()3374 void MainWindow::saveAll()
3375 {
3376     for(int i=0; i<ui->tabSqlAreas->count(); i++) {
3377         SqlExecutionArea* sqlExecArea = qobject_cast<SqlExecutionArea*>(ui->tabSqlAreas->widget(i));
3378         if(sqlExecArea->getEditor()->isModified() && !sqlExecArea->fileName().isEmpty())
3379             saveSqlFile(i);
3380     }
3381     if(!currentProjectFilename.isEmpty())
3382         saveProject();
3383     fileSave();
3384 
3385 }
3386 
showContextMenuSqlTabBar(const QPoint & pos)3387 void MainWindow::showContextMenuSqlTabBar(const QPoint& pos)
3388 {
3389     // Don't show context menu if the mouse click was outside of all the tabs
3390     int tab = ui->tabSqlAreas->tabBar()->tabAt(pos);
3391     if(tab == -1)
3392         return;
3393 
3394     // Prepare all menu actions
3395     QAction* actionRename = new QAction(this);
3396     actionRename->setText(tr("Rename Tab"));
3397     connect(actionRename, &QAction::triggered, [this, tab]() {
3398         renameSqlTab(tab);
3399     });
3400 
3401     QAction* actionDuplicate = new QAction(this);
3402     actionDuplicate->setText(tr("Duplicate Tab"));
3403     connect(actionDuplicate, &QAction::triggered, [this, tab]() {
3404         QString tab_name = ui->tabSqlAreas->tabText(tab).remove("&").remove(QRegExp(" \\(\\d+\\)$"));
3405         QString new_tab_name;
3406         for(int i=1;;i++)
3407         {
3408             new_tab_name = tab_name + QString(" (%1)").arg(i);
3409             bool name_already_exists = false;
3410             for(int j=0;j<ui->tabSqlAreas->count();j++)
3411             {
3412                 if(ui->tabSqlAreas->tabText(j).remove("&") == new_tab_name)
3413                 {
3414                     name_already_exists = true;
3415                     break;
3416                 }
3417             }
3418 
3419             if(!name_already_exists)
3420                 break;
3421         }
3422 
3423         int new_tab = openSqlTab();
3424         ui->tabSqlAreas->setTabText(new_tab, new_tab_name);
3425 
3426         SqlExecutionArea* old_area = qobject_cast<SqlExecutionArea*>(ui->tabSqlAreas->widget(tab));
3427         SqlExecutionArea* new_area = qobject_cast<SqlExecutionArea*>(ui->tabSqlAreas->widget(new_tab));
3428         new_area->setSql(old_area->getSql());
3429     });
3430 
3431     QAction* actionClose = new QAction(this);
3432     actionClose->setText(tr("Close Tab"));
3433     actionClose->setShortcut(tr("Ctrl+W"));
3434     connect(actionClose, &QAction::triggered, [this, tab]() {
3435         closeSqlTab(tab);
3436     });
3437 
3438     // Show menu
3439     QMenu* menuTabs = new QMenu(this);
3440     menuTabs->addAction(actionRename);
3441     menuTabs->addAction(actionDuplicate);
3442     menuTabs->addAction(actionClose);
3443     menuTabs->exec(ui->tabSqlAreas->mapToGlobal(pos));
3444 }
3445 
openUrlOrFile(const QString & urlString)3446 void MainWindow::openUrlOrFile(const QString& urlString)
3447 {
3448     QUrl url = QUrl::fromUserInput(urlString, QFileInfo(db.currentFile()).path(), QUrl::AssumeLocalFile);
3449     if(url.isValid()) {
3450         if(QDesktopServices::openUrl(url))
3451             showStatusMessage5s(tr("Opening '%1'...").arg(url.toDisplayString()));
3452         else
3453             showStatusMessage5s(tr("There was an error opening '%1'...").arg(url.toDisplayString()));
3454 
3455     } else
3456         showStatusMessage5s(tr("Value is not a valid URL or filename: %1").arg(url.errorString()));
3457 }
3458 
focusSqlEditor()3459 void MainWindow::focusSqlEditor()
3460 {
3461     SqlExecutionArea* sqlArea = qobject_cast<SqlExecutionArea*>(ui->tabSqlAreas->currentWidget());
3462     if(sqlArea)
3463         sqlArea->getEditor()->setFocus();
3464 }
3465 
moveDocksTo(Qt::DockWidgetArea area)3466 void MainWindow::moveDocksTo(Qt::DockWidgetArea area)
3467 {
3468     addDockWidget(area, ui->dockEdit);
3469     addDockWidget(area, ui->dockLog);
3470     tabifyDockWidget(ui->dockLog, ui->dockPlot);
3471     tabifyDockWidget(ui->dockLog, ui->dockSchema);
3472     tabifyDockWidget(ui->dockLog, ui->dockRemote);
3473 }
3474