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