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