1 #include "SqlExecutionArea.h"
2 #include "ui_SqlExecutionArea.h"
3 #include "sqltextedit.h"
4 #include "sqlitetablemodel.h"
5 #include "sqlitedb.h"
6 #include "Settings.h"
7 #include "ExportDataDialog.h"
8 #include "FilterTableHeader.h"
9 
10 #include <QInputDialog>
11 #include <QMessageBox>
12 #include <QShortcut>
13 #include <QFile>
14 
SqlExecutionArea(DBBrowserDB & _db,QWidget * parent)15 SqlExecutionArea::SqlExecutionArea(DBBrowserDB& _db, QWidget* parent) :
16     QWidget(parent),
17     db(_db),
18     ui(new Ui::SqlExecutionArea),
19     m_columnsResized(false),
20     error_state(false)
21 {
22     // Create UI
23     ui->setupUi(this);
24 
25     // Create model
26     model = new SqliteTableModel(db, this);
27     ui->tableResult->setModel(model);
28     connect(model, &SqliteTableModel::finishedFetch, this, &SqlExecutionArea::fetchedData);
29     connect(ui->tableResult->filterHeader(), &FilterTableHeader::sectionPressed, ui->tableResult, &QTableView::selectColumn);
30 
31     ui->findFrame->hide();
32 
33     QShortcut* shortcutHideFind = new QShortcut(QKeySequence("ESC"), ui->findLineEdit);
34     connect(shortcutHideFind, &QShortcut::activated, this, &SqlExecutionArea::hideFindFrame);
35 
36     connect(ui->findLineEdit, &QLineEdit::textChanged, this, &SqlExecutionArea::findLineEdit_textChanged);
37     connect(ui->previousToolButton, &QToolButton::clicked, this, &SqlExecutionArea::findPrevious);
38     connect(ui->nextToolButton, &QToolButton::clicked, this, &SqlExecutionArea::findNext);
39     connect(ui->findLineEdit, &QLineEdit::returnPressed, this, &SqlExecutionArea::findNext);
40     connect(ui->hideFindButton, &QToolButton::clicked, this, &SqlExecutionArea::hideFindFrame);
41 
42     connect(&fileSystemWatch, &QFileSystemWatcher::fileChanged, this, &SqlExecutionArea::fileChanged);
43 
44     // Save to settings when sppliter is moved, but only to memory.
45     connect(ui->splitter, &QSplitter::splitterMoved, this,  [this]() {
46             Settings::setValue("editor", "splitter1_sizes", ui->splitter->saveState(), /* dont_save_to_disk */ true);
47         });
48     connect(ui->splitter_2, &QSplitter::splitterMoved, this, [this]() {
49             Settings::setValue("editor", "splitter2_sizes", ui->splitter_2->saveState(), /* dont_save_to_disk */ true);
50         });
51 
52     // Set collapsible the editErrors panel
53     ui->splitter_2->setCollapsible(1, true);
54 
55     // Load settings
56     reloadSettings();
57 }
58 
~SqlExecutionArea()59 SqlExecutionArea::~SqlExecutionArea()
60 {
61     delete ui;
62 }
63 
getSql() const64 QString SqlExecutionArea::getSql() const
65 {
66     return ui->editEditor->text();
67 }
68 
getSelectedSql() const69 QString SqlExecutionArea::getSelectedSql() const
70 {
71     return ui->editEditor->selectedText().trimmed().replace(QChar(0x2029), '\n');
72 }
73 
setSql(const QString & sql)74 void SqlExecutionArea::setSql(const QString& sql)
75 {
76     ui->editEditor->setText(sql);
77 }
78 
finishExecution(const QString & result,const bool ok)79 void SqlExecutionArea::finishExecution(const QString& result, const bool ok)
80 {
81     error_state = !ok;
82     m_columnsResized = false;
83     ui->editErrors->setPlainText(result);
84     // Set reddish background when not ok
85     if (showErrorIndicators)
86     {
87         if (ok)
88             ui->editErrors->setStyleSheet("");
89         else
90             ui->editErrors->setStyleSheet("QTextEdit {color: white; background-color: rgb(255, 102, 102)}");
91     }
92 
93 }
94 
fetchedData()95 void SqlExecutionArea::fetchedData()
96 {
97     // Don't resize the columns more than once to fit their contents. This is necessary because the finishedFetch signal of the model
98     // is emitted for each loaded prefetch block and we want to avoid resizes while scrolling down.
99     if(m_columnsResized)
100         return;
101     m_columnsResized = true;
102 
103     // Set column widths according to their contents but make sure they don't exceed a certain size
104     ui->tableResult->resizeColumnsToContents();
105     for(int i = 0; i < model->columnCount(); i++)
106     {
107         if(ui->tableResult->columnWidth(i) > 300)
108             ui->tableResult->setColumnWidth(i, 300);
109     }
110 }
111 
getEditor()112 SqlTextEdit *SqlExecutionArea::getEditor()
113 {
114     return ui->editEditor;
115 }
116 
getTableResult()117 ExtendedTableWidget *SqlExecutionArea::getTableResult()
118 {
119     return ui->tableResult;
120 }
121 
getStatusEdit()122 QTextEdit* SqlExecutionArea::getStatusEdit()
123 {
124     return ui->editErrors;
125 }
126 
saveAsCsv()127 void SqlExecutionArea::saveAsCsv()
128 {
129     ExportDataDialog dialog(db, ExportDataDialog::ExportFormatCsv, this, model->query());
130     dialog.exec();
131 }
132 
reloadSettings()133 void SqlExecutionArea::reloadSettings()
134 {
135     // Reload editor and table settings
136     ui->editEditor->reloadSettings();
137     ui->tableResult->reloadSettings();
138 
139     // Set font
140     QFont logfont(Settings::getValue("editor", "font").toString());
141     logfont.setStyleHint(QFont::TypeWriter);
142     logfont.setPointSize(Settings::getValue("log", "fontsize").toInt());
143     ui->editErrors->setFont(logfont);
144 
145     // Apply horizontal/vertical tiling option
146     if(Settings::getValue("editor", "horizontal_tiling").toBool())
147         ui->splitter->setOrientation(Qt::Horizontal);
148     else
149         ui->splitter->setOrientation(Qt::Vertical);
150 
151     ui->splitter->restoreState(Settings::getValue("editor", "splitter1_sizes").toByteArray());
152     ui->splitter_2->restoreState(Settings::getValue("editor", "splitter2_sizes").toByteArray());
153 
154     // Reload model settings
155     model->reloadSettings();
156 
157     // Check if error indicators are enabled for the not-ok background clue
158     showErrorIndicators = Settings::getValue("editor", "error_indicators").toBool();
159     if (!showErrorIndicators)
160         ui->editErrors->setStyleSheet("");
161 
162 }
163 
find(QString expr,bool forward)164 void SqlExecutionArea::find(QString expr, bool forward)
165 {
166 
167     bool found = ui->editEditor->findText
168       (expr,
169        ui->regexpCheckBox->isChecked(),
170        ui->caseCheckBox->isChecked(),
171        ui->wholeWordsCheckBox->isChecked(),
172        /* wrap */ true,
173        forward);
174 
175     // Set reddish background when not found
176     if (found || expr.isEmpty())
177         ui->findLineEdit->setStyleSheet("");
178     else
179         ui->findLineEdit->setStyleSheet("QLineEdit {color: white; background-color: rgb(255, 102, 102)}");
180 
181 }
182 
findPrevious()183 void SqlExecutionArea::findPrevious()
184 {
185     find(ui->findLineEdit->text(), false);
186 }
187 
findNext()188 void SqlExecutionArea::findNext()
189 {
190     find(ui->findLineEdit->text(), true);
191 }
192 
findLineEdit_textChanged(const QString &)193 void SqlExecutionArea::findLineEdit_textChanged(const QString &)
194 {
195     // When the text changes, perform an incremental search from cursor
196     // position, or from begin of the selection position.
197 
198     // For incremental search while typing we need to start from the
199     // begining of the current selection, otherwise we'd jump to the
200     // next occurrence
201     if (ui->editEditor->hasSelectedText()) {
202         int lineFrom;
203         int indexFrom;
204         int lineTo;
205         int indexTo;
206         ui->editEditor->getSelection(&lineFrom, &indexFrom, &lineTo, &indexTo);
207         ui->editEditor->setCursorPosition(lineFrom, indexFrom);
208     }
209 
210     find(ui->findLineEdit->text(), true);
211 }
212 
hideFindFrame()213 void SqlExecutionArea::hideFindFrame()
214 {
215     ui->editEditor->setFocus();
216     ui->findFrame->hide();
217     emit findFrameVisibilityChanged(false);
218 }
219 
setFindFrameVisibility(bool show)220 void SqlExecutionArea::setFindFrameVisibility(bool show)
221 {
222     if (show) {
223         ui->findFrame->show();
224         ui->findLineEdit->setFocus();
225         ui->findLineEdit->selectAll();
226         emit findFrameVisibilityChanged(true);
227     } else {
228         hideFindFrame();
229     }
230 }
231 
openFile(const QString & filename)232 void SqlExecutionArea::openFile(const QString& filename)
233 {
234     // Open file for reading
235     QFile f(filename);
236     f.open(QIODevice::ReadOnly);
237     if(!f.isOpen())
238     {
239         QMessageBox::warning(this, qApp->applicationName(), tr("Couldn't read file: %1.").arg(f.errorString()));
240         return;
241     }
242 
243     // Read in the entire file
244     ui->editEditor->setText(f.readAll());
245 
246     // No modifications yet
247     ui->editEditor->setModified(false);
248 
249     // Remember file name
250     sqlFileName = filename;
251 
252     // Start watching this file for changes and unwatch the previously watched file, if any
253     if(!fileSystemWatch.files().empty())
254         fileSystemWatch.removePaths(fileSystemWatch.files());
255     fileSystemWatch.addPath(filename);
256 }
257 
saveFile(const QString & filename)258 void SqlExecutionArea::saveFile(const QString& filename)
259 {
260     // Unwatch all files now. By unwathing them before the actual saving, we are not notified of our own changes
261     if(!fileSystemWatch.files().empty())
262         fileSystemWatch.removePaths(fileSystemWatch.files());
263 
264     // Open file for writing
265     QFile f(filename);
266     f.open(QIODevice::WriteOnly);
267     if(!f.isOpen())
268     {
269         QMessageBox::warning(this, qApp->applicationName(), tr("Couldn't save file: %1.").arg(f.errorString()));
270         return;
271     }
272 
273     // Write to the file
274     if(f.write(getSql().toUtf8()) != -1)
275     {
276         // Close file now. If we let the destructor close it, we can get change notifications.
277         f.close();
278         // Set modified to false so we can get control of unsaved changes when closing.
279         ui->editEditor->setModified(false);
280 
281         // Remember file name
282         sqlFileName = filename;
283 
284         // Start watching this file
285         fileSystemWatch.addPath(filename);
286     } else {
287         QMessageBox::warning(this, qApp->applicationName(), tr("Couldn't save file: %1.").arg(f.errorString()));
288         return;
289     }
290 }
291 
fileChanged(const QString & filename)292 void SqlExecutionArea::fileChanged(const QString& filename)
293 {
294     // Check if there are unsaved changes in the file
295     QString changes;
296     if(ui->editEditor->isModified())
297         changes = QString(" ") + tr("Your changes will be lost when reloading it!");
298 
299     // Ask user whether to realod the modified file
300     if(QMessageBox::question(
301                 this,
302                 qApp->applicationName(),
303                 tr("The file \"%1\" was modified by another program. Do you want to reload it?%2").arg(filename, changes),
304                 QMessageBox::Yes | QMessageBox::Ignore) == QMessageBox::Yes)
305     {
306         // Read in the file
307         openFile(filename);
308     } else {
309         // The file does not match the file on the disk anymore. So set the modified flag
310         ui->editEditor->setModified(true);
311     }
312 }
313 
saveState()314 void SqlExecutionArea::saveState() {
315 
316     // Save to disk last stored splitter sizes
317     Settings::setValue("editor", "splitter1_sizes", Settings::getValue("editor", "splitter1_sizes"));
318     Settings::setValue("editor", "splitter2_sizes", Settings::getValue("editor", "splitter2_sizes"));
319 }
320