1 /* This file is part of the KDE project
2 Copyright 2010 Stefan Nikolaus <stefan.nikolaus@kdemail.net>
3 Copyright (C) 2002-2003 Norbert Andres <nandres@web.de>
4 (C) 2002 Ariya Hidayat <ariya@kde.org>
5 (C) 2002 Philipp Mueller <philipp.mueller@gmx.de>
6 (C) 2002 John Dailey <dailey@vt.edu>
7 (C) 2000-2001 Werner Trobin <trobin@kde.org>
8 (C) 2000-2001 Laurent Montel <montel@kde.org>
9 (C) 1999-2002 David Faure <faure@kde.org>
10 (C) 1999 Stephan Kulow <coolo@kde.org>
11 (C) 1999 Reginald Stadlbauer <reggie@kde.org>
12 (C) 1998-1999 Torben Weis <weis@kde.org>
13
14 This library is free software; you can redistribute it and/or
15 modify it under the terms of the GNU Library General Public
16 License as published by the Free Software Foundation; either
17 version 2 of the License, or (at your option) any later version.
18
19 This library is distributed in the hope that it will be useful,
20 but WITHOUT ANY WARRANTY; without even the implied warranty of
21 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
22 Library General Public License for more details.
23
24 You should have received a copy of the GNU Library General Public License
25 along with this library; see the file COPYING.LIB. If not, write to
26 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
27 Boston, MA 02110-1301, USA.
28 */
29
30 // Local
31 #include "ConsolidateDialog.h"
32
33 //Qt Includes
34 #include <QCheckBox>
35 #include <QGridLayout>
36 #include <QLabel>
37 #include <QPushButton>
38 #include <QListWidget>
39
40 // KF5
41 #include <kmessagebox.h>
42
43 #include <KoIcon.h>
44
45 // Sheets
46 #include "SheetsDebug.h"
47 #include <Global.h>
48 #include "calligra_sheets_limits.h"
49 #include <Localization.h>
50 #include <Map.h>
51 #include "ui/Selection.h"
52 #include <Sheet.h>
53 #include <Util.h>
54
55 #include <Formula.h>
56 #include <ValueConverter.h>
57 #include <Cell.h>
58
59 #include "commands/DataManipulators.h"
60
61 #include "ui_ConsolidateWidget.h"
62 #include "ui_ConsolidateDetailsWidget.h"
63
64 using namespace Calligra::Sheets;
65
66 class ConsolidateDialog::Private
67 {
68 public:
69 Selection *selection;
70 Ui::ConsolidateWidget mainWidget;
71 Ui::ConsolidateDetailsWidget detailsWidget;
72
73 public:
74 void setContent(Sheet *sheet, int row, int column, const QString &text, KUndo2Command *parent);
75 };
76
77
ConsolidateDialog(QWidget * parent,Selection * selection)78 ConsolidateDialog::ConsolidateDialog(QWidget* parent, Selection* selection)
79 : KoDialog(parent)
80 , d(new Private)
81 {
82 d->selection = selection;
83
84 setCaption(i18n("Consolidate"));
85 setModal(false);
86 setAttribute(Qt::WA_DeleteOnClose);
87 setButtons(Ok | Cancel | Details);
88 enableButton(Ok, false);
89
90 QWidget *widget = new QWidget(this);
91 d->mainWidget.setupUi(widget);
92 setMainWidget(widget);
93
94 widget = new QWidget(this);
95 d->detailsWidget.setupUi(widget);
96 setDetailsWidget(widget);
97
98 // ui refinements Designer is not capable of
99 d->mainWidget.m_addButton->setIcon(koIcon("list-add"));
100 d->mainWidget.m_removeButton->setIcon(koIcon("list-remove"));
101
102 d->mainWidget.m_function->addItem(i18n("Sum"), "SUM");
103 d->mainWidget.m_function->addItem(i18n("Average"), "AVERAGE");
104 d->mainWidget.m_function->addItem(i18n("Count"), "COUNT");
105 d->mainWidget.m_function->addItem(i18n("Max"), "MAX");
106 d->mainWidget.m_function->addItem(i18n("Min"), "MIN");
107 d->mainWidget.m_function->addItem(i18n("Product"), "PRODUCT");
108 d->mainWidget.m_function->addItem(i18n("Standard Deviation"), "STDDEV");
109 d->mainWidget.m_function->addItem(i18n("Variance"), "VAR");
110
111 connect(d->mainWidget.m_addButton, SIGNAL(clicked()),
112 this, SLOT(slotAdd()));
113 connect(d->mainWidget.m_removeButton, SIGNAL(clicked()),
114 this, SLOT(slotRemove()));
115 connect(d->mainWidget.m_sourceRange, SIGNAL(returnPressed()),
116 this, SLOT(slotReturnPressed()));
117
118 connect(d->selection, SIGNAL(changed(Region)),
119 this, SLOT(slotSelectionChanged()));
120 }
121
~ConsolidateDialog()122 ConsolidateDialog::~ConsolidateDialog()
123 {
124 delete d;
125 }
126
accept()127 void ConsolidateDialog::accept()
128 {
129 // Quit, if there are no source ranges.
130 if (d->mainWidget.m_sourceRanges->count() == 0) {
131 KMessageBox::error(this, i18n("You have to define at least one source range."));
132 return;
133 }
134
135 Map *const map = d->selection->activeSheet()->map();
136 ValueConverter *const converter = map->converter();
137
138 Sheet *const destinationSheet = d->selection->activeSheet();
139 int dx = d->selection->lastRange().left();
140 int dy = d->selection->lastRange().top();
141
142 const int index = d->mainWidget.m_function->currentIndex();
143 const QString function = d->mainWidget.m_function->itemData(index).toString();
144
145 QList<Region> ranges;
146 for (int i = 0; i < d->mainWidget.m_sourceRanges->count(); ++i) {
147 const QString address = d->mainWidget.m_sourceRanges->item(i)->text();
148 const Region region(address, map, destinationSheet);
149 if (!region.isValid()) {
150 KMessageBox::error(this, i18n("%1 is not a valid cell range." , address));
151 return;
152 }
153 ranges.append(region);
154 }
155
156 enum { D_ROW, D_COL, D_NONE, D_BOTH } desc;
157 if (d->detailsWidget.m_rowHeader->isChecked() && d->detailsWidget.m_columnHeader->isChecked())
158 desc = D_BOTH;
159 else if (d->detailsWidget.m_rowHeader->isChecked())
160 desc = D_ROW;
161 else if (d->detailsWidget.m_columnHeader->isChecked())
162 desc = D_COL;
163 else
164 desc = D_NONE;
165
166 const QRect firstRange = ranges[0].firstRange();
167 const int columns = firstRange.width();
168 const int rows = firstRange.height();
169
170 // Check, if the first cell range has a sufficient size.
171 if (columns <= ((desc == D_BOTH || desc == D_COL) ? 1 : 0) ||
172 rows <= ((desc == D_BOTH || desc == D_ROW) ? 1 : 0)) {
173 KMessageBox::error(this, i18n("The range\n%1\nis too small", ranges[0].name()));
174 return;
175 }
176
177 // Check, if the first cell range is too large.
178 if (ranges[0].isAllSelected() || ranges[0].isColumnOrRowSelected()) {
179 KMessageBox::error(this, i18n("The range\n%1\nis too large" , ranges[0].name()));
180 return;
181 }
182
183 // Check the other cell ranges.
184 for (int i = 1; i < ranges.count(); ++i) {
185 const int columns2 = ranges[i].firstRange().width();
186 const int rows2 = ranges[i].firstRange().height();
187
188 // Check, if the cell range is too large.
189 if (ranges[i].isAllSelected() || ranges[i].isColumnOrRowSelected()) {
190 KMessageBox::error(this, i18n("The range\n%1\nis too large" , ranges[i].name()));
191 return;
192 }
193
194 // Check whether all ranges...
195 // - have the same size, if no headers are given.
196 // - have the same amount of rows, if only column headers are given
197 // - have the same amount of columns, if only row headers are given
198 if ((desc == D_NONE && (columns != columns2 || rows != rows2)) ||
199 (desc == D_COL && rows != rows2) ||
200 (desc == D_ROW && columns != columns2)) {
201 QString tmp = i18n("The ranges\n%1\nand\n%2\nhave different size", ranges[0].name() , ranges[i].name());
202 KMessageBox::error(this, tmp);
203 return;
204 }
205 }
206
207 // Create the cumulating parent command.
208 KUndo2Command *const command = new KUndo2Command(kundo2_i18n("Consolidate"));
209
210 // Create the destination cell range
211 if (desc == D_NONE) {
212 // Check whether the destination is part of the source ...
213 const QRect destinationRange(dx, dy, columns, rows);
214 for (int i = 0; i < ranges.count(); ++i) {
215 Sheet *const sheet = ranges[i].firstSheet();
216 Q_ASSERT(sheet);
217 const QRect range = ranges[i].firstRange();
218
219 if (sheet == destinationSheet && range.intersects(destinationRange)) {
220 QString tmp(i18n("The source range intersects the destination range."));
221 KMessageBox::error(this, tmp);
222 return;
223 }
224 }
225
226 // Now create the destination cell range
227 for (int col = 0; col < columns; ++col) {
228 for (int row = 0; row < rows; ++row) {
229 bool novalue = true;
230 QString formula = '=' + function + '(';
231 for (int i = 0; i < ranges.count(); ++i) {
232 Sheet *const sheet = ranges[i].firstSheet();
233 Q_ASSERT(sheet);
234 const QRect range = ranges[i].firstRange();
235 const Cell cell(sheet, col + range.left(), row + range.top());
236 if (!cell.value().isEmpty())
237 novalue = false;
238 if (i != 0) {
239 formula += ';';
240 }
241 formula += (sheet != destinationSheet) ? cell.fullName() : cell.name();
242 }
243 formula += ')';
244
245 if (!novalue) {
246 d->setContent(destinationSheet, dy + row, dx + col, formula, command);
247 }
248 }
249 }
250 } else if (desc == D_COL) {
251 // Get list of all descriptions in the columns
252 QHash<QString, QList<Cell> > columnHeaderCells;
253 for (int i = 0; i < ranges.count(); ++i) {
254 Sheet *const sheet = ranges[i].firstSheet();
255 Q_ASSERT(sheet);
256 const QRect range = ranges[i].firstRange();
257 for (int col = range.left(); col <= range.right() ; ++col) {
258 const Cell cell(sheet, col, range.top());
259 const Value value = cell.value();
260 const QString columnHeader = converter->asString(value).asString();
261 columnHeaderCells[columnHeader].append(cell);
262 }
263 }
264 QStringList columnHeaders = columnHeaderCells.keys();
265 columnHeaders.sort();
266
267 // Check whether the destination is part of the source ...
268 const QRect destinationRange(dx, dy, columnHeaders.count(), rows);
269 for (int i = 0; i < ranges.count(); ++i) {
270 Sheet *const sheet = ranges[i].firstSheet();
271 Q_ASSERT(sheet);
272 const QRect range = ranges[i].firstRange();
273 if (sheet == destinationSheet && range.intersects(destinationRange)) {
274 QString tmp(i18n("The source range intersects the destination range."));
275 KMessageBox::error(this, tmp);
276 return;
277 }
278 }
279
280 // Now create the destination cell range
281 for (int col = 0; col < columnHeaders.count(); ++col) {
282 // Fill in the header data.
283 const QString columnHeader = columnHeaders[col];
284 d->setContent(destinationSheet, dy, dx + col, columnHeader, command);
285
286 const QList<Cell> cells = columnHeaderCells[columnHeader];
287 for (int row = 1; row < rows; ++row) {
288 QString formula = '=' + function + '(';
289 for (int i = 0; i < cells.count(); ++i) {
290 if (i != 0) {
291 formula += ';';
292 }
293 Sheet *const sheet = cells[i].sheet();
294 const int headerColumn = cells[i].column();
295 const int headerRow = cells[i].row();
296 const Cell cell(sheet, headerColumn, headerRow + row);
297 const bool fullName = sheet != destinationSheet;
298 formula += fullName ? cell.fullName() : cell.name();
299 }
300 formula += ')';
301
302 d->setContent(destinationSheet, dy + row, dx + col, formula, command);
303 }
304 }
305 } else if (desc == D_ROW) {
306 // Get list of all descriptions in the rows
307 QHash<QString, QList<Cell> > rowHeaderCells;
308 for (int i = 0; i < ranges.count(); ++i) {
309 Sheet *const sheet = ranges[i].firstSheet();
310 Q_ASSERT(sheet);
311 const QRect range = ranges[i].firstRange();
312 for (int row = range.top(); row <= range.bottom() ; ++row) {
313 const Cell cell(sheet, range.left(), row);
314 const Value value = cell.value();
315 const QString rowHeader = converter->asString(value).asString();
316 rowHeaderCells[rowHeader].append(cell);
317 }
318 }
319 QStringList rowHeaders = rowHeaderCells.keys();
320 rowHeaders.sort();
321
322 // Check whether the destination is part of the source ...
323 const QRect destinationRange(dx, dy, columns, rowHeaders.count());
324 for (int i = 0; i < ranges.count(); ++i) {
325 Sheet *const sheet = ranges[i].firstSheet();
326 Q_ASSERT(sheet);
327 const QRect range = ranges[i].firstRange();
328 if (sheet == destinationSheet && range.intersects(destinationRange)) {
329 QString tmp(i18n("The source range intersects the destination range."));
330 KMessageBox::error(this, tmp);
331 return;
332 }
333 }
334
335 // Now create the destination cell range
336 for (int row = 0; row < rowHeaders.count(); ++row) {
337 // Fill in the header data.
338 const QString rowHeader = rowHeaders[row];
339 d->setContent(destinationSheet, dy + row, dx, rowHeader, command);
340
341 const QList<Cell> cells = rowHeaderCells[rowHeader];
342 for (int col = 1; col < columns; ++col) {
343 QString formula = '=' + function + '(';
344 for (int i = 0; i < cells.count(); ++i) {
345 if (i != 0) {
346 formula += ';';
347 }
348 Sheet *const sheet = cells[i].sheet();
349 const int headerColumn = cells[i].column();
350 const int headerRow = cells[i].row();
351 const Cell cell(sheet, headerColumn + col, headerRow);
352 const bool fullName = sheet != destinationSheet;
353 formula += fullName ? cell.fullName() : cell.name();
354 }
355 formula += ')';
356
357 d->setContent(destinationSheet, dy + row, dx + col, formula, command);
358 }
359 }
360 } else if (desc == D_BOTH) {
361 // Get list of all descriptions in the rows
362 QStringList rowHeaders;
363 for (int i = 0; i < ranges.count(); ++i) {
364 Sheet *const sheet = ranges[i].firstSheet();
365 Q_ASSERT(sheet);
366 const QRect range = ranges[i].firstRange();
367 for (int row = range.top() + 1; row <= range.bottom() ; ++row) {
368 const Value value = Cell(sheet, range.left(), row).value();
369 const QString rowHeader = converter->asString(value).asString();
370 if (!rowHeaders.contains(rowHeader)) {
371 rowHeaders.append(rowHeader);
372 }
373 }
374 }
375 rowHeaders.sort();
376
377 // Get list of all descriptions in the columns
378 QStringList columnHeaders;
379 for (int i = 0; i < ranges.count(); ++i) {
380 Sheet *const sheet = ranges[i].firstSheet();
381 Q_ASSERT(sheet);
382 const QRect range = ranges[i].firstRange();
383 for (int col = range.left() + 1; col <= range.right() ; ++col) {
384 const Value value = Cell(sheet, col, range.top()).value();
385 const QString columnHeader = converter->asString(value).asString();
386 if (!columnHeaders.contains(columnHeader)) {
387 columnHeaders.append(columnHeader);
388 }
389 }
390 }
391 columnHeaders.sort();
392
393 // Check whether the destination is part of the source ...
394 const QRect destinationRange(dx, dy, columnHeaders.count(), rowHeaders.count());
395 for (int i = 0; i < ranges.count(); ++i) {
396 Sheet *const sheet = ranges[i].firstSheet();
397 Q_ASSERT(sheet);
398 const QRect range = ranges[i].firstRange();
399 if (sheet == destinationSheet && range.intersects(destinationRange)) {
400 QString tmp(i18n("The source range intersects the destination range."));
401 KMessageBox::error(this, tmp);
402 return;
403 }
404 }
405
406 // Fill the list with all interesting cells
407 QHash<QString /* row */, QHash<QString /* col */, QList<Cell> > > list;
408 for (int i = 0; i < ranges.count(); ++i) {
409 Sheet *const sheet = ranges[i].firstSheet();
410 Q_ASSERT(sheet);
411 const QRect range = ranges[i].firstRange();
412 for (int col = range.left() + 1; col <= range.right() ; ++col) {
413 const Value columnValue = Cell(sheet, col, range.top()).value();
414 const QString columnHeader = converter->asString(columnValue).asString();
415 for (int row = range.top() + 1; row <= range.bottom() ; ++row) {
416 const Value rowValue = Cell(sheet, range.left(), row).value();
417 const QString rowHeader = converter->asString(rowValue).asString();
418 list[rowHeader][columnHeader].append(Cell(sheet, col, row));
419 }
420 }
421 }
422
423 // Draw the row description
424 for (int i = 0; i < rowHeaders.count(); ++i) {
425 d->setContent(destinationSheet, dy + 1 + i, dx, rowHeaders[i], command);
426 }
427
428 // Draw the column description
429 for (int i = 0; i < columnHeaders.count(); ++i) {
430 d->setContent(destinationSheet, dy, dx + 1 + i, columnHeaders[i], command);
431 }
432
433 // Draw the data
434 for (int row = 0; row < rowHeaders.count(); ++row) {
435 for (int col = 0; col < columnHeaders.count(); ++col) {
436 QString formula = '=' + function + '(';
437 const QString rowHeader = rowHeaders[row];
438 const QString columnHeader = columnHeaders[col];
439 const QList<Cell> lst = list[rowHeader][columnHeader];
440 for (int i = 0; i < lst.count(); ++i) {
441 if (i != 0) {
442 formula += ';';
443 }
444 const bool fullName = lst[i].sheet() != destinationSheet;
445 formula += fullName ? lst[i].fullName() : lst[i].name();
446 }
447 formula += ')';
448
449 d->setContent(destinationSheet, dy + 1 + row, dx + 1 + col, formula, command);
450 }
451 }
452 }
453
454 // execute the cumulating parent command
455 map->addCommand(command);
456
457 KoDialog::accept();
458 }
459
slotAdd()460 void ConsolidateDialog::slotAdd()
461 {
462 slotReturnPressed();
463 }
464
slotRemove()465 void ConsolidateDialog::slotRemove()
466 {
467 int i = d->mainWidget.m_sourceRanges->currentRow();
468 if (i < 0)
469 return;
470
471 delete d->mainWidget.m_sourceRanges->takeItem(i);
472
473 if (d->mainWidget.m_sourceRanges->count() == 0)
474 enableButton(Ok, false);
475 }
476
slotSelectionChanged()477 void ConsolidateDialog::slotSelectionChanged()
478 {
479 if (!d->selection->isValid()) {
480 d->mainWidget.m_sourceRange->setText("");
481 return;
482 }
483
484 QString area = d->selection->name();
485 d->mainWidget.m_sourceRange->setText(area);
486 d->mainWidget.m_sourceRange->setSelection(0, area.length());
487 }
488
slotReturnPressed()489 void ConsolidateDialog::slotReturnPressed()
490 {
491 QString txt = d->mainWidget.m_sourceRange->text();
492
493 const Region r(txt, d->selection->activeSheet()->map());
494 if (!r.isValid()) {
495 KMessageBox::error(this, i18n("The range\n%1\n is malformed", txt));
496 return;
497 }
498
499 if (!txt.isEmpty()) {
500 d->mainWidget.m_sourceRanges->addItem(txt);
501 enableButton(Ok, true);
502 }
503 }
504
setContent(Sheet * sheet,int row,int column,const QString & text,KUndo2Command * parent)505 void ConsolidateDialog::Private::setContent(Sheet *sheet, int row, int column,
506 const QString &text, KUndo2Command *parent)
507 {
508 Value value;
509 // Directly evaluate the formula, i.e. copy data, i.e. do not link to data?
510 if (detailsWidget.m_copyData->isChecked()) {
511 Formula formula(sheet);
512 formula.setExpression(text);
513 if (!formula.isValid()) {
514 debugSheets << "Invalid formula:" << text;
515 return; // Quit before creating/adding the sub-command.
516 }
517 value = formula.eval();
518 } else {
519 value = Value(text);
520 }
521
522 DataManipulator *const command = new DataManipulator(parent);
523 command->setSheet(sheet);
524 command->setValue(value);
525 command->setParsing(!detailsWidget.m_copyData->isChecked());
526 command->add(QPoint(column, row));
527 // executed by the parent command
528 }
529