// SPDX-License-Identifier: LGPL-2.1-or-later /**************************************************************************** * * * Copyright (c) 2024 The FreeCAD Project Association AISBL * * * * This file is part of FreeCAD. * * * * FreeCAD is free software: you can redistribute it and/or modify it * * under the terms of the GNU Lesser General Public License as * * published by the Free Software Foundation, either version 2.1 of the * * License, or (at your option) any later version. * * * * FreeCAD is distributed in the hope that it will be useful, but * * WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * * Lesser General Public License for more details. * * * * You should have received a copy of the GNU Lesser General Public * * License along with FreeCAD. If not, see * * . * * * ***************************************************************************/ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "StartView.h" #include "FileCardDelegate.h" #include "FileCardView.h" #include "FirstStartWidget.h" #include "FlowLayout.h" #include "NewFileButton.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace StartGui; TYPESYSTEM_SOURCE_ABSTRACT(StartGui::StartView, Gui::MDIView) // NOLINT StartView::StartView(QWidget* parent) : Gui::MDIView(nullptr, parent) , _contents(new QStackedWidget(parent)) , _newFileLabel {nullptr} , _examplesLabel {nullptr} , _recentFilesLabel {nullptr} , _customFolderLabel {nullptr} , _showOnStartupCheckBox {nullptr} { setObjectName(QLatin1String("StartView")); auto hGrp = App::GetApplication().GetParameterGroupByPath( "User parameter:BaseApp/Preferences/Mod/Start" ); auto cardSpacing = hGrp->GetInt("FileCardSpacing", 15); // NOLINT auto showExamples = hGrp->GetBool("ShowExamples", true); // NOLINT // Verify that the folder specified in preferences is available before showing it std::string customFolder(hGrp->GetASCII("CustomFolder", "")); bool showCustomFolder = false; if (!customFolder.empty()) { showCustomFolder = true; } // First start page auto firstStartScrollArea = gsl::owner(new QScrollArea()); auto firstStartScrollWidget = gsl::owner(new QWidget(firstStartScrollArea)); firstStartScrollArea->setWidget(firstStartScrollWidget); firstStartScrollArea->setWidgetResizable(true); auto firstStartRegion = gsl::owner(new QHBoxLayout(firstStartScrollWidget)); firstStartRegion->setAlignment(Qt::AlignCenter); auto firstStartWidget = gsl::owner(new FirstStartWidget(this)); connect(firstStartWidget, &FirstStartWidget::dismissed, this, &StartView::firstStartWidgetDismissed); firstStartRegion->addWidget(firstStartWidget); _contents->addWidget(firstStartScrollArea); // Documents page auto documentsWidget = gsl::owner(new QWidget()); _contents->addWidget(documentsWidget); auto documentsMainLayout = gsl::owner(new QVBoxLayout()); documentsWidget->setLayout(documentsMainLayout); auto documentsScrollArea = gsl::owner(new QScrollArea()); documentsScrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAsNeeded); documentsMainLayout->addWidget(documentsScrollArea); auto documentsScrollWidget = gsl::owner(new QWidget(documentsScrollArea)); documentsScrollArea->setWidget(documentsScrollWidget); documentsScrollArea->setWidgetResizable(true); auto documentsContentLayout = gsl::owner(new QVBoxLayout(documentsScrollWidget)); documentsContentLayout->setSizeConstraint(QLayout::SizeConstraint::SetMinAndMaxSize); _newFileLabel = gsl::owner(new QLabel()); documentsContentLayout->addWidget(_newFileLabel); auto createNewRow = gsl::owner(new QWidget); auto flowLayout = gsl::owner(new FlowLayout); // Reset margins of layout to provide consistent spacing flowLayout->setContentsMargins({}); // This allows new file widgets to be targeted via QSS createNewRow->setObjectName(QStringLiteral("CreateNewRow")); createNewRow->setLayout(flowLayout); documentsContentLayout->addWidget(createNewRow); configureNewFileButtons(flowLayout); _recentFilesLabel = gsl::owner(new QLabel()); documentsContentLayout->addWidget(_recentFilesLabel); auto recentFilesListWidget = gsl::owner(new FileCardView(_contents)); connect(recentFilesListWidget, &QListView::clicked, this, &StartView::fileCardSelected); documentsContentLayout->addWidget(recentFilesListWidget); auto customFolderListWidget = gsl::owner(new FileCardView(_contents)); customFolderListWidget->setVisible(showCustomFolder); _customFolderLabel = gsl::owner(new QLabel()); _customFolderLabel->setVisible(showCustomFolder); documentsContentLayout->addWidget(_customFolderLabel); connect(customFolderListWidget, &QListView::clicked, this, &StartView::fileCardSelected); documentsContentLayout->addWidget(customFolderListWidget); auto examplesListWidget = gsl::owner(new FileCardView(_contents)); examplesListWidget->setVisible(showExamples); _examplesLabel = gsl::owner(new QLabel()); _examplesLabel->setVisible(showExamples); documentsContentLayout->addWidget(_examplesLabel); connect(examplesListWidget, &QListView::clicked, this, &StartView::fileCardSelected); documentsContentLayout->addWidget(examplesListWidget); documentsContentLayout->setSpacing(static_cast(cardSpacing)); documentsContentLayout->addStretch(); // Documents page footer auto footerLayout = gsl::owner(new QHBoxLayout()); documentsMainLayout->addLayout(footerLayout); _openFirstStart = gsl::owner(new QPushButton()); _openFirstStart->setIcon(QIcon(QLatin1String(":/icons/preferences-general.svg"))); connect(_openFirstStart, &QPushButton::clicked, this, &StartView::openFirstStartClicked); _showOnStartupCheckBox = gsl::owner(new QCheckBox()); bool showOnStartup = hGrp->GetBool("ShowOnStartup", true); _showOnStartupCheckBox->setCheckState( showOnStartup ? Qt::CheckState::Unchecked : Qt::CheckState::Checked ); connect(_showOnStartupCheckBox, &QCheckBox::toggled, this, &StartView::showOnStartupChanged); footerLayout->addWidget(_openFirstStart); footerLayout->addStretch(); footerLayout->addWidget(_showOnStartupCheckBox); setCentralWidget(_contents); // Set startup widget according to the first start parameter auto firstStart = hGrp->GetBool("FirstStart2024", true); // NOLINT _contents->setCurrentWidget(firstStart ? firstStartScrollArea : documentsWidget); configureCustomFolderListWidget(customFolderListWidget); configureExamplesListWidget(examplesListWidget); configureRecentFilesListWidget(recentFilesListWidget, _recentFilesLabel); QTimer::singleShot(2000, [this, recentFilesListWidget]() { auto updateFun = [this, recentFilesListWidget]() { configureRecentFilesListWidget(recentFilesListWidget, _recentFilesLabel); }; auto recentFiles = Gui::getMainWindow()->findChild(); if (recentFiles != nullptr) { connect(recentFiles, &Gui::RecentFilesAction::recentFilesListModified, this, updateFun); } }); isInitialized = true; retranslateUi(); } void StartView::configureNewFileButtons(QLayout* layout) const { auto newEmptyFile = gsl::owner(new NewFileButton( {tr("Empty File"), tr("Creates a new empty FreeCAD file"), QLatin1String(":/icons/document-new.svg")} )); auto openFile = gsl::owner(new NewFileButton( {tr("Open File"), tr("Opens an existing CAD file or 3D model"), QLatin1String(":/icons/document-open.svg")} )); auto partDesign = gsl::owner(new NewFileButton( {tr("Parametric Body"), tr("Creates a body with the Part Design workbench"), QLatin1String(":/icons/PartDesignWorkbench.svg")} )); auto assembly = gsl::owner(new NewFileButton( {tr("Assembly"), tr("Creates an assembly project"), QLatin1String(":/icons/AssemblyWorkbench.svg")} )); auto draft = gsl::owner(new NewFileButton( {tr("2D Draft"), tr("Creates a 2D Draft document"), QLatin1String(":/icons/DraftWorkbench.svg")} )); auto arch = gsl::owner(new NewFileButton( {tr("BIM/Architecture"), tr("Creates an architectural project"), QLatin1String(":/icons/BIMWorkbench.svg")} )); // TODO: Ensure all of the required WBs are actually available layout->addWidget(partDesign); layout->addWidget(assembly); layout->addWidget(draft); layout->addWidget(arch); layout->addWidget(newEmptyFile); layout->addWidget(openFile); connect(newEmptyFile, &QPushButton::clicked, this, &StartView::newEmptyFile); connect(openFile, &QPushButton::clicked, this, &StartView::openExistingFile); connect(partDesign, &QPushButton::clicked, this, &StartView::newPartDesignFile); connect(assembly, &QPushButton::clicked, this, &StartView::newAssemblyFile); connect(draft, &QPushButton::clicked, this, &StartView::newDraftFile); connect(arch, &QPushButton::clicked, this, &StartView::newArchFile); } void StartView::configureFileCardWidget(QListView* fileCardWidget) { auto delegate = gsl::owner(new FileCardDelegate(fileCardWidget)); fileCardWidget->setItemDelegate(delegate); fileCardWidget->setMinimumWidth(fileCardWidget->parentWidget()->width()); // fileCardWidget->setGridSize( // fileCardWidget->itemDelegate()->sizeHint(QStyleOptionViewItem(), // fileCardWidget->model()->index(0, 0))); } void StartView::configureRecentFilesListWidget(QListView* recentFilesListWidget, QLabel* recentFilesLabel) { _recentFilesModel.loadRecentFiles(); recentFilesListWidget->setModel(&_recentFilesModel); configureFileCardWidget(recentFilesListWidget); auto recentFilesGroup = App::GetApplication().GetParameterGroupByPath( "User parameter:BaseApp/Preferences/RecentFiles" ); auto numRecentFiles {recentFilesGroup->GetInt("RecentFiles", 0)}; if (numRecentFiles == 0) { recentFilesListWidget->hide(); recentFilesLabel->hide(); } else { recentFilesListWidget->show(); recentFilesLabel->show(); } } void StartView::configureExamplesListWidget(QListView* examplesListWidget) { _examplesModel.loadExamples(); examplesListWidget->setModel(&_examplesModel); configureFileCardWidget(examplesListWidget); } void StartView::configureCustomFolderListWidget(QListView* customFolderListWidget) { _customFolderModel.loadCustomFolder(); customFolderListWidget->setModel(&_customFolderModel); configureFileCardWidget(customFolderListWidget); } void StartView::newEmptyFile() { Gui::Application::Instance->commandManager().runCommandByName("Std_New"); postStart(PostStartBehavior::switchWorkbench); } void StartView::newPartDesignFile() { Gui::Application::Instance->commandManager().runCommandByName("Std_New"); Gui::Application::Instance->activateWorkbench("PartDesignWorkbench"); Gui::Application::Instance->commandManager().runCommandByName("PartDesign_Body"); postStart(PostStartBehavior::doNotSwitchWorkbench); } void StartView::openExistingFile() { auto originalDocument = Gui::Application::Instance->activeDocument(); Gui::Application::Instance->commandManager().runCommandByName("Std_Open"); Gui::Application::checkForRecomputes(); if (Gui::Application::Instance->activeDocument() != originalDocument) { // Only run this if the user chose a new document to open (that is, they didn't cancel the // open file dialog) postStart(PostStartBehavior::switchWorkbench); } } void StartView::newAssemblyFile() { Gui::Application::Instance->commandManager().runCommandByName("Std_New"); Gui::Application::Instance->activateWorkbench("AssemblyWorkbench"); Gui::Application::Instance->commandManager().runCommandByName("Assembly_CreateAssembly"); Gui::Application::Instance->commandManager().runCommandByName("Std_Refresh"); postStart(PostStartBehavior::doNotSwitchWorkbench); } void StartView::newDraftFile() { Gui::Application::Instance->commandManager().runCommandByName("Std_New"); Gui::Application::Instance->activateWorkbench("DraftWorkbench"); Gui::Application::Instance->commandManager().runCommandByName("Std_ViewTop"); postStart(PostStartBehavior::doNotSwitchWorkbench); } void StartView::newArchFile() { Gui::Application::Instance->commandManager().runCommandByName("Std_New"); try { Gui::Application::Instance->activateWorkbench("BIMWorkbench"); } catch (...) { Gui::Application::Instance->activateWorkbench("ArchWorkbench"); } // Set the camera zoom level to 10 m, which is more appropriate for architectural projects Gui::Command::doCommand( Gui::Command::Gui, "Gui.activeDocument().activeView().viewDefaultOrientation(None, 10000.0)" ); postStart(PostStartBehavior::doNotSwitchWorkbench); } bool StartView::onHasMsg(const char* pMsg) const { if (strcmp("AllowsOverlayOnHover", pMsg) == 0) { return false; } return MDIView::onHasMsg(pMsg); } void StartView::postStart(PostStartBehavior behavior) { auto hGrp = App::GetApplication().GetParameterGroupByPath( "User parameter:BaseApp/Preferences/Mod/Start" ); if (behavior == PostStartBehavior::switchWorkbench) { auto wb = hGrp->GetASCII("AutoloadModule", ""); if (wb == "$LastModule") { wb = App::GetApplication() .GetParameterGroupByPath("User parameter:BaseApp/Preferences/General") ->GetASCII("LastModule", ""); } if (!wb.empty()) { Gui::Application::Instance->activateWorkbench(wb.c_str()); } } if (auto closeStart = hGrp->GetBool("closeStart", false)) { for (QWidget* w = this; w != nullptr; w = w->parentWidget()) { if (auto mdiSub = qobject_cast(w)) { mdiSub->close(); return; } } } } void StartView::fileCardSelected(const QModelIndex& index) { try { auto filename = index.data(static_cast(Start::DisplayedFilesModelRoles::path)).toString(); Gui::ModuleIO::verifyAndOpenFile(filename); } catch (Base::PyException& e) { Base::Console().error(e.getMessage().c_str()); } catch (Base::Exception& e) { Base::Console().error(e.getMessage().c_str()); } catch (...) { Base::Console().error("An unknown error occurred"); } } void StartView::showOnStartupChanged(bool checked) { auto hGrp = App::GetApplication().GetParameterGroupByPath( "User parameter:BaseApp/Preferences/Mod/Start" ); hGrp->SetBool( "ShowOnStartup", !checked ); // The sense of this option has been reversed: the checkbox actually says // "*Don't* show on startup" now, but the option is preserved in its // original sense, so is stored inverted. } void StartView::openFirstStartClicked() { _contents->setCurrentIndex(0); } void StartView::firstStartWidgetDismissed() { auto hGrp = App::GetApplication().GetParameterGroupByPath( "User parameter:BaseApp/Preferences/Mod/Start" ); hGrp->SetBool("FirstStart2024", false); _contents->setCurrentIndex(1); } void StartView::changeEvent(QEvent* event) { if (!isInitialized) { return; } _openFirstStart->setEnabled(true); Gui::Document* doc = Gui::Application::Instance->activeDocument(); if (doc) { if (auto view = dynamic_cast(doc->getActiveView())) { Gui::View3DInventorViewer* viewer = view->getViewer(); if (viewer->isEditing()) { _openFirstStart->setEnabled(false); } } } if (event->type() == QEvent::LanguageChange) { this->retranslateUi(); } Gui::MDIView::changeEvent(event); } void StartView::showEvent(QShowEvent* event) { if (auto mainWindow = Gui::getMainWindow()) { if (auto mdiArea = mainWindow->findChild()) { connect( mdiArea, &QMdiArea::subWindowActivated, this, &StartView::onMdiSubWindowActivated, Qt::UniqueConnection ); } } Gui::MDIView::showEvent(event); } void StartView::onMdiSubWindowActivated(QMdiSubWindow* subWindow) { // check if start view is activated subwindow if yes, then enable updates // so we can once again receive paint events bool isOurWindow = subWindow && subWindow->isAncestorOf(this); setListViewUpdatesEnabled(isOurWindow); } void StartView::setListViewUpdatesEnabled(bool enabled) { // disable updates on all QListView widgets when inactive to prevent unnecessary paint events QList listViews = findChildren(); for (QListView* listView : listViews) { listView->setUpdatesEnabled(enabled); if (listView->viewport()) { listView->viewport()->setUpdatesEnabled(enabled); } } } void StartView::retranslateUi() { QString title = QCoreApplication::translate("Workbench", "Start"); setWindowTitle(title); const QLatin1String h1Start("

"); const QLatin1String h1End("

"); _newFileLabel->setText(h1Start + tr("New File") + h1End); _examplesLabel->setText(h1Start + tr("Examples") + h1End); _recentFilesLabel->setText(h1Start + tr("Recent Files") + h1End); auto hGrp = App::GetApplication().GetParameterGroupByPath( "User parameter:BaseApp/Preferences/Mod/Start" ); std::string customFolder(hGrp->GetASCII("CustomFolder", "")); bool shortCustomFolder = hGrp->GetBool("ShortCustomFolder", true); // false shows full path if (!customFolder.empty()) { if (shortCustomFolder) { _customFolderLabel->setToolTip(QString::fromUtf8(customFolder.c_str())); customFolder = customFolder.substr(customFolder.find_last_of("/\\") + 1); } _customFolderLabel->setText(h1Start + QString::fromUtf8(customFolder.c_str()) + h1End); } QString application = QString::fromUtf8(App::Application::Config()["ExeName"].c_str()); _openFirstStart->setText(tr("Open First Start Setup")); _showOnStartupCheckBox->setText(tr("Do not show this Start page again (start with blank screen)")); }