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(" "));
3274 else
3275 out << QString("<td><pre>%1</pre></td>").arg((!header_data.isEmpty()) ? header_data : QString(" "));
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(" "));
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