1 /*
2    SPDX-FileCopyrightText: 2010 Marco Mentasti <marcomentasti@gmail.com>
3 
4    SPDX-License-Identifier: LGPL-2.0-only
5 */
6 
7 #include "dataoutputwidget.h"
8 #include "dataoutputmodel.h"
9 #include "dataoutputview.h"
10 #include "exportwizard.h"
11 
12 #include <ktexteditor/application.h>
13 #include <ktexteditor/document.h>
14 #include <ktexteditor/editor.h>
15 #include <ktexteditor/mainwindow.h>
16 #include <ktexteditor/view.h>
17 
18 #include <KLocalizedString>
19 #include <KMessageBox>
20 #include <KToggleAction>
21 #include <KToolBar>
22 #include <QAction>
23 
24 #include <QApplication>
25 #include <QClipboard>
26 #include <QElapsedTimer>
27 #include <QFile>
28 #include <QHeaderView>
29 #include <QLayout>
30 #include <QSize>
31 #include <QSqlError>
32 #include <QSqlQuery>
33 #include <QStyle>
34 #include <QTextStream>
35 #include <QTime>
36 #include <QTimer>
37 
DataOutputWidget(QWidget * parent)38 DataOutputWidget::DataOutputWidget(QWidget *parent)
39     : QWidget(parent)
40     , m_model(new DataOutputModel(this))
41     , m_view(new DataOutputView(this))
42     , m_isEmpty(true)
43 {
44     m_view->setModel(m_model);
45 
46     QHBoxLayout *layout = new QHBoxLayout(this);
47     m_dataLayout = new QVBoxLayout();
48 
49     KToolBar *toolbar = new KToolBar(this);
50     toolbar->setOrientation(Qt::Vertical);
51     toolbar->setToolButtonStyle(Qt::ToolButtonIconOnly);
52 
53     // ensure reasonable icons sizes, like e.g. the quick-open and co. icons
54     // the normal toolbar sizes are TOO large, e.g. for scaled stuff even more!
55     const int iconSize = style()->pixelMetric(QStyle::PM_ButtonIconSize, nullptr, this);
56     toolbar->setIconSize(QSize(iconSize, iconSize));
57 
58     /// TODO: disable actions if no results are displayed or selected
59 
60     QAction *action;
61 
62     action = new QAction(QIcon::fromTheme(QStringLiteral("distribute-horizontal-x")), i18nc("@action:intoolbar", "Resize columns to contents"), this);
63     toolbar->addAction(action);
64     connect(action, &QAction::triggered, this, &DataOutputWidget::resizeColumnsToContents);
65 
66     action = new QAction(QIcon::fromTheme(QStringLiteral("distribute-vertical-y")), i18nc("@action:intoolbar", "Resize rows to contents"), this);
67     toolbar->addAction(action);
68     connect(action, &QAction::triggered, this, &DataOutputWidget::resizeRowsToContents);
69 
70     action = new QAction(QIcon::fromTheme(QStringLiteral("edit-copy")), i18nc("@action:intoolbar", "Copy"), this);
71     toolbar->addAction(action);
72     m_view->addAction(action);
73     connect(action, &QAction::triggered, this, &DataOutputWidget::slotCopySelected);
74 
75     action = new QAction(QIcon::fromTheme(QStringLiteral("document-export-table")), i18nc("@action:intoolbar", "Export..."), this);
76     toolbar->addAction(action);
77     m_view->addAction(action);
78     connect(action, &QAction::triggered, this, &DataOutputWidget::slotExport);
79 
80     action = new QAction(QIcon::fromTheme(QStringLiteral("edit-clear")), i18nc("@action:intoolbar", "Clear"), this);
81     toolbar->addAction(action);
82     connect(action, &QAction::triggered, this, &DataOutputWidget::clearResults);
83 
84     toolbar->addSeparator();
85 
86     KToggleAction *toggleAction =
87         new KToggleAction(QIcon::fromTheme(QStringLiteral("applications-education-language")), i18nc("@action:intoolbar", "Use system locale"), this);
88     toolbar->addAction(toggleAction);
89     connect(toggleAction, &QAction::triggered, this, &DataOutputWidget::slotToggleLocale);
90 
91     m_dataLayout->addWidget(m_view);
92 
93     layout->addWidget(toolbar);
94     layout->addLayout(m_dataLayout);
95     layout->setContentsMargins(0, 0, 0, 0);
96 
97     setLayout(layout);
98 }
99 
~DataOutputWidget()100 DataOutputWidget::~DataOutputWidget()
101 {
102 }
103 
showQueryResultSets(QSqlQuery & query)104 void DataOutputWidget::showQueryResultSets(QSqlQuery &query)
105 {
106     /// TODO: loop resultsets if > 1
107     /// NOTE from Qt Documentation:
108     /// When one of the statements is a non-select statement a count of affected rows
109     /// may be available instead of a result set.
110 
111     if (!query.isSelect() || query.lastError().isValid()) {
112         return;
113     }
114 
115     m_model->setQuery(query);
116 
117     m_isEmpty = false;
118 
119     QTimer::singleShot(0, this, &DataOutputWidget::resizeColumnsToContents);
120 
121     raise();
122 }
123 
clearResults()124 void DataOutputWidget::clearResults()
125 {
126     // avoid crash when calling QSqlQueryModel::clear() after removing connection from the QSqlDatabase list
127     if (m_isEmpty) {
128         return;
129     }
130 
131     m_model->clear();
132 
133     m_isEmpty = true;
134 
135     /// HACK needed to refresh headers. please correct if there's a better way
136     m_view->horizontalHeader()->hide();
137     m_view->verticalHeader()->hide();
138 
139     m_view->horizontalHeader()->show();
140     m_view->verticalHeader()->show();
141 }
142 
resizeColumnsToContents()143 void DataOutputWidget::resizeColumnsToContents()
144 {
145     if (m_model->rowCount() == 0) {
146         return;
147     }
148 
149     m_view->resizeColumnsToContents();
150 }
151 
resizeRowsToContents()152 void DataOutputWidget::resizeRowsToContents()
153 {
154     if (m_model->rowCount() == 0) {
155         return;
156     }
157 
158     m_view->resizeRowsToContents();
159 
160     int h = m_view->rowHeight(0);
161 
162     if (h > 0) {
163         m_view->verticalHeader()->setDefaultSectionSize(h);
164     }
165 }
166 
slotToggleLocale()167 void DataOutputWidget::slotToggleLocale()
168 {
169     m_model->setUseSystemLocale(!m_model->useSystemLocale());
170 }
171 
slotCopySelected()172 void DataOutputWidget::slotCopySelected()
173 {
174     if (m_model->rowCount() <= 0) {
175         return;
176     }
177 
178     while (m_model->canFetchMore()) {
179         m_model->fetchMore();
180     }
181 
182     if (!m_view->selectionModel()->hasSelection()) {
183         m_view->selectAll();
184     }
185 
186     QString text;
187     QTextStream stream(&text);
188 
189     exportData(stream);
190 
191     if (!text.isEmpty()) {
192         QApplication::clipboard()->setText(text);
193     }
194 }
195 
slotExport()196 void DataOutputWidget::slotExport()
197 {
198     if (m_model->rowCount() <= 0) {
199         return;
200     }
201 
202     while (m_model->canFetchMore()) {
203         m_model->fetchMore();
204     }
205 
206     if (!m_view->selectionModel()->hasSelection()) {
207         m_view->selectAll();
208     }
209 
210     ExportWizard wizard(this);
211 
212     if (wizard.exec() != QDialog::Accepted) {
213         return;
214     }
215 
216     bool outputInDocument = wizard.field(QStringLiteral("outDocument")).toBool();
217     bool outputInClipboard = wizard.field(QStringLiteral("outClipboard")).toBool();
218     bool outputInFile = wizard.field(QStringLiteral("outFile")).toBool();
219 
220     bool exportColumnNames = wizard.field(QStringLiteral("exportColumnNames")).toBool();
221     bool exportLineNumbers = wizard.field(QStringLiteral("exportLineNumbers")).toBool();
222 
223     Options opt = NoOptions;
224 
225     if (exportColumnNames) {
226         opt |= ExportColumnNames;
227     }
228     if (exportLineNumbers) {
229         opt |= ExportLineNumbers;
230     }
231 
232     bool quoteStrings = wizard.field(QStringLiteral("checkQuoteStrings")).toBool();
233     bool quoteNumbers = wizard.field(QStringLiteral("checkQuoteNumbers")).toBool();
234 
235     QChar stringsQuoteChar = (quoteStrings) ? wizard.field(QStringLiteral("quoteStringsChar")).toString().at(0) : QLatin1Char('\0');
236     QChar numbersQuoteChar = (quoteNumbers) ? wizard.field(QStringLiteral("quoteNumbersChar")).toString().at(0) : QLatin1Char('\0');
237 
238     QString fieldDelimiter = wizard.field(QStringLiteral("fieldDelimiter")).toString();
239 
240     if (outputInDocument) {
241         KTextEditor::MainWindow *mw = KTextEditor::Editor::instance()->application()->activeMainWindow();
242         KTextEditor::View *kv = mw->activeView();
243 
244         if (!kv) {
245             return;
246         }
247 
248         QString text;
249         QTextStream stream(&text);
250 
251         exportData(stream, stringsQuoteChar, numbersQuoteChar, fieldDelimiter, opt);
252 
253         kv->insertText(text);
254         kv->setFocus();
255     } else if (outputInClipboard) {
256         QString text;
257         QTextStream stream(&text);
258 
259         exportData(stream, stringsQuoteChar, numbersQuoteChar, fieldDelimiter, opt);
260 
261         QApplication::clipboard()->setText(text);
262     } else if (outputInFile) {
263         QString url = wizard.field(QStringLiteral("outFileUrl")).toString();
264         QFile data(url);
265         if (data.open(QFile::WriteOnly | QFile::Truncate)) {
266             QTextStream stream(&data);
267 
268             exportData(stream, stringsQuoteChar, numbersQuoteChar, fieldDelimiter, opt);
269 
270             stream.flush();
271         } else {
272             KMessageBox::error(this, xi18nc("@info", "Unable to open file <filename>%1</filename>", url));
273         }
274     }
275 }
276 
exportData(QTextStream & stream,const QChar stringsQuoteChar,const QChar numbersQuoteChar,const QString & fieldDelimiter,const Options opt)277 void DataOutputWidget::exportData(QTextStream &stream,
278                                   const QChar stringsQuoteChar,
279                                   const QChar numbersQuoteChar,
280                                   const QString &fieldDelimiter,
281                                   const Options opt)
282 {
283     QItemSelectionModel *selectionModel = m_view->selectionModel();
284 
285     if (!selectionModel->hasSelection()) {
286         return;
287     }
288 
289     QString fixedFieldDelimiter = fieldDelimiter;
290 
291     /// FIXME: ugly workaround...
292     fixedFieldDelimiter.replace(QLatin1String("\\t"), QLatin1String("\t"));
293     fixedFieldDelimiter.replace(QLatin1String("\\r"), QLatin1String("\r"));
294     fixedFieldDelimiter.replace(QLatin1String("\\n"), QLatin1String("\n"));
295 
296     QElapsedTimer t;
297     t.start();
298 
299     QSet<int> columns;
300     QSet<int> rows;
301     QHash<QPair<int, int>, QString> snapshot;
302 
303     const QModelIndexList selectedIndexes = selectionModel->selectedIndexes();
304 
305     snapshot.reserve(selectedIndexes.count());
306 
307     for (const QModelIndex &index : selectedIndexes) {
308         const QVariant data = index.data(Qt::UserRole);
309 
310         const int col = index.column();
311         const int row = index.row();
312 
313         if (!columns.contains(col)) {
314             columns.insert(col);
315         }
316         if (!rows.contains(row)) {
317             rows.insert(row);
318         }
319 
320         if (data.type() < 7) // is numeric or boolean
321         {
322             if (numbersQuoteChar != QLatin1Char('\0')) {
323                 snapshot[qMakePair(row, col)] = numbersQuoteChar + data.toString() + numbersQuoteChar;
324             } else {
325                 snapshot[qMakePair(row, col)] = data.toString();
326             }
327         } else {
328             if (stringsQuoteChar != QLatin1Char('\0')) {
329                 snapshot[qMakePair(row, col)] = stringsQuoteChar + data.toString() + stringsQuoteChar;
330             } else {
331                 snapshot[qMakePair(row, col)] = data.toString();
332             }
333         }
334     }
335 
336     if (opt.testFlag(ExportColumnNames)) {
337         if (opt.testFlag(ExportLineNumbers)) {
338             stream << fixedFieldDelimiter;
339         }
340 
341         QSetIterator<int> j(columns);
342         while (j.hasNext()) {
343             const QVariant data = m_model->headerData(j.next(), Qt::Horizontal);
344 
345             if (stringsQuoteChar != QLatin1Char('\0')) {
346                 stream << stringsQuoteChar + data.toString() + stringsQuoteChar;
347             } else {
348                 stream << data.toString();
349             }
350 
351             if (j.hasNext()) {
352                 stream << fixedFieldDelimiter;
353             }
354         }
355         stream << "\n";
356     }
357 
358     for (const int row : qAsConst(rows)) {
359         if (opt.testFlag(ExportLineNumbers)) {
360             stream << row + 1 << fixedFieldDelimiter;
361         }
362 
363         QSetIterator<int> j(columns);
364         while (j.hasNext()) {
365             stream << snapshot.value(qMakePair(row, j.next()));
366 
367             if (j.hasNext()) {
368                 stream << fixedFieldDelimiter;
369             }
370         }
371         stream << "\n";
372     }
373 
374     qDebug() << "Export in" << t.elapsed() << "msecs";
375 }
376