1 /****************************************************************************
2 **
3 ** Copyright (C) 2020 Uwe Kindler
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qt Creator.
7 **
8 ** Commercial License Usage
9 ** Licensees holding valid commercial Qt licenses may use this file in
10 ** accordance with the commercial license agreement provided with the
11 ** Software or, alternatively, in accordance with the terms contained in
12 ** a written agreement between you and The Qt Company. For licensing terms
13 ** and conditions see https://www.qt.io/terms-conditions. For further
14 ** information use the contact form at https://www.qt.io/contact-us.
15 **
16 ** GNU Lesser General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU Lesser
18 ** General Public License version 2.1 or (at your option) any later version.
19 ** The licenses are as published by the Free Software Foundation
20 ** and appearing in the file LICENSE.LGPLv21 included in the packaging
21 ** of this file. Please review the following information to ensure
22 ** the GNU Lesser General Public License version 2.1 requirements
23 ** will be met: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
24 **
25 ** GNU General Public License Usage
26 ** Alternatively, this file may be used under the terms of the GNU
27 ** General Public License version 3 or (at your option) any later version
28 ** approved by the KDE Free Qt Foundation. The licenses are as published by
29 ** the Free Software Foundation and appearing in the file LICENSE.GPL3
30 ** included in the packaging of this file. Please review the following
31 ** information to ensure the GNU General Public License requirements will
32 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
33 **
34 ****************************************************************************/
35 
36 #include "dockmanager.h"
37 
38 #include "ads_globals.h"
39 #include "dockareatitlebar.h"
40 #include "dockareawidget.h"
41 #include "dockfocuscontroller.h"
42 #include "dockingstatereader.h"
43 #include "dockoverlay.h"
44 #include "dockwidget.h"
45 #include "dockwidgettab.h"
46 #include "floatingdockcontainer.h"
47 #include "iconprovider.h"
48 
49 #include "workspacedialog.h"
50 
51 #include <utils/algorithm.h>
52 #include <utils/qtcassert.h>
53 
54 #include <algorithm>
55 #include <iostream>
56 
57 #include <QAction>
58 #include <QApplication>
59 #include <QDateTime>
60 #include <QDir>
61 #include <QFile>
62 #include <QFileInfo>
63 #include <QList>
64 #include <QLoggingCategory>
65 #include <QMainWindow>
66 #include <QMap>
67 #include <QMenu>
68 #include <QMessageBox>
69 #include <QSettings>
70 #include <QVariant>
71 #include <QXmlStreamWriter>
72 
73 static Q_LOGGING_CATEGORY(adsLog, "qtc.qmldesigner.advanceddockingsystem", QtWarningMsg)
74 
75 namespace ADS
76 {
77     /**
78      * Internal file version in case the structure changes internally
79      */
80     enum eStateFileVersion {
81         InitialVersion = 0,       //!< InitialVersion
82         Version1 = 1,             //!< Version1
83         CurrentVersion = Version1 //!< CurrentVersion
84     };
85 
86     static DockManager::ConfigFlags g_staticConfigFlags = DockManager::DefaultNonOpaqueConfig;
87 
88     /**
89      * Private data class of DockManager class (pimpl)
90      */
91     class DockManagerPrivate
92     {
93     public:
94         DockManager *q;
95         QList<QPointer<FloatingDockContainer>> m_floatingWidgets;
96         QList<DockContainerWidget *> m_containers;
97         DockOverlay *m_containerOverlay = nullptr;
98         DockOverlay *m_dockAreaOverlay = nullptr;
99         QMap<QString, DockWidget *> m_dockWidgetsMap;
100         bool m_restoringState = false;
101         QVector<FloatingDockContainer *> m_uninitializedFloatingWidgets;
102         DockFocusController *m_focusController = nullptr;
103 
104         QString m_workspaceName;
105         bool m_workspaceListDirty = true;
106         QStringList m_workspaces;
107         QSet<QString> m_workspacePresets;
108         QHash<QString, QDateTime> m_workspaceDateTimes;
109         QString m_workspaceToRestoreAtStartup;
110         bool m_autorestoreLastWorkspace; // This option is set in the Workspace Manager!
111         QSettings *m_settings = nullptr;
112         QString m_workspacePresetsPath;
113         bool m_modeChangeState = false;
114 
115         /**
116          * Private data constructor
117          */
118         DockManagerPrivate(DockManager *parent);
119 
120         /**
121          * Restores the state. If testing is set to true it will check if
122          * the given data stream is a valid docking system state file.
123          */
124         bool restoreStateFromXml(const QByteArray &state,
125                                  int version,
126                                  bool testing = false);
127 
128         /**
129          * Restore state
130          */
131         bool restoreState(const QByteArray &state, int version);
132 
133         void restoreDockWidgetsOpenState();
134         void restoreDockAreasIndices();
135         void emitTopLevelEvents();
136 
hideFloatingWidgets()137         void hideFloatingWidgets()
138         {
139             // Hide updates of floating widgets from user
140             for (auto floatingWidget : qAsConst(m_floatingWidgets))
141                 floatingWidget->hide();
142         }
143 
markDockWidgetsDirty()144         void markDockWidgetsDirty()
145         {
146             for (auto dockWidget : qAsConst(m_dockWidgetsMap))
147                 dockWidget->setProperty("dirty", true);
148         }
149 
150         /**
151          * Restores the container with the given index
152          */
153         bool restoreContainer(int index, DockingStateReader &stream, bool testing);
154 
155         void workspaceLoadingProgress();
156     }; // class DockManagerPrivate
157 
DockManagerPrivate(DockManager * parent)158     DockManagerPrivate::DockManagerPrivate(DockManager *parent)
159         : q(parent)
160     {}
161 
restoreContainer(int index,DockingStateReader & stream,bool testing)162     bool DockManagerPrivate::restoreContainer(int index, DockingStateReader &stream, bool testing)
163     {
164         if (testing)
165             index = 0;
166 
167         bool result = false;
168         if (index >= m_containers.count()) {
169             FloatingDockContainer *floatingWidget = new FloatingDockContainer(q);
170             result = floatingWidget->restoreState(stream, testing);
171         } else {
172             qCInfo(adsLog) << "d->m_containers[i]->restoreState ";
173             auto container = m_containers[index];
174             if (container->isFloating())
175                 result = container->floatingWidget()->restoreState(stream, testing);
176             else
177                 result = container->restoreState(stream, testing);
178         }
179 
180         return result;
181     }
182 
restoreStateFromXml(const QByteArray & state,int version,bool testing)183     bool DockManagerPrivate::restoreStateFromXml(const QByteArray &state, int version, bool testing)
184     {
185         Q_UNUSED(version) // TODO version is not needed, why is it in here in the first place?
186 
187         if (state.isEmpty())
188             return false;
189 
190         DockingStateReader stateReader(state);
191         if (!stateReader.readNextStartElement())
192             return false;
193 
194         if (stateReader.name() != QLatin1String("QtAdvancedDockingSystem"))
195             return false;
196 
197         qCInfo(adsLog) << stateReader.attributes().value("version");
198         bool ok;
199         int v = stateReader.attributes().value("version").toInt(&ok);
200         if (!ok || v > CurrentVersion)
201             return false;
202 
203         stateReader.setFileVersion(v);
204 
205         qCInfo(adsLog) << stateReader.attributes().value("userVersion");
206         // Older files do not support UserVersion but we still want to load them so
207         // we first test if the attribute exists
208         if (!stateReader.attributes().value("userVersion").isEmpty())
209         {
210             v = stateReader.attributes().value("userVersion").toInt(&ok);
211             if (!ok || v != version)
212                 return false;
213         }
214 
215         bool result = true;
216 #ifdef ADS_DEBUG_PRINT
217         int dockContainers = stateReader.attributes().value("containers").toInt();
218         qCInfo(adsLog) << dockContainers;
219 #endif
220         int dockContainerCount = 0;
221         while (stateReader.readNextStartElement()) {
222             if (stateReader.name() == QLatin1String("container")) {
223                 result = restoreContainer(dockContainerCount, stateReader, testing);
224                 if (!result)
225                     break;
226 
227                 dockContainerCount++;
228             }
229         }
230 
231         if (!testing) {
232             // Delete remaining empty floating widgets
233             int floatingWidgetIndex = dockContainerCount - 1;
234             int deleteCount = m_floatingWidgets.count() - floatingWidgetIndex;
235             for (int i = 0; i < deleteCount; ++i) {
236                 m_floatingWidgets[floatingWidgetIndex + i]->deleteLater();
237                 q->removeDockContainer(m_floatingWidgets[floatingWidgetIndex + i]->dockContainer());
238             }
239         }
240 
241         return result;
242     }
243 
restoreDockWidgetsOpenState()244     void DockManagerPrivate::restoreDockWidgetsOpenState()
245     {
246         // All dock widgets, that have not been processed in the restore state
247         // function are invisible to the user now and have no assigned dock area
248         // They do not belong to any dock container, until the user toggles the
249         // toggle view action the next time
250         for (auto dockWidget : qAsConst(m_dockWidgetsMap)) {
251             if (dockWidget->property(internal::dirtyProperty).toBool()) {
252                 dockWidget->flagAsUnassigned();
253                 emit dockWidget->viewToggled(false);
254             } else {
255                 dockWidget->toggleViewInternal(
256                     !dockWidget->property(internal::closedProperty).toBool());
257             }
258         }
259     }
260 
restoreDockAreasIndices()261     void DockManagerPrivate::restoreDockAreasIndices()
262     {
263         // Now all dock areas are properly restored and we setup the index of
264         // The dock areas because the previous toggleView() action has changed
265         // the dock area index
266         int count = 0;
267         for (auto dockContainer : qAsConst(m_containers)) {
268             count++;
269             for (int i = 0; i < dockContainer->dockAreaCount(); ++i) {
270                 DockAreaWidget *dockArea = dockContainer->dockArea(i);
271                 QString dockWidgetName = dockArea->property("currentDockWidget").toString();
272                 DockWidget *dockWidget = nullptr;
273                 if (!dockWidgetName.isEmpty())
274                     dockWidget = q->findDockWidget(dockWidgetName);
275 
276                 if (!dockWidget || dockWidget->isClosed()) {
277                     int index = dockArea->indexOfFirstOpenDockWidget();
278                     if (index < 0)
279                         continue;
280 
281                     dockArea->setCurrentIndex(index);
282                 } else {
283                     dockArea->internalSetCurrentDockWidget(dockWidget);
284                 }
285             }
286         }
287     }
288 
emitTopLevelEvents()289     void DockManagerPrivate::emitTopLevelEvents()
290     {
291         // Finally we need to send the topLevelChanged() signals for all dock
292         // widgets if top level changed
293         for (auto dockContainer : qAsConst(m_containers)) {
294             DockWidget *topLevelDockWidget = dockContainer->topLevelDockWidget();
295             if (topLevelDockWidget) {
296                 topLevelDockWidget->emitTopLevelChanged(true);
297             } else {
298                 for (int i = 0; i < dockContainer->dockAreaCount(); ++i) {
299                     auto dockArea = dockContainer->dockArea(i);
300                     for (auto dockWidget : dockArea->dockWidgets())
301                         dockWidget->emitTopLevelChanged(false);
302                 }
303             }
304         }
305     }
306 
restoreState(const QByteArray & state,int version)307     bool DockManagerPrivate::restoreState(const QByteArray &state, int version)
308     {
309         QByteArray currentState = state.startsWith("<?xml") ? state : qUncompress(state);
310         // Check the format of the given data stream
311         if (!restoreStateFromXml(currentState, version, true)) {
312             qCInfo(adsLog) << "checkFormat: Error checking format!!!";
313             return false;
314         }
315 
316         // Hide updates of floating widgets from use
317         hideFloatingWidgets();
318         markDockWidgetsDirty();
319 
320         if (!restoreStateFromXml(currentState, version)) {
321             qCInfo(adsLog) << "restoreState: Error restoring state!!!";
322             return false;
323         }
324 
325         restoreDockWidgetsOpenState();
326         restoreDockAreasIndices();
327         emitTopLevelEvents();
328 
329         return true;
330     }
331 
DockManager(QWidget * parent)332     DockManager::DockManager(QWidget *parent)
333         : DockContainerWidget(this, parent)
334         , d(new DockManagerPrivate(this))
335     {
336         connect(this, &DockManager::workspaceListChanged, this, [=] {
337             d->m_workspaceListDirty = true;
338         });
339 
340         createRootSplitter();
341         QMainWindow *mainWindow = qobject_cast<QMainWindow *>(parent);
342         if (mainWindow) {
343             mainWindow->setCentralWidget(this);
344         }
345 
346         d->m_dockAreaOverlay = new DockOverlay(this, DockOverlay::ModeDockAreaOverlay);
347         d->m_containerOverlay = new DockOverlay(this, DockOverlay::ModeContainerOverlay);
348         d->m_containers.append(this);
349 
350         if (DockManager::configFlags().testFlag(DockManager::FocusHighlighting))
351             d->m_focusController = new DockFocusController(this);
352     }
353 
~DockManager()354     DockManager::~DockManager()
355     {
356         emit aboutToUnloadWorkspace(d->m_workspaceName);
357         save();
358         saveStartupWorkspace();
359 
360         // Using a temporal vector since the destructor of
361         // FloatingDockWidgetContainer alters d->m_floatingWidgets.
362         std::vector<FloatingDockContainer *> aboutToDeletes;
363         for (auto floatingWidget : qAsConst(d->m_floatingWidgets)) {
364             if (floatingWidget)
365                 aboutToDeletes.push_back(floatingWidget);
366         }
367 
368         for (auto del : aboutToDeletes) {
369             delete del;
370         }
371 
372         d->m_floatingWidgets.clear();
373 
374         delete d;
375     }
376 
configFlags()377     DockManager::ConfigFlags DockManager::configFlags() { return g_staticConfigFlags; }
378 
setConfigFlags(const ConfigFlags flags)379     void DockManager::setConfigFlags(const ConfigFlags flags) { g_staticConfigFlags = flags; }
380 
setConfigFlag(eConfigFlag flag,bool on)381     void DockManager::setConfigFlag(eConfigFlag flag, bool on)
382     {
383         internal::setFlag(g_staticConfigFlags, flag, on);
384     }
385 
testConfigFlag(eConfigFlag flag)386     bool DockManager::testConfigFlag(eConfigFlag flag)
387     {
388         return configFlags().testFlag(flag);
389     }
390 
iconProvider()391     IconProvider &DockManager::iconProvider()
392     {
393         static IconProvider instance;
394         return instance;
395     }
396 
startDragDistance()397     int DockManager::startDragDistance()
398     {
399         return static_cast<int>(QApplication::startDragDistance() * 1.5);
400     }
401 
setSettings(QSettings * settings)402     void DockManager::setSettings(QSettings *settings) { d->m_settings = settings; }
403 
setWorkspacePresetsPath(const QString & path)404     void DockManager::setWorkspacePresetsPath(const QString &path) { d->m_workspacePresetsPath = path; }
405 
addDockWidget(DockWidgetArea area,DockWidget * dockWidget,DockAreaWidget * dockAreaWidget)406     DockAreaWidget *DockManager::addDockWidget(DockWidgetArea area,
407                                                DockWidget *dockWidget,
408                                                DockAreaWidget *dockAreaWidget)
409     {
410         d->m_dockWidgetsMap.insert(dockWidget->objectName(), dockWidget);
411         return DockContainerWidget::addDockWidget(area, dockWidget, dockAreaWidget);
412     }
413 
initialize()414     void DockManager::initialize()
415     {
416         syncWorkspacePresets();
417 
418         QString workspace = ADS::Constants::DEFAULT_WORKSPACE;
419 
420         // Determine workspace to restore at startup
421         if (autoRestorLastWorkspace()) {
422             QString lastWS = lastWorkspace();
423             if (!lastWS.isEmpty() && workspaces().contains(lastWS))
424                 workspace = lastWS;
425             else
426                 qDebug() << "Couldn't restore last workspace!";
427         }
428 
429         openWorkspace(workspace);
430     }
431 
addDockWidgetTab(DockWidgetArea area,DockWidget * dockWidget)432     DockAreaWidget *DockManager::addDockWidgetTab(DockWidgetArea area, DockWidget *dockWidget)
433     {
434         DockAreaWidget *areaWidget = lastAddedDockAreaWidget(area);
435         if (areaWidget)
436             return addDockWidget(ADS::CenterDockWidgetArea, dockWidget, areaWidget);
437         else if (!openedDockAreas().isEmpty())
438             return addDockWidget(area, dockWidget, openedDockAreas().constLast());
439         else
440             return addDockWidget(area, dockWidget, nullptr);
441     }
442 
addDockWidgetTabToArea(DockWidget * dockWidget,DockAreaWidget * dockAreaWidget)443     DockAreaWidget *DockManager::addDockWidgetTabToArea(DockWidget *dockWidget,
444                                                         DockAreaWidget *dockAreaWidget)
445     {
446         return addDockWidget(ADS::CenterDockWidgetArea, dockWidget, dockAreaWidget);
447     }
448 
addDockWidgetFloating(DockWidget * dockWidget)449     FloatingDockContainer *DockManager::addDockWidgetFloating(DockWidget *dockWidget)
450     {
451         d->m_dockWidgetsMap.insert(dockWidget->objectName(), dockWidget);
452         DockAreaWidget *oldDockArea = dockWidget->dockAreaWidget();
453         if (oldDockArea)
454             oldDockArea->removeDockWidget(dockWidget);
455 
456         dockWidget->setDockManager(this);
457         FloatingDockContainer *floatingWidget = new FloatingDockContainer(dockWidget);
458         floatingWidget->resize(dockWidget->size());
459         if (isVisible())
460             floatingWidget->show();
461         else
462             d->m_uninitializedFloatingWidgets.append(floatingWidget);
463 
464         return floatingWidget;
465     }
466 
registerFloatingWidget(FloatingDockContainer * floatingWidget)467     void DockManager::registerFloatingWidget(FloatingDockContainer *floatingWidget)
468     {
469         d->m_floatingWidgets.append(floatingWidget);
470         emit floatingWidgetCreated(floatingWidget);
471         qCInfo(adsLog) << "d->FloatingWidgets.count() " << d->m_floatingWidgets.count();
472     }
473 
removeFloatingWidget(FloatingDockContainer * floatingWidget)474     void DockManager::removeFloatingWidget(FloatingDockContainer *floatingWidget)
475     {
476         d->m_floatingWidgets.removeAll(floatingWidget);
477     }
478 
registerDockContainer(DockContainerWidget * dockContainer)479     void DockManager::registerDockContainer(DockContainerWidget *dockContainer)
480     {
481         d->m_containers.append(dockContainer);
482     }
483 
removeDockContainer(DockContainerWidget * dockContainer)484     void DockManager::removeDockContainer(DockContainerWidget *dockContainer)
485     {
486         if (this != dockContainer)
487             d->m_containers.removeAll(dockContainer);
488     }
489 
containerOverlay() const490     DockOverlay *DockManager::containerOverlay() const { return d->m_containerOverlay; }
491 
dockAreaOverlay() const492     DockOverlay *DockManager::dockAreaOverlay() const { return d->m_dockAreaOverlay; }
493 
dockContainers() const494     const QList<DockContainerWidget *> DockManager::dockContainers() const
495     {
496         return d->m_containers;
497     }
498 
floatingWidgets() const499     const QList<QPointer<FloatingDockContainer>> DockManager::floatingWidgets() const
500     {
501         return d->m_floatingWidgets;
502     }
503 
zOrderIndex() const504     unsigned int DockManager::zOrderIndex() const { return 0; }
505 
saveState(int version) const506     QByteArray DockManager::saveState(int version) const
507     {
508         QByteArray xmlData;
509         QXmlStreamWriter stream(&xmlData);
510         auto configFlags = DockManager::configFlags();
511         stream.setAutoFormatting(configFlags.testFlag(XmlAutoFormattingEnabled));
512         stream.writeStartDocument();
513         stream.writeStartElement("QtAdvancedDockingSystem");
514         stream.writeAttribute("version", QString::number(CurrentVersion));
515         stream.writeAttribute("userVersion", QString::number(version));
516         stream.writeAttribute("containers", QString::number(d->m_containers.count()));
517         for (auto container : qAsConst(d->m_containers))
518             container->saveState(stream);
519 
520         stream.writeEndElement();
521         stream.writeEndDocument();
522         return xmlData;
523     }
524 
restoreState(const QByteArray & state,int version)525     bool DockManager::restoreState(const QByteArray &state, int version)
526     {
527         // Prevent multiple calls as long as state is not restore. This may
528         // happen, if QApplication::processEvents() is called somewhere
529         if (d->m_restoringState)
530             return false;
531 
532         // We hide the complete dock manager here. Restoring the state means
533         // that DockWidgets are removed from the DockArea internal stack layout
534         // which in turn  means, that each time a widget is removed the stack
535         // will show and raise the next available widget which in turn
536         // triggers show events for the dock widgets. To avoid this we hide the
537         // dock manager. Because there will be no processing of application
538         // events until this function is finished, the user will not see this
539         // hiding
540         bool isHidden = this->isHidden();
541         if (!isHidden)
542             hide();
543 
544         d->m_restoringState = true;
545         emit restoringState();
546         bool result = d->restoreState(state, version);
547         d->m_restoringState = false;
548         if (!isHidden)
549             show();
550 
551         emit stateRestored();
552         return result;
553     }
554 
showEvent(QShowEvent * event)555     void DockManager::showEvent(QShowEvent *event)
556     {
557         Super::showEvent(event);
558         if (d->m_uninitializedFloatingWidgets.empty())
559             return;
560 
561         for (auto floatingWidget : qAsConst(d->m_uninitializedFloatingWidgets))
562             floatingWidget->show();
563 
564         d->m_uninitializedFloatingWidgets.clear();
565     }
566 
findDockWidget(const QString & objectName) const567     DockWidget *DockManager::findDockWidget(const QString &objectName) const
568     {
569         return d->m_dockWidgetsMap.value(objectName, nullptr);
570     }
571 
removeDockWidget(DockWidget * dockWidget)572     void DockManager::removeDockWidget(DockWidget *dockWidget)
573     {
574         emit dockWidgetAboutToBeRemoved(dockWidget);
575         d->m_dockWidgetsMap.remove(dockWidget->objectName());
576         DockContainerWidget::removeDockWidget(dockWidget);
577         emit dockWidgetRemoved(dockWidget);
578     }
579 
dockWidgetsMap() const580     QMap<QString, DockWidget *> DockManager::dockWidgetsMap() const { return d->m_dockWidgetsMap; }
581 
isRestoringState() const582     bool DockManager::isRestoringState() const { return d->m_restoringState; }
583 
showWorkspaceMananger()584     void DockManager::showWorkspaceMananger()
585     {
586         save(); // Save current workspace
587 
588         WorkspaceDialog workspaceDialog(this, parentWidget());
589         workspaceDialog.setAutoLoadWorkspace(autoRestorLastWorkspace());
590         workspaceDialog.exec();
591 
592         QTC_ASSERT(d->m_settings, return );
593         d->m_settings->setValue(Constants::AUTO_RESTORE_WORKSPACE_SETTINGS_KEY,
594                                 workspaceDialog.autoLoadWorkspace());
595     }
596 
isWorkspacePreset(const QString & workspace) const597     bool DockManager::isWorkspacePreset(const QString &workspace) const
598     {
599         return d->m_workspacePresets.contains(workspace);
600     }
601 
save()602     bool DockManager::save()
603     {
604         if (isModeChangeState())
605             return false;
606 
607         emit aboutToSaveWorkspace();
608 
609         bool result = write(activeWorkspace(), saveState(), parentWidget());
610         if (result)
611             d->m_workspaceDateTimes.insert(activeWorkspace(), QDateTime::currentDateTime());
612         else
613             QMessageBox::warning(parentWidget(),
614                                  tr("Cannot Save Workspace"),
615                                  tr("Could not save workspace to file %1")
616                                      .arg(workspaceNameToFilePath(d->m_workspaceName)
617                                               .toUserOutput()));
618 
619         return result;
620     }
621 
activeWorkspace() const622     QString DockManager::activeWorkspace() const { return d->m_workspaceName; }
623 
lastWorkspace() const624     QString DockManager::lastWorkspace() const
625     {
626         QTC_ASSERT(d->m_settings, return {});
627         return d->m_settings->value(Constants::STARTUP_WORKSPACE_SETTINGS_KEY).toString();
628     }
629 
autoRestorLastWorkspace() const630     bool DockManager::autoRestorLastWorkspace() const
631     {
632         QTC_ASSERT(d->m_settings, return false);
633         return d->m_settings->value(Constants::AUTO_RESTORE_WORKSPACE_SETTINGS_KEY).toBool();
634     }
635 
636     const QString m_dirName = QLatin1String("workspaces");
637     const QString m_fileExt = QLatin1String(".wrk"); // TODO
638 
workspaceFileExtension() const639     QString DockManager::workspaceFileExtension() const { return m_fileExt; }
640 
workspaces()641     QStringList DockManager::workspaces()
642     {
643         if (d->m_workspaces.isEmpty() || d->m_workspaceListDirty) {
644             auto tmp = Utils::toSet(d->m_workspaces);
645 
646             QTC_ASSERT(d->m_settings, return {});
647             QDir workspaceDir(QFileInfo(d->m_settings->fileName()).path() + QLatin1Char('/')
648                               + m_dirName);
649             QFileInfoList workspaceFiles
650                 = workspaceDir.entryInfoList(QStringList() << QLatin1Char('*') + m_fileExt,
651                                              QDir::NoFilter,
652                                              QDir::Time);
653             for (const QFileInfo &fileInfo : workspaceFiles) {
654                 QString workspaceName = fileNameToWorkspaceName(fileInfo.completeBaseName());
655                 d->m_workspaceDateTimes.insert(workspaceName, fileInfo.lastModified());
656                 tmp.insert(workspaceName);
657             }
658 
659             d->m_workspaceListDirty = false;
660             d->m_workspaces = Utils::toList(tmp);
661         }
662         return d->m_workspaces;
663     }
664 
workspacePresets() const665     QSet<QString> DockManager::workspacePresets() const
666     {
667         if (d->m_workspacePresets.isEmpty()) {
668             QDir workspacePresetsDir(d->m_workspacePresetsPath);
669             QFileInfoList workspacePresetsFiles
670                 = workspacePresetsDir.entryInfoList(QStringList() << QLatin1Char('*') + m_fileExt,
671                                                     QDir::NoFilter,
672                                                     QDir::Time);
673             for (const QFileInfo &fileInfo : workspacePresetsFiles)
674                 d->m_workspacePresets.insert(fileNameToWorkspaceName(fileInfo.completeBaseName()));
675         }
676         return d->m_workspacePresets;
677     }
678 
workspaceDateTime(const QString & workspace) const679     QDateTime DockManager::workspaceDateTime(const QString &workspace) const
680     {
681         return d->m_workspaceDateTimes.value(workspace);
682     }
683 
workspaceNameToFilePath(const QString & workspaceName) const684     Utils::FilePath DockManager::workspaceNameToFilePath(const QString &workspaceName) const
685     {
686         QTC_ASSERT(d->m_settings, return {});
687         return Utils::FilePath::fromString(
688             QFileInfo(d->m_settings->fileName()).path() + QLatin1Char('/') + m_dirName
689             + QLatin1Char('/') + workspaceNameToFileName(workspaceName));
690     }
691 
fileNameToWorkspaceName(const QString & fileName) const692     QString DockManager::fileNameToWorkspaceName(const QString &fileName) const
693     {
694         QString copy = QFileInfo(fileName).baseName();
695         copy.replace("_", " ");
696         return copy;
697     }
698 
workspaceNameToFileName(const QString & workspaceName) const699     QString DockManager::workspaceNameToFileName(const QString &workspaceName) const
700     {
701         QString copy = workspaceName;
702         copy.replace(" ", "_");
703         copy.append(m_fileExt);
704         return copy;
705     }
706 
707     /**
708      * Creates \a workspace, but does not actually create the file.
709      */
createWorkspace(const QString & workspace)710     bool DockManager::createWorkspace(const QString &workspace)
711     {
712         if (workspaces().contains(workspace))
713             return false;
714 
715         bool result = write(workspace, saveState(), parentWidget());
716         if (result) {
717             d->m_workspaces.insert(1, workspace);
718             d->m_workspaceDateTimes.insert(workspace, QDateTime::currentDateTime());
719             emit workspaceListChanged();
720         } else {
721             QMessageBox::warning(parentWidget(),
722                                  tr("Cannot Save Workspace"),
723                                  tr("Could not save workspace to file %1")
724                                      .arg(workspaceNameToFilePath(d->m_workspaceName)
725                                               .toUserOutput()));
726         }
727 
728         return result;
729     }
730 
openWorkspace(const QString & workspace)731     bool DockManager::openWorkspace(const QString &workspace)
732     {
733         // Do nothing if we have that workspace already loaded, exception if it is
734         // a preset workspace. In this case we still want to be able to load the
735         // default workspace to undo potential user changes.
736         if (workspace == d->m_workspaceName && !isWorkspacePreset(workspace))
737             return true;
738 
739         if (!workspaces().contains(workspace))
740             return false;
741 
742         // Check if the currently active workspace isn't empty and try to save it
743         if (!d->m_workspaceName.isEmpty()) {
744             // Allow everyone to set something in the workspace and before saving
745             emit aboutToUnloadWorkspace(d->m_workspaceName);
746             if (!save())
747                 return false;
748         }
749 
750         // Try loading the file
751         QByteArray data = loadWorkspace(workspace);
752         if (data.isEmpty())
753             return false;
754 
755         emit openingWorkspace(workspace);
756         // If data was loaded from file try to restore its state
757         if (!data.isNull() && !restoreState(data))
758             return false;
759 
760         d->m_workspaceName = workspace;
761         emit workspaceLoaded(workspace);
762 
763         return true;
764     }
765 
reloadActiveWorkspace()766     bool DockManager::reloadActiveWorkspace()
767     {
768         if (!workspaces().contains(activeWorkspace()))
769             return false;
770 
771         // Try loading the file
772         QByteArray data = loadWorkspace(activeWorkspace());
773         if (data.isEmpty())
774             return false;
775 
776         // If data was loaded from file try to restore its state
777         if (!data.isNull() && !restoreState(data))
778             return false;
779 
780         emit workspaceReloaded(activeWorkspace());
781 
782         return true;
783     }
784 
785     /**
786      * \brief Shows a dialog asking the user to confirm deleting the workspace \p workspace
787      */
confirmWorkspaceDelete(const QStringList & workspace)788     bool DockManager::confirmWorkspaceDelete(const QStringList &workspace)
789     {
790         const QString title = workspace.size() == 1 ? tr("Delete Workspace")
791                                                     : tr("Delete Workspaces");
792         const QString question = workspace.size() == 1
793                                      ? tr("Delete workspace %1?").arg(workspace.first())
794                                      : tr("Delete these workspaces?\n    %1")
795                                            .arg(workspace.join("\n    "));
796         return QMessageBox::question(parentWidget(),
797                                      title,
798                                      question,
799                                      QMessageBox::Yes | QMessageBox::No)
800                == QMessageBox::Yes;
801     }
802 
803     /**
804      * Deletes \a workspace name from workspace list and the file from disk.
805      */
deleteWorkspace(const QString & workspace)806     bool DockManager::deleteWorkspace(const QString &workspace)
807     {
808         // Remove workspace from internal list
809         if (!d->m_workspaces.contains(workspace))
810             return false;
811 
812         // Remove corresponding workspace file
813         QFile fi(workspaceNameToFilePath(workspace).toString());
814         if (fi.exists()) {
815             if (fi.remove()) {
816                 d->m_workspaces.removeOne(workspace);
817                 emit workspacesRemoved();
818                 emit workspaceListChanged();
819                 return true;
820             }
821         }
822 
823         return false;
824     }
825 
deleteWorkspaces(const QStringList & workspaces)826     void DockManager::deleteWorkspaces(const QStringList &workspaces)
827     {
828         for (const QString &workspace : workspaces)
829             deleteWorkspace(workspace);
830     }
831 
cloneWorkspace(const QString & original,const QString & clone)832     bool DockManager::cloneWorkspace(const QString &original, const QString &clone)
833     {
834         if (!d->m_workspaces.contains(original))
835             return false;
836 
837         QFile fi(workspaceNameToFilePath(original).toString());
838         // If the file does not exist, we can still clone
839         if (!fi.exists() || fi.copy(workspaceNameToFilePath(clone).toString())) {
840             d->m_workspaces.insert(1, clone);
841             d->m_workspaceDateTimes
842                 .insert(clone, workspaceNameToFilePath(clone).lastModified());
843             emit workspaceListChanged();
844             return true;
845         }
846         return false;
847     }
848 
renameWorkspace(const QString & original,const QString & newName)849     bool DockManager::renameWorkspace(const QString &original, const QString &newName)
850     {
851         if (!cloneWorkspace(original, newName))
852             return false;
853 
854         if (original == activeWorkspace())
855             openWorkspace(newName);
856 
857         return deleteWorkspace(original);
858     }
859 
resetWorkspacePreset(const QString & workspace)860     bool DockManager::resetWorkspacePreset(const QString &workspace)
861     {
862         if (!isWorkspacePreset(workspace))
863             return false;
864 
865         Utils::FilePath fileName = workspaceNameToFilePath(workspace);
866 
867         if (!QFile::remove(fileName.toString()))
868             return false;
869 
870         QDir presetsDir(d->m_workspacePresetsPath);
871         bool result = QFile::copy(presetsDir.filePath(workspaceNameToFileName(workspace)),
872                                   fileName.toString());
873         if (result)
874             d->m_workspaceDateTimes.insert(workspace, QDateTime::currentDateTime());
875 
876         return result;
877     }
878 
setModeChangeState(bool value)879     void DockManager::setModeChangeState(bool value)
880     {
881         d->m_modeChangeState = value;
882     }
883 
isModeChangeState() const884     bool DockManager::isModeChangeState() const
885     {
886         return d->m_modeChangeState;
887     }
888 
importWorkspace(const QString & workspace)889     void DockManager::importWorkspace(const QString &workspace)
890     {
891         // Extract workspace name
892         QString workspaceName = fileNameToWorkspaceName(workspace);
893 
894         // Check if the workspace is already contained in the list of workspaces. If that is the case
895         // add a counter to the workspace name.
896         if (workspaces().contains(workspaceName)) {
897             int i = 2;
898             QString copy;
899             do {
900                 copy = workspaceName + QLatin1String(" (") + QString::number(i) + QLatin1Char(')');
901                 ++i;
902             } while (workspaces().contains(copy));
903             workspaceName = copy;
904         }
905 
906         QString fileName = workspaceNameToFileName(workspaceName);
907         QFile file(workspace);
908         if (!file.exists()) {
909             qCInfo(adsLog) << QString("File doesn't exist '%1'").arg(workspace);
910             return;
911         }
912 
913         QDir workspaceDir(QFileInfo(d->m_settings->fileName()).path() + QLatin1Char('/') + m_dirName);
914 
915         if (!file.copy(workspaceDir.filePath(fileName))) {
916             qCInfo(adsLog) << QString("Could not copy '%1' to '%2' error: %3").arg(
917                 workspace, workspaceDir.filePath(fileName), file.errorString());
918         } else {
919             d->m_workspaces.insert(1, workspaceName);
920             d->m_workspaceDateTimes.insert(workspaceName,
921                                            workspaceNameToFilePath(workspaceName).lastModified());
922             d->m_workspaceListDirty = true;
923             // After importing the workspace, update the workspace list
924             workspaces();
925             emit workspaceListChanged();
926         }
927     }
928 
exportWorkspace(const QString & target,const QString & workspace)929     void DockManager::exportWorkspace(const QString &target, const QString &workspace)
930     {
931         // If we came this far the user decided that in case the target already exists to overwrite it.
932         // We first need to remove the existing file, otherwise QFile::copy() will fail.
933         QFileInfo targetFileInfo(target);
934 
935         // Remove the file which supposed to be overwritten
936         if (targetFileInfo.exists()) {
937             QFile fi(targetFileInfo.absoluteFilePath());
938             if (!fi.remove()) {
939                 qCInfo(adsLog) << QString("Couldn't remove '%1'").arg(targetFileInfo.absoluteFilePath());
940                 return;
941             }
942         }
943 
944         // Check if the target directory exists
945         if (!targetFileInfo.absoluteDir().exists()) {
946             qCInfo(adsLog) << QString("Directory doesn't exist '%1'").arg(targetFileInfo.dir().dirName());
947             return;
948         }
949 
950         // Check if the workspace exists
951         Utils::FilePath workspaceFilePath = workspaceNameToFilePath(workspace);
952         if (!workspaceFilePath.exists()) {
953            qCInfo(adsLog) << QString("Workspace doesn't exist '%1'").arg(workspaceFilePath.toString());
954            return;
955         }
956 
957         // Finally copy the workspace to the target
958         QFile workspaceFile(workspaceFilePath.toString());
959         if (!workspaceFile.copy(targetFileInfo.absoluteFilePath())) {
960             qCInfo(adsLog) << QString("Could not copy '%1' to '%2' error: %3").arg(
961                 workspace, workspaceFilePath.toString(), workspaceFile.errorString());
962         }
963     }
964 
write(const QString & workspace,const QByteArray & data,QString * errorString) const965     bool DockManager::write(const QString &workspace, const QByteArray &data, QString *errorString) const
966     {
967         Utils::FilePath fileName = workspaceNameToFilePath(workspace);
968 
969         QDir tmp;
970         tmp.mkpath(fileName.toFileInfo().path());
971         Utils::FileSaver fileSaver(fileName, QIODevice::Text);
972         if (!fileSaver.hasError())
973             fileSaver.write(data);
974 
975         bool ok = fileSaver.finalize();
976 
977         if (!ok && errorString)
978             *errorString = fileSaver.errorString();
979 
980         return ok;
981     }
982 
write(const QString & workspace,const QByteArray & data,QWidget * parent) const983     bool DockManager::write(const QString &workspace, const QByteArray &data, QWidget *parent) const
984     {
985         QString errorString;
986         const bool success = write(workspace, data, &errorString);
987         if (!success)
988             QMessageBox::critical(parent,
989                                   QCoreApplication::translate("Utils::FileSaverBase", "File Error"),
990                                   errorString);
991         return success;
992     }
993 
loadWorkspace(const QString & workspace) const994     QByteArray DockManager::loadWorkspace(const QString &workspace) const
995     {
996         QByteArray data;
997         Utils::FilePath fileName = workspaceNameToFilePath(workspace);
998         if (fileName.exists()) {
999             QFile file(fileName.toString());
1000             if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
1001                 QMessageBox::warning(parentWidget(),
1002                                      tr("Cannot Restore Workspace"),
1003                                      tr("Could not restore workspace %1")
1004                                          .arg(fileName.toUserOutput()));
1005                 return data;
1006             }
1007             data = file.readAll();
1008             file.close();
1009         }
1010         return data;
1011     }
1012 
syncWorkspacePresets()1013     void DockManager::syncWorkspacePresets()
1014     {
1015         // Get a list of all workspace presets
1016         QSet<QString> presets = workspacePresets();
1017 
1018         // Get a list of all available workspaces
1019         QSet<QString> availableWorkspaces = Utils::toSet(workspaces());
1020         presets.subtract(availableWorkspaces);
1021 
1022         // Copy all missing workspace presets over to the local workspace folder
1023         QDir presetsDir(d->m_workspacePresetsPath);
1024         QDir workspaceDir(QFileInfo(d->m_settings->fileName()).path() + QLatin1Char('/') + m_dirName);
1025         // Try do create the 'workspaces' directory if it doesn't exist already
1026         workspaceDir.mkpath(workspaceDir.absolutePath());
1027         if (!workspaceDir.exists()) {
1028             qCInfo(adsLog) << QString("Could not make directory '%1')").arg(workspaceDir.absolutePath());
1029             return;
1030         }
1031 
1032         for (const auto &preset : presets) {
1033             QString fileName = workspaceNameToFileName(preset);
1034             QString filePath = presetsDir.filePath(fileName);
1035             QFile file(filePath);
1036 
1037             if (file.exists()) {
1038                 if (!file.copy(workspaceDir.filePath(fileName)))
1039                     qCInfo(adsLog) << QString("Could not copy '%1' to '%2' error: %3").arg(
1040                         filePath, workspaceDir.filePath(fileName), file.errorString());
1041 
1042                 d->m_workspaceListDirty = true;
1043             }
1044         }
1045 
1046         // After copying over missing workspace presets, update the workspace list
1047         workspaces();
1048     }
1049 
saveStartupWorkspace()1050     void DockManager::saveStartupWorkspace()
1051     {
1052         QTC_ASSERT(d->m_settings, return );
1053         d->m_settings->setValue(Constants::STARTUP_WORKSPACE_SETTINGS_KEY, activeWorkspace());
1054     }
1055 
notifyWidgetOrAreaRelocation(QWidget * droppedWidget)1056     void DockManager::notifyWidgetOrAreaRelocation(QWidget *droppedWidget)
1057     {
1058         if (d->m_focusController)
1059             d->m_focusController->notifyWidgetOrAreaRelocation(droppedWidget);
1060     }
1061 
notifyFloatingWidgetDrop(FloatingDockContainer * floatingWidget)1062     void DockManager::notifyFloatingWidgetDrop(FloatingDockContainer *floatingWidget)
1063     {
1064         if (d->m_focusController)
1065             d->m_focusController->notifyFloatingWidgetDrop(floatingWidget);
1066     }
1067 
setDockWidgetFocused(DockWidget * dockWidget)1068     void DockManager::setDockWidgetFocused(DockWidget *dockWidget)
1069     {
1070         if (d->m_focusController)
1071             d->m_focusController->setDockWidgetFocused(dockWidget);
1072     }
1073 
1074 } // namespace ADS
1075