1 /*
2     SPDX-FileCopyrightText: 2021 Michail Vourlakos <mvourlakos@gmail.com>
3     SPDX-License-Identifier: GPL-2.0-or-later
4 */
5 
6 #include "viewscontroller.h"
7 
8 // local
9 #include <config-latte.h>
10 #include "ui_viewsdialog.h"
11 #include "viewsdialog.h"
12 #include "viewshandler.h"
13 #include "viewsmodel.h"
14 #include "viewstableview.h"
15 #include "delegates/namedelegate.h"
16 #include "delegates/singleoptiondelegate.h"
17 #include "delegates/singletextdelegate.h"
18 #include "../generic/generictools.h"
19 #include "../settingsdialog/templateskeeper.h"
20 #include "../../data/errorinformationdata.h"
21 #include "../../layout/genericlayout.h"
22 #include "../../layout/centrallayout.h"
23 #include "../../layouts/manager.h"
24 #include "../../layouts/synchronizer.h"
25 #include "../../view/view.h"
26 
27 // Qt
28 #include <QHeaderView>
29 #include <QItemSelection>
30 
31 // KDE
32 #include <KMessageWidget>
33 #include <KSharedConfig>
34 
35 #if KF5_VERSION_MINOR >= 71
36 #include <KIO/OpenUrlJob>
37 #else
38 #include <KRun>
39 #endif
40 
41 namespace Latte {
42 namespace Settings {
43 namespace Controller {
44 
45 
Views(Settings::Handler::ViewsHandler * parent)46 Views::Views(Settings::Handler::ViewsHandler *parent)
47     : QObject(parent),
48       m_handler(parent),
49       m_model(new Model::Views(this, m_handler->corona())),
50       m_proxyModel(new QSortFilterProxyModel(this)),
51       m_view(m_handler->ui()->viewsTable),
52       m_storage(KConfigGroup(KSharedConfig::openConfig(),"LatteSettingsDialog").group("ViewsDialog"))
53 {
54     loadConfig();
55     m_proxyModel->setSourceModel(m_model);
56 
57     connect(m_model, &QAbstractItemModel::dataChanged, this, &Views::dataChanged);
58     connect(m_model, &Model::Views::rowsInserted, this, &Views::dataChanged);
59     connect(m_model, &Model::Views::rowsRemoved, this, &Views::dataChanged);
60 
61     connect(m_handler, &Handler::ViewsHandler::currentLayoutChanged, this, &Views::onCurrentLayoutChanged);
62 
63     init();
64 }
65 
~Views()66 Views::~Views()
67 {
68     saveConfig();
69 }
70 
proxyModel() const71 QAbstractItemModel *Views::proxyModel() const
72 {
73     return m_proxyModel;
74 }
75 
baseModel() const76 QAbstractItemModel *Views::baseModel() const
77 {
78     return m_model;
79 }
80 
view() const81 QTableView *Views::view() const
82 {
83     return m_view;
84 }
85 
init()86 void Views::init()
87 {
88     m_view->setModel(m_proxyModel);
89     //m_view->setHorizontalHeader(m_headerView);
90     m_view->verticalHeader()->setVisible(false);
91     m_view->setSortingEnabled(true);
92 
93     m_proxyModel->setSortRole(Model::Views::SORTINGROLE);
94     m_proxyModel->setSortCaseSensitivity(Qt::CaseInsensitive);
95 
96     m_view->sortByColumn(m_viewSortColumn, m_viewSortOrder);
97 
98     m_view->setItemDelegateForColumn(Model::Views::IDCOLUMN, new Settings::View::Delegate::SingleText(this));
99     m_view->setItemDelegateForColumn(Model::Views::NAMECOLUMN, new Settings::View::Delegate::NameDelegate(this));
100     m_view->setItemDelegateForColumn(Model::Views::SCREENCOLUMN, new Settings::View::Delegate::SingleOption(this));
101     m_view->setItemDelegateForColumn(Model::Views::EDGECOLUMN, new Settings::View::Delegate::SingleOption(this));
102     m_view->setItemDelegateForColumn(Model::Views::ALIGNMENTCOLUMN, new Settings::View::Delegate::SingleOption(this));
103     m_view->setItemDelegateForColumn(Model::Views::SUBCONTAINMENTSCOLUMN, new Settings::View::Delegate::SingleText(this));
104 
105     applyColumnWidths();
106 
107     m_cutAction = new QAction(QIcon::fromTheme("edit-cut"), i18n("Cut"), m_view);
108     m_cutAction->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_X));
109     connect(m_cutAction, &QAction::triggered, this, &Views::cutSelectedViews);
110 
111     m_copyAction = new QAction(QIcon::fromTheme("edit-copy"), i18n("Copy"), m_view);
112     m_copyAction->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_C));
113     connect(m_copyAction, &QAction::triggered, this, &Views::copySelectedViews);
114 
115     m_pasteAction = new QAction(QIcon::fromTheme("edit-paste"), i18n("Paste"), m_view);
116     m_pasteAction->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_V));
117     connect(m_pasteAction, &QAction::triggered, this, &Views::pasteSelectedViews);
118 
119     m_duplicateAction = new QAction(QIcon::fromTheme("edit-copy"), i18n("Duplicate Here"), m_view);
120     m_duplicateAction->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_D));
121     connect(m_duplicateAction, &QAction::triggered, this, &Views::duplicateSelectedViews);
122 
123     m_view->addAction(m_cutAction);
124     m_view->addAction(m_copyAction);
125     m_view->addAction(m_duplicateAction);
126     m_view->addAction(m_pasteAction);
127 
128     onSelectionsChanged();
129 
130     connect(m_view, &View::ViewsTableView::selectionsChanged, this, &Views::onSelectionsChanged);
131     connect(m_view, &QObject::destroyed, this, &Views::storeColumnWidths);
132 
133     connect(m_view->horizontalHeader(), &QObject::destroyed, this, [&]() {
134         m_viewSortColumn = m_view->horizontalHeader()->sortIndicatorSection();
135         m_viewSortOrder = m_view->horizontalHeader()->sortIndicatorOrder();
136     });
137 }
138 
reset()139 void Views::reset()
140 {
141     m_model->resetData();
142 
143     //! Clear any templates keeper data in order to produce reupdates if needed
144     m_handler->layoutsController()->templatesKeeper()->clear();
145 }
146 
hasChangedData() const147 bool Views::hasChangedData() const
148 {
149     return m_model->hasChangedData();
150 }
151 
hasSelectedView() const152 bool Views::hasSelectedView() const
153 {
154     return m_view->selectionModel()->hasSelection();
155 }
156 
selectedViewsCount() const157 int Views::selectedViewsCount() const
158 {
159     return m_view->selectionModel()->selectedRows(Model::Views::IDCOLUMN).count();
160 }
161 
rowForId(QString id) const162 int Views::rowForId(QString id) const
163 {
164     for (int i = 0; i < m_proxyModel->rowCount(); ++i) {
165         QString rowId = m_proxyModel->data(m_proxyModel->index(i, Model::Views::IDCOLUMN), Qt::UserRole).toString();
166 
167         if (rowId == id) {
168             return i;
169         }
170     }
171 
172     return -1;
173 }
174 
selectedViewsCurrentData() const175 const Data::ViewsTable Views::selectedViewsCurrentData() const
176 {
177     Data::ViewsTable selectedviews;
178 
179     if (!hasSelectedView()) {
180         return selectedviews;
181     }
182 
183     QModelIndexList layoutidindexes = m_view->selectionModel()->selectedRows(Model::Views::IDCOLUMN);
184 
185     for(int i=0; i<layoutidindexes.count(); ++i) {
186         QString selectedid = layoutidindexes[i].data(Qt::UserRole).toString();
187         selectedviews <<  m_model->currentData(selectedid);
188     }
189 
190     return selectedviews;
191 }
192 
appendViewFromViewTemplate(const Data::View & view)193 const Latte::Data::View Views::appendViewFromViewTemplate(const Data::View &view)
194 {
195     Data::View newview = view;
196     newview.name = uniqueViewName(view.name);
197     m_model->appendTemporaryView(newview);
198     return newview;
199 }
200 
currentData(const QString & id)201 const Latte::Data::View Views::currentData(const QString &id)
202 {
203     return m_model->currentData(id);
204 }
205 
selectedViewsForClipboard()206 Data::ViewsTable Views::selectedViewsForClipboard()
207 {
208     Data::ViewsTable clipboardviews;
209     if (!hasSelectedView()) {
210         return clipboardviews;
211     }
212 
213     Data::ViewsTable selectedviews = selectedViewsCurrentData();
214     Latte::Data::Layout currentlayout = m_handler->currentData();
215 
216     for(int i=0; i<selectedviews.rowCount(); ++i) {
217         if (selectedviews[i].state() == Data::View::IsInvalid) {
218             continue;
219         }
220 
221         Latte::Data::View copiedview = selectedviews[i];
222 
223         if (selectedviews[i].state() == Data::View::IsCreated) {
224             QString storedviewpath = m_handler->layoutsController()->templatesKeeper()->storedView(currentlayout.id, selectedviews[i].id);
225             copiedview.setState(Data::View::OriginFromLayout, storedviewpath, currentlayout.id, selectedviews[i].id);
226         } else if (selectedviews[i].state() == Data::View::OriginFromViewTemplate) {
227             copiedview.setState(Data::View::OriginFromViewTemplate, selectedviews[i].originFile(), currentlayout.id, selectedviews[i].id);
228         } else if (selectedviews[i].state() == Data::View::OriginFromLayout) {
229             //! is already in valid values
230         }
231 
232         copiedview.isActive = false;
233         clipboardviews << copiedview;
234 
235     }
236 
237     return clipboardviews;
238 }
239 
copySelectedViews()240 void Views::copySelectedViews()
241 {
242     qDebug() << Q_FUNC_INFO;
243 
244     if (!hasSelectedView()) {
245         return;
246     }
247 
248     //! reset cut substates for views
249     Data::ViewsTable currentviews = m_model->currentViewsData();
250     for (int i=0; i<currentviews.rowCount(); ++i) {
251         Data::View cview = currentviews[i];
252         cview.isMoveOrigin = false;
253         m_model->updateCurrentView(cview.id, cview);
254     }
255 
256     Data::ViewsTable clipboardviews = selectedViewsForClipboard();
257 
258     //! reset cut substates for views
259     for (int i=0; i<clipboardviews.rowCount(); ++i) {
260         clipboardviews[i].isMoveOrigin = false;
261 
262         /*   Data::View tempview = m_model->currentData(clipboardviews[i].id);
263         tempview.isMoveOrigin = false;
264         m_model->updateCurrentView(tempview.id, tempview);*/
265     }
266 
267     m_handler->layoutsController()->templatesKeeper()->setClipboardContents(clipboardviews);
268 }
269 
cutSelectedViews()270 void Views::cutSelectedViews()
271 {
272     qDebug() << Q_FUNC_INFO;
273 
274     if (!hasSelectedView()) {
275         return;
276     }
277 
278     //! reset previous move records
279     Data::ViewsTable currentviews = m_model->currentViewsData();
280     for (int i=0; i<currentviews.rowCount(); ++i) {
281         Data::View cview = currentviews[i];
282         cview.isMoveOrigin = false;
283         m_model->updateCurrentView(cview.id, cview);
284     }
285 
286     Data::ViewsTable clipboardviews = selectedViewsForClipboard();
287 
288     //! activate cut substates for views
289     for (int i=0; i<clipboardviews.rowCount(); ++i) {
290         clipboardviews[i].isMoveOrigin = true;
291 
292         Data::View tempview = m_model->currentData(clipboardviews[i].id);
293         tempview.isMoveOrigin = true;
294         m_model->updateCurrentView(tempview.id, tempview);
295     }
296 
297     m_handler->layoutsController()->templatesKeeper()->setClipboardContents(clipboardviews);
298 }
299 
pasteSelectedViews()300 void Views::pasteSelectedViews()
301 {
302     Data::ViewsTable clipboardviews = m_handler->layoutsController()->templatesKeeper()->clipboardContents();
303     Latte::Data::Layout currentlayout = m_handler->currentData();
304 
305     bool hascurrentlayoutcuttedviews{false};
306 
307     for(int i=0; i<clipboardviews.rowCount(); ++i) {
308         if (clipboardviews[i].isMoveOrigin && clipboardviews[i].originLayout() == currentlayout.id) {
309             hascurrentlayoutcuttedviews = true;
310             continue;
311         }
312 
313         if (clipboardviews[i].isMoveOrigin) {
314             //! update cut flags only for real cutted view and not for copied one
315             clipboardviews[i].isMoveOrigin = false;
316             clipboardviews[i].isMoveDestination = true;
317         }
318 
319         appendViewFromViewTemplate(clipboardviews[i]);
320     }
321 
322     if (hascurrentlayoutcuttedviews) {
323         m_handler->showInlineMessage(i18n("Docks and panels from <b>Paste</b> action are already present in current layout"),
324                                      KMessageWidget::Warning);
325     }
326 }
327 
duplicateSelectedViews()328 void Views::duplicateSelectedViews()
329 {
330     qDebug() << Q_FUNC_INFO;
331 
332     if (!hasSelectedView()) {
333         return;
334     }
335 
336     Data::ViewsTable selectedviews = selectedViewsCurrentData();
337     Latte::Data::Layout currentlayout = m_handler->currentData();
338 
339     for(int i=0; i<selectedviews.rowCount(); ++i) {
340         if (selectedviews[i].state() == Data::View::IsCreated) {
341             QString storedviewpath = m_handler->layoutsController()->templatesKeeper()->storedView(currentlayout.id, selectedviews[i].id);
342             Latte::Data::View duplicatedview = selectedviews[i];
343             duplicatedview.setState(Data::View::OriginFromLayout, storedviewpath, currentlayout.id, selectedviews[i].id);
344             duplicatedview.isActive = false;
345             appendViewFromViewTemplate(duplicatedview);
346         } else if (selectedviews[i].state() == Data::View::OriginFromViewTemplate
347                    || selectedviews[i].state() == Data::View::OriginFromLayout) {
348             Latte::Data::View duplicatedview = selectedviews[i];
349             duplicatedview.isActive = false;
350             appendViewFromViewTemplate(duplicatedview);
351         }
352     }
353 }
354 
removeSelectedViews()355 void Views::removeSelectedViews()
356 {
357     if (!hasSelectedView()) {
358         return;
359     }
360 
361     Data::ViewsTable selectedviews = selectedViewsCurrentData();;
362 
363     int selectionheadrow = m_model->rowForId(selectedviews[0].id);
364 
365     for (int i=0; i<selectedviews.rowCount(); ++i) {
366         m_model->removeView(selectedviews[i].id);
367     }
368 
369     m_view->selectRow(qBound(0, selectionheadrow, m_model->rowCount()-1));
370 }
371 
selectRow(const QString & id)372 void Views::selectRow(const QString &id)
373 {
374     m_view->selectRow(rowForId(id));
375 }
376 
onCurrentLayoutChanged()377 void Views::onCurrentLayoutChanged()
378 {
379     Data::Layout currentlayoutdata = m_handler->currentData();
380 
381     Data::ViewsTable clipboardviews = m_handler->layoutsController()->templatesKeeper()->clipboardContents();
382 
383     if (!clipboardviews.isEmpty()) {
384         //! clipboarded views needs to update the relevant flags to loaded views
385         for (int i=0; i<currentlayoutdata.views.rowCount(); ++i) {
386             QString vid = currentlayoutdata.views[i].id;
387 
388             if (!clipboardviews.containsId(vid)) {
389                 continue;
390             }
391 
392             if (clipboardviews[vid].isMoveOrigin && (clipboardviews[vid].originLayout() == currentlayoutdata.id)) {
393                 currentlayoutdata.views[vid].isMoveOrigin = true;
394             }
395         }
396     }
397 
398     m_model->setOriginalData(currentlayoutdata.views);
399 
400     //! track viewscountchanged signal for current active layout scenario
401     for (const auto &var : m_currentLayoutConnections) {
402         QObject::disconnect(var);
403     }
404 
405     Latte::CentralLayout *currentlayout = m_handler->layoutsController()->centralLayout(currentlayoutdata.id);
406 
407     if (currentlayout && currentlayout->isActive()) {
408         m_currentLayoutConnections << connect(currentlayout, &Layout::GenericLayout::viewsCountChanged, this, [&, currentlayout](){
409             m_model->updateActiveStatesBasedOn(currentlayout);
410         });
411     }
412 
413     messagesForErrorsWarnings(currentlayout);
414 }
415 
onSelectionsChanged()416 void Views::onSelectionsChanged()
417 {
418     bool hasselectedview = hasSelectedView();
419 
420     m_cutAction->setVisible(hasselectedview);
421     m_copyAction->setVisible(hasselectedview);
422     m_duplicateAction->setVisible(hasselectedview);
423     m_pasteAction->setEnabled(m_handler->layoutsController()->templatesKeeper()->hasClipboardContents());
424 }
425 
viewsForRemovalCount() const426 int Views::viewsForRemovalCount() const
427 {
428     if (!hasChangedData()) {
429         return 0;
430     }
431 
432     Latte::Data::ViewsTable originalViews = m_model->originalViewsData();
433     Latte::Data::ViewsTable currentViews = m_model->currentViewsData();
434     Latte::Data::ViewsTable removedViews = originalViews.subtracted(currentViews);
435 
436     return removedViews.rowCount();
437 }
438 
hasValidOriginView(const Data::View & view)439 bool Views::hasValidOriginView(const Data::View &view)
440 {
441     bool viewidisinteger{true};
442     int vid_int = view.originView().toInt(&viewidisinteger);
443     QString vid_str = view.originView();
444 
445     if (vid_str.isEmpty() || !viewidisinteger || vid_int<=0) {
446         return false;
447     }
448 
449     return true;
450 }
451 
originLayout(const Data::View & view)452 CentralLayout *Views::originLayout(const Data::View &view)
453 {
454     QString origincurrentid = view.originLayout();
455     Data::Layout originlayoutdata = m_handler->layoutsController()->originalData(origincurrentid);
456 
457     Latte::CentralLayout *originactive = m_handler->layoutsController()->isLayoutOriginal(origincurrentid) ?
458                 m_handler->corona()->layoutsManager()->synchronizer()->centralLayout(originlayoutdata.name) : nullptr;
459 
460     return originactive;
461 }
462 
updateDoubledMoveDestinationRows()463 void Views::updateDoubledMoveDestinationRows() {
464     //! only one isMoveDestination should exist for each unique move isMoveOrigin case
465     //! all the rest that have been created through Cut/Paste or Duplicate options should become
466     //! simple OriginFromViewTemplate cases
467 
468     for (int i=0; i<m_model->rowCount(); ++i) {
469         Data::View baseview = m_model->at(i);
470 
471         if (!baseview.isMoveDestination || baseview.state()!=Data::View::OriginFromLayout) {
472             continue;
473         }
474 
475         for (int j=i+1; j<m_model->rowCount(); ++j) {
476             Data::View subsequentview = m_model->at(j);
477 
478             if (subsequentview.isMoveDestination
479                     && subsequentview.state() == Data::View::OriginFromLayout
480                     && subsequentview.originFile() == baseview.originFile()
481                     && subsequentview.originLayout() == baseview.originLayout()
482                     && subsequentview.originView() == baseview.originView()) {
483                 //! this is a subsequent view that needs to be updated properly
484                 subsequentview.isMoveDestination = false;
485                 subsequentview.isMoveOrigin = false;
486                 subsequentview.setState(Data::View::OriginFromViewTemplate, subsequentview.originFile(), QString(), QString());
487                 m_model->updateCurrentView(subsequentview.id, subsequentview);
488             }
489         }
490     }
491 }
492 
messagesForErrorsWarnings(const Latte::CentralLayout * centralLayout,const bool & showNoErrorsMessage)493 void Views::messagesForErrorsWarnings(const Latte::CentralLayout *centralLayout, const bool &showNoErrorsMessage)
494 {
495     if (!centralLayout) {
496         return;
497     }
498 
499     Data::Layout currentdata = centralLayout->data();
500 
501     m_model->clearErrorsAndWarnings();
502 
503     //! warnings
504     if (currentdata.warnings > 0) {
505         Data::WarningsList warnings = centralLayout->warnings();
506 
507         // show warnings
508         for (int i=0; i< warnings.count(); ++i) {
509             if (warnings[i].id == Data::Warning::ORPHANEDSUBCONTAINMENT) {
510                 messageForWarningOrphanedSubContainments(warnings[i]);
511             } else if (warnings[i].id == Data::Warning::APPLETANDCONTAINMENTWITHSAMEID) {
512                 messageForWarningAppletAndContainmentWithSameId(warnings[i]);
513             }
514         }
515 
516         // count warnings per view
517         for (int i=0; i<warnings.count(); ++i) {
518             for (int j=0; j<warnings[i].information.rowCount(); ++j) {
519                 if (!warnings[i].information[j].containment.isValid()) {
520                     continue;
521                 }
522 
523                 QString cid = warnings[i].information[j].containment.storageId;
524                 Data::View view = m_model->currentData(cid);
525                 if (!view.isValid()) {
526                     //! one step back from subcontainment to view in order to find the influenced view id
527                     cid = m_model->viewForSubContainment(cid);
528                     view = m_model->currentData(cid);
529                 }
530 
531                 if (view.isValid()) {
532                     view.warnings++;
533                     m_model->updateCurrentView(cid, view);
534                 }
535             }
536         }
537     }
538 
539     //! errors
540     if (currentdata.errors > 0) {
541         Data::ErrorsList errors = centralLayout->errors();
542 
543         // show errors
544         for (int i=0; i< errors.count(); ++i) {
545             if (errors[i].id == Data::Error::APPLETSWITHSAMEID) {
546                 messageForErrorAppletsWithSameId(errors[i]);
547             } else if (errors[i].id == Data::Error::ORPHANEDPARENTAPPLETOFSUBCONTAINMENT) {
548                 messageForErrorOrphanedParentAppletOfSubContainment(errors[i]);
549             }
550         }
551 
552         // count errors per view
553         for (int i=0; i<errors.count(); ++i) {
554             for (int j=0; j<errors[i].information.rowCount(); ++j) {
555                 if (!errors[i].information[j].containment.isValid()) {
556                     continue;
557                 }
558 
559                 QString cid = errors[i].information[j].containment.storageId;
560                 Data::View view = m_model->currentData(cid);
561                 if (!view.isValid()) {
562                     //! one step back from subcontainment to view in order to find the influenced view id
563                     cid = m_model->viewForSubContainment(cid);
564                     view = m_model->currentData(cid);
565                 }
566 
567                 if (view.isValid()) {
568                     view.errors++;
569                     m_model->updateCurrentView(cid, view);
570                 }
571             }
572         }
573     }
574 
575     m_handler->layoutsController()->setLayoutCurrentErrorsWarnings(currentdata.id, currentdata.errors, currentdata.warnings);
576 
577     if (showNoErrorsMessage && currentdata.errors == 0 && currentdata.warnings == 0) {
578         m_handler->showInlineMessage(i18n("Really nice! You are good to go, your layout does not report any errors or warnings."),
579                                      KMessageWidget::Positive,
580                                      false);
581     }
582 
583 }
584 
showDefaultPersistentErrorWarningInlineMessage(const QString & messageText,const KMessageWidget::MessageType & messageType,QList<QAction * > extraActions,const bool & showOpenLayoutAction)585 void Views::showDefaultPersistentErrorWarningInlineMessage(const QString &messageText,
586                                                            const KMessageWidget::MessageType &messageType,
587                                                            QList<QAction *> extraActions,
588                                                            const bool &showOpenLayoutAction)
589 {
590     QList<QAction *> actions;
591     actions << extraActions;
592 
593     if (showOpenLayoutAction) {
594         Data::Layout currentlayout = m_handler->currentData();
595 
596         //! add default action to open layout
597         QAction *openlayoutaction = new QAction(i18n("Edit Layout"), this);
598         openlayoutaction->setEnabled(!currentlayout.isActive);
599         openlayoutaction->setIcon(QIcon::fromTheme("document-edit"));
600         openlayoutaction->setData(currentlayout.id);
601         actions << openlayoutaction;
602 
603         connect(openlayoutaction, &QAction::triggered, this, [&, openlayoutaction]() {
604             QString file = openlayoutaction->data().toString();
605 
606             if (!file.isEmpty()) {
607 #if KF5_VERSION_MINOR >= 71
608                 auto job = new KIO::OpenUrlJob(QUrl::fromLocalFile(file), QStringLiteral("text/plain"), this);
609                 job->start();
610 #else
611                 KRun::runUrl(QUrl::fromLocalFile(file), QStringLiteral("text/plain"), m_view);
612 #endif
613                 showDefaultInlineMessageValidator();
614             }
615         });
616     }
617 
618     //! show message
619     m_handler->showInlineMessage(messageText,
620                                  messageType,
621                                  true,
622                                  actions);
623 }
624 
showDefaultInlineMessageValidator()625 void Views::showDefaultInlineMessageValidator()
626 {
627     Data::Layout currentlayout = m_handler->currentData();
628 
629     //! add default action to open layout
630     QAction *validateaction = new QAction(i18n("Validate"), this);
631     validateaction->setIcon(QIcon::fromTheme("view-refresh"));
632     validateaction->setData(currentlayout.id);
633 
634     QList<QAction *> actions;
635     actions << validateaction;
636 
637     connect(validateaction, &QAction::triggered, this, [&, currentlayout]() {
638 
639         auto centrallayout = m_handler->layoutsController()->centralLayout(currentlayout.id);
640         if (centrallayout && !centrallayout->isActive()) {
641             KSharedConfigPtr lFile = KSharedConfig::openConfig(centrallayout->file());
642             //! update configuration with latest changes
643             lFile->reparseConfiguration();
644         }
645 
646         messagesForErrorsWarnings(centrallayout, true);
647     });
648 
649     QString messagetext = i18n("After you have made your layout file changes, please click <b>Validate</b> to confirm them.");
650 
651     //! show message
652     m_handler->showInlineMessage(messagetext,
653                                  KMessageWidget::Warning,
654                                  true,
655                                  actions);
656 }
657 
messageForErrorAppletsWithSameId(const Data::Error & error)658 void Views::messageForErrorAppletsWithSameId(const Data::Error &error)
659 {
660     if (error.id != Data::Error::APPLETSWITHSAMEID) {
661         return;
662     }
663 
664     //! construct message
665     QString message = i18nc("error id and title", "<b>Error #%1: %2</b> <br/>",error.id, error.name);
666     message += "<br/>";
667     message += i18n("In your layout there are two or more applets with same id. Such situation can create crashes, abnormal behavior and data loss when you activate and use this layout.<br/>");
668 
669     message += "<br/>";
670     message += i18n("<b>Applets:</b><br/>");
671     for (int i=0; i<error.information.rowCount(); ++i) {
672         QString appletname = error.information[i].applet.visibleName();
673         QString appletstorageid = error.information[i].applet.storageId;
674         QString viewname = visibleViewName(error.information[i].containment.storageId);
675         QString containmentname = viewname.isEmpty() ? error.information[i].containment.visibleName() : viewname;
676         QString containmentstorageid = error.information[i].containment.storageId;
677         message += i18nc("applets with same id error, applet name, applet id, containment name, containment id",
678                          "&nbsp;&nbsp;• <b>%1</b> [#%2] inside  <b>%3</b> [#%4]<br/>",
679                          appletname,
680                          appletstorageid,
681                          containmentname,
682                          containmentstorageid);
683     }
684 
685     message += "<br/>";
686     message += i18n("<b>Possible Solutions:</b><br/>");
687     message += i18n("&nbsp;&nbsp;1. Activate this layout and restart Latte<br/>");
688     message += i18n("&nbsp;&nbsp;2. Remove the mentioned applets from your layout<br/>");
689     message += i18n("&nbsp;&nbsp;3. Update manually the applets id when the layout is <b>not active</b><br/>");
690     message += i18n("&nbsp;&nbsp;4. Remove this layout totally<br/>");
691 
692     showDefaultPersistentErrorWarningInlineMessage(message, KMessageWidget::Error);
693 }
694 
messageForErrorOrphanedParentAppletOfSubContainment(const Data::Error & error)695 void Views::messageForErrorOrphanedParentAppletOfSubContainment(const Data::Error &error)
696 {
697     if (error.id != Data::Error::ORPHANEDPARENTAPPLETOFSUBCONTAINMENT) {
698         return;
699     }
700 
701     //! construct message
702     QString message = i18nc("error id and title", "<b>Error #%1: %2</b> <br/><br/>", error.id, error.name);
703     message += i18n("In your layout there are orphaned pseudo applets that link to unexistent subcontainments. Such case is for example a systemtray that has lost connection with its child applets. Such situation can create crashes, abnormal behavior and data loss when you activate and use this layout.<br/>");
704 
705     message += "<br/>";
706     message += i18n("<b>Pseudo Applets:</b><br/>");
707     for (int i=0; i<error.information.rowCount(); ++i) {
708         if (!error.information[i].applet.isValid()) {
709             continue;
710         }
711 
712         QString appletname = error.information[i].applet.visibleName();
713         QString appletstorageid = error.information[i].applet.storageId;
714         QString viewname = visibleViewName(error.information[i].containment.storageId);
715         QString containmentname = viewname.isEmpty() ? error.information[i].containment.visibleName() : viewname;
716         QString containmentstorageid = error.information[i].containment.storageId;
717         message += i18nc("orphaned pseudo applets, applet name, applet id, containment name, containment id",
718                          "&nbsp;&nbsp;• <b>%1</b> [#%2] inside  <b>%3</b> [#%4]<br/>",
719                          appletname,
720                          appletstorageid,
721                          containmentname,
722                          containmentstorageid);
723     }
724 
725     message += "<br/>";
726     message += i18n("<b>Orphaned Subcontainments:</b><br/>");
727     for (int i=0; i<error.information.rowCount(); ++i) {
728         if (error.information[i].applet.isValid()) {
729             continue;
730         }
731 
732         QString viewname = visibleViewName(error.information[i].containment.storageId);
733         QString containmentname = viewname.isEmpty() ? error.information[i].containment.visibleName() : viewname;
734         QString containmentstorageid = error.information[i].containment.storageId;
735         message += i18nc("orphaned subcontainments, containment name, containment id",
736                          "&nbsp;&nbsp;• <b>%1</b> [#%2] <br/>",
737                          containmentname,
738                          containmentstorageid);
739     }
740 
741     message += "<br/>";
742     message += i18n("<b>Possible Solutions:</b><br/>");
743     message += i18n("&nbsp;&nbsp;1. Update manually the subcontainment id inside pseudo applet settings when the layout is <b>not active</b><br/>");
744     message += i18n("&nbsp;&nbsp;2. Remove this layout totally<br/>");
745 
746     //! show message
747     showDefaultPersistentErrorWarningInlineMessage(message, KMessageWidget::Error);
748 }
749 
messageForWarningAppletAndContainmentWithSameId(const Data::Warning & warning)750 void Views::messageForWarningAppletAndContainmentWithSameId(const Data::Warning &warning)
751 {
752     if (warning.id != Data::Warning::APPLETANDCONTAINMENTWITHSAMEID) {
753         return;
754     }
755 
756     //! construct message
757     QString message = i18nc("warning id and title", "<b>Warning #%1: %2</b> <br/><br/>", warning.id, warning.name);
758     message += i18n("In your layout there are applets and containments with the same id. Such situation is not dangerous but it should not occur.<br/>");
759 
760     message += "<br/>";
761     message += i18n("<b>Applets:</b><br/>");
762     for (int i=0; i<warning.information.rowCount(); ++i) {
763         if (!warning.information[i].applet.isValid()) {
764             continue;
765         }
766 
767         QString appletname = warning.information[i].applet.visibleName();
768         QString appletstorageid = warning.information[i].applet.storageId;
769         QString viewname = visibleViewName(warning.information[i].containment.storageId);
770         QString containmentname = viewname.isEmpty() ? warning.information[i].containment.visibleName() : viewname;
771         QString containmentstorageid = warning.information[i].containment.storageId;
772         message += i18nc("applets, applet name, applet id, containment name, containment id",
773                          "&nbsp;&nbsp;• <b>%1</b> [#%2] inside  <b>%3</b> [#%4]<br/>",
774                          appletname,
775                          appletstorageid,
776                          containmentname,
777                          containmentstorageid);
778     }
779 
780     message += "<br/>";
781     message += i18n("<b>Containments:</b><br/>");
782     for (int i=0; i<warning.information.rowCount(); ++i) {
783         if (warning.information[i].applet.isValid()) {
784             continue;
785         }
786 
787         QString viewname = visibleViewName(warning.information[i].containment.storageId);
788         QString containmentname = viewname.isEmpty() ? warning.information[i].containment.visibleName() : viewname;
789         QString containmentstorageid = warning.information[i].containment.storageId;
790         message += i18nc("containments, containment name, containment id",
791                          "&nbsp;&nbsp;• <b>%1</b> [#%2] <br/>",
792                          containmentname,
793                          containmentstorageid);
794     }
795 
796     message += "<br/>";
797     message += i18n("<b>Possible Solutions:</b><br/>");
798     message += i18n("&nbsp;&nbsp;1. Update manually the containments or applets id when the layout is <b>not active</b><br/>");
799     message += i18n("&nbsp;&nbsp;2. Remove any of the containments or applets that conflict with each other<br/>");
800 
801     //! show message
802     showDefaultPersistentErrorWarningInlineMessage(message, KMessageWidget::Warning);
803 }
804 
messageForWarningOrphanedSubContainments(const Data::Warning & warning)805 void Views::messageForWarningOrphanedSubContainments(const Data::Warning &warning)
806 {
807     if (warning.id != Data::Warning::ORPHANEDSUBCONTAINMENT) {
808         return;
809     }
810 
811     QList<int> orphaned;
812 
813     //! construct message
814     QString message = i18nc("warning id and title", "<b>Warning #%1: %2</b> <br/><br/>", warning.id, warning.name);
815     message += i18n("In your layout there are orphaned subcontainments that are not used by any dock or panel. Such situation is not dangerous but it is advised to remove them in order to reduce memory usage.<br/>");
816 
817     message += "<br/>";
818     message += i18n("<b>Orphaned Subcontainments:</b><br/>");
819     for (int i=0; i<warning.information.rowCount(); ++i) {
820         if (warning.information[i].applet.isValid()) {
821             continue;
822         }
823 
824         QString viewname = visibleViewName(warning.information[i].containment.storageId);
825         QString containmentname = viewname.isEmpty() ? warning.information[i].containment.visibleName() : viewname;
826         QString containmentstorageid = warning.information[i].containment.storageId;
827         message += i18nc("orphaned subcontainments, containment name, containment id",
828                          "&nbsp;&nbsp;• <b>%1</b> [#%2] <br/>",
829                          containmentname,
830                          containmentstorageid);
831 
832         orphaned << warning.information[i].containment.storageId.toInt();
833     }
834 
835     message += "<br/>";
836     message += i18n("<b>Possible Solutions:</b><br/>");
837     message += i18n("&nbsp;&nbsp;1. Click <b>Repair</b> button in order to remove orphaned subcontainments<br/>");
838     message += i18n("&nbsp;&nbsp;2. Remove manually orphaned subcontainments when the layout is <b>not active</b><br/>");
839 
840     //! add extra repair action
841     QAction *repairlayoutaction = new QAction(i18n("Repair"), this);
842     repairlayoutaction->setIcon(QIcon::fromTheme("dialog-yes"));
843     QList<QAction *> extraactions;
844     extraactions << repairlayoutaction;
845 
846     Latte::Data::Layout currentlayout = m_handler->currentData();
847 
848     connect(repairlayoutaction, &QAction::triggered, this, [&, currentlayout, orphaned]() {
849         auto centrallayout = m_handler->layoutsController()->centralLayout(currentlayout.id);
850 
851         for (int i=0; i<orphaned.count(); ++i) {
852             centrallayout->removeOrphanedSubContainment(orphaned[i]);
853         }
854 
855         messagesForErrorsWarnings(centrallayout, true);
856     });
857 
858     //! show message
859     showDefaultPersistentErrorWarningInlineMessage(message,
860                                                    KMessageWidget::Warning,
861                                                    extraactions);
862 }
863 
save()864 void Views::save()
865 {
866     //! when this function is called we consider that removal has already been approved
867     updateDoubledMoveDestinationRows();
868 
869     Latte::Data::Layout originallayout = m_handler->originalData();
870     Latte::Data::Layout currentlayout = m_handler->currentData();
871     Latte::CentralLayout *central = m_handler->layoutsController()->centralLayout(currentlayout.id);
872 
873     //! views in model
874     Latte::Data::ViewsTable originalViews = m_model->originalViewsData();
875     Latte::Data::ViewsTable currentViews = m_model->currentViewsData();
876     Latte::Data::ViewsTable alteredViews = m_model->alteredViews();
877     Latte::Data::ViewsTable newViews = m_model->newViews();
878 
879     QHash<QString, Data::View> newviewsresponses;
880     QHash<QString, Data::View> cuttedpastedviews;
881     QHash<QString, Data::View> cuttedpastedactiveviews;
882 
883     m_debugSaveCall++;
884     qDebug() << "org.kde.latte ViewsDialog::save() call: " << m_debugSaveCall << "-------- ";
885 
886     //! add new views that are accepted
887     for(int i=0; i<newViews.rowCount(); ++i){
888         if (newViews[i].isMoveDestination) {
889             CentralLayout *originActive = originLayout(newViews[i]);
890             bool inmovebetweenactivelayouts = central->isActive() && originActive && central != originActive && hasValidOriginView(newViews[i]);
891 
892             if (inmovebetweenactivelayouts) {
893                 cuttedpastedactiveviews[newViews[i].id] = newViews[i];
894                 continue;
895             }
896 
897             cuttedpastedviews[newViews[i].id] = newViews[i];
898         }
899 
900         if (newViews[i].state() == Data::View::OriginFromViewTemplate) {
901             Data::View addedview = central->newView(newViews[i]);
902 
903             newviewsresponses[newViews[i].id] = addedview;
904         } else if (newViews[i].state() == Data::View::OriginFromLayout) {
905             Data::View adjustedview = newViews[i];
906             adjustedview.setState(Data::View::OriginFromViewTemplate, newViews[i].originFile(), QString(), QString());
907             Data::View addedview = central->newView(adjustedview);
908 
909             newviewsresponses[newViews[i].id] = addedview;
910         }
911     }
912 
913     //! update altered views
914     for (int i=0; i<alteredViews.rowCount(); ++i) {
915         if (alteredViews[i].state() == Data::View::IsCreated && !alteredViews[i].isMoveOrigin) {
916             qDebug() << "org.kde.latte ViewsDialog::save() updating altered view :: " << alteredViews[i];
917             central->updateView(alteredViews[i]);
918         }
919     }
920 
921     //! remove deprecated views that have been removed from user
922     Latte::Data::ViewsTable removedViews = originalViews.subtracted(currentViews);
923 
924     for (int i=0; i<removedViews.rowCount(); ++i) {
925         qDebug() << "org.kde.latte ViewsDialog::save() real removing view :: " << removedViews[i];
926         central->removeView(removedViews[i]);
927     }
928 
929     //! remove deprecated views from external layouts that must be removed because of Cut->Paste Action
930     for(const auto vid: cuttedpastedviews.keys()){
931         bool viewidisinteger{true};
932         int vid_int = cuttedpastedviews[vid].originView().toInt(&viewidisinteger);
933         QString vid_str = cuttedpastedviews[vid].originView();
934 
935         if (vid_str.isEmpty() || !viewidisinteger || vid_int<=0) {
936             //! ignore origin views that have not been created already
937             continue;
938         }
939 
940         qDebug() << "org.kde.latte ViewsDialog::save() removing cut-pasted view :: " << cuttedpastedviews[vid];
941 
942         //! Be Careful: Remove deprecated views from Cut->Paste Action
943         QString origincurrentid = cuttedpastedviews[vid].originLayout();
944         Data::Layout originlayout = m_handler->layoutsController()->originalData(origincurrentid);
945         Latte::CentralLayout *origin = m_handler->layoutsController()->centralLayout(originlayout.id);
946 
947         Data::ViewsTable originviews = Latte::Layouts::Storage::self()->views(origin);
948 
949         if (originviews.containsId(vid_str)) {
950             origin->removeView(originviews[vid_str]);
951         }
952     }
953 
954     //! move active views between different active layouts
955     for (const auto vid: cuttedpastedactiveviews.keys()) {
956         Data::View pastedactiveview = cuttedpastedactiveviews[vid];
957         uint originviewid = pastedactiveview.originView().toUInt();
958         CentralLayout *origin = originLayout(pastedactiveview);
959         QString originlayoutname = origin->name();
960         QString destinationlayoutname = originallayout.name;
961 
962         auto view = origin->viewForContainment(originviewid);
963 
964         QString tempviewid = pastedactiveview.id;
965         pastedactiveview.id = QString::number(originviewid);
966 
967         qDebug() << "org.kde.latte ViewsDialog::save() move to another layout cutted-pasted active view :: " << pastedactiveview;
968 
969         if (view) {
970             //! onscreen_view->onscreen_view
971             //! onscreen_view->offscreen_view
972             pastedactiveview.setState(pastedactiveview.state(), pastedactiveview.originFile(), destinationlayoutname, pastedactiveview.originView());
973             origin->updateView(pastedactiveview);
974         } else {
975             //! offscreen_view->onscreen_view
976             m_handler->corona()->layoutsManager()->moveView(originlayoutname, originviewid, destinationlayoutname);
977             //!is needed in order for layout to not trigger another move
978             pastedactiveview.setState(Data::View::IsCreated, QString(), QString(), QString());
979             central->updateView(pastedactiveview);
980         }
981 
982         pastedactiveview.setState(Data::View::IsCreated, QString(), QString(), QString());
983         newviewsresponses[tempviewid] = pastedactiveview;
984     }
985 
986     //! update
987     if ((removedViews.rowCount() > 0) || (newViews.rowCount() > 0)) {
988         m_handler->corona()->layoutsManager()->synchronizer()->syncActiveLayoutsToOriginalFiles();
989     }
990 
991     //! update model for newly added views
992     for (const auto vid: newviewsresponses.keys()) {
993         m_model->setOriginalView(vid, newviewsresponses[vid]);
994     }
995 
996     //! update all table with latest data and make the original one
997     currentViews = m_model->currentViewsData();
998     m_model->setOriginalData(currentViews);
999 
1000     //! update model activeness
1001     if (central->isActive()) {
1002         m_model->updateActiveStatesBasedOn(central);
1003     }
1004 
1005     //! Clear any templates keeper data in order to produce reupdates if needed
1006     m_handler->layoutsController()->templatesKeeper()->clear();
1007 }
1008 
uniqueViewName(QString name)1009 QString Views::uniqueViewName(QString name)
1010 {
1011     if (name.isEmpty()) {
1012         return name;
1013     }
1014 
1015     int pos_ = name.lastIndexOf(QRegExp(QString(" - [0-9]+")));
1016 
1017     if (m_model->containsCurrentName(name) && pos_ > 0) {
1018         name = name.left(pos_);
1019     }
1020 
1021     int i = 2;
1022 
1023     QString namePart = name;
1024 
1025     while (m_model->containsCurrentName(name)) {
1026         name = namePart + " - " + QString::number(i);
1027         i++;
1028     }
1029 
1030     return name;
1031 }
1032 
visibleViewName(const QString & id) const1033 QString Views::visibleViewName(const QString &id) const
1034 {
1035     if (id.isEmpty()) {
1036         return QString();
1037     }
1038 
1039     Data::View view = m_model->currentData(id);
1040 
1041     if (view.isValid()) {
1042         return view.name;
1043     }
1044 
1045     return QString();
1046 
1047 }
1048 
applyColumnWidths()1049 void Views::applyColumnWidths()
1050 {
1051     m_view->horizontalHeader()->setSectionResizeMode(Model::Views::SUBCONTAINMENTSCOLUMN, QHeaderView::Stretch);
1052 
1053     if (m_viewColumnWidths.count()<(Model::Views::columnCount()-1)) {
1054         return;
1055     }
1056 
1057     m_view->setColumnWidth(Model::Views::IDCOLUMN, m_viewColumnWidths[0].toInt());
1058     m_view->setColumnWidth(Model::Views::NAMECOLUMN, m_viewColumnWidths[1].toInt());
1059     m_view->setColumnWidth(Model::Views::SCREENCOLUMN, m_viewColumnWidths[2].toInt());
1060     m_view->setColumnWidth(Model::Views::EDGECOLUMN, m_viewColumnWidths[3].toInt());
1061     m_view->setColumnWidth(Model::Views::ALIGNMENTCOLUMN, m_viewColumnWidths[4].toInt());
1062 }
1063 
storeColumnWidths()1064 void Views::storeColumnWidths()
1065 {
1066     if (m_viewColumnWidths.isEmpty() || (m_viewColumnWidths.count()<Model::Views::columnCount()-1)) {
1067         m_viewColumnWidths.clear();
1068         for (int i=0; i<Model::Views::columnCount(); ++i) {
1069             m_viewColumnWidths << "";
1070         }
1071     }
1072 
1073     m_viewColumnWidths[0] = QString::number(m_view->columnWidth(Model::Views::IDCOLUMN));
1074     m_viewColumnWidths[1] = QString::number(m_view->columnWidth(Model::Views::NAMECOLUMN));
1075     m_viewColumnWidths[2] = QString::number(m_view->columnWidth(Model::Views::SCREENCOLUMN));
1076     m_viewColumnWidths[3] = QString::number(m_view->columnWidth(Model::Views::EDGECOLUMN));
1077     m_viewColumnWidths[4] = QString::number(m_view->columnWidth(Model::Views::ALIGNMENTCOLUMN));
1078 }
1079 
loadConfig()1080 void Views::loadConfig()
1081 {
1082     QStringList defaultcolumnwidths;
1083     defaultcolumnwidths << QString::number(59) << QString::number(256) << QString::number(142) << QString::number(135) << QString::number(131);
1084 
1085     m_viewColumnWidths = m_storage.readEntry("columnWidths", defaultcolumnwidths);
1086     m_viewSortColumn = m_storage.readEntry("sortColumn", (int)Model::Views::SCREENCOLUMN);
1087     m_viewSortOrder = static_cast<Qt::SortOrder>(m_storage.readEntry("sortOrder", (int)Qt::AscendingOrder));
1088 }
1089 
saveConfig()1090 void Views::saveConfig()
1091 {
1092     m_storage.writeEntry("columnWidths", m_viewColumnWidths);
1093     m_storage.writeEntry("sortColumn", m_viewSortColumn);
1094     m_storage.writeEntry("sortOrder", (int)m_viewSortOrder);
1095 }
1096 
1097 }
1098 }
1099 }
1100 
1101