// 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 "PreCompiled.h" #ifndef _PreComp_ #include #include #include #include #include #include #include #include #include #include #include #endif #include "StartView.h" #include "FileCardDelegate.h" #include "FileCardView.h" #include "FirstStartWidget.h" #include "FlowLayout.h" #include #include #include #include #include #include #include #include #include #include #include #include using namespace StartGui; TYPESYSTEM_SOURCE_ABSTRACT(StartGui::StartView, Gui::MDIView) // NOLINT namespace { struct NewButton { QString heading; QString description; QString iconPath; }; class NewFileButton: public QPushButton { public: NewFileButton(const NewButton& newButton) { auto hGrp = App::GetApplication().GetParameterGroupByPath( "User parameter:BaseApp/Preferences/Mod/Start"); const auto cardSpacing = static_cast(hGrp->GetInt("FileCardSpacing", 25)); // NOLINT const auto newFileIconSize = static_cast(hGrp->GetInt("NewFileIconSize", 48)); // NOLINT const auto cardLabelWith = static_cast(hGrp->GetInt("FileCardLabelWith", 180)); // NOLINT auto mainLayout = gsl::owner(new QHBoxLayout(this)); auto iconLabel = gsl::owner(new QLabel(this)); mainLayout->addWidget(iconLabel); QIcon baseIcon(newButton.iconPath); iconLabel->setPixmap(baseIcon.pixmap(newFileIconSize, newFileIconSize)); iconLabel->setPixmap(baseIcon.pixmap(newFileIconSize, newFileIconSize)); auto textLayout = gsl::owner(new QVBoxLayout); auto textLabelLine1 = gsl::owner(new QLabel(this)); textLabelLine1->setText(newButton.heading); textLabelLine1->setStyleSheet(QLatin1String("font-weight: bold;")); auto textLabelLine2 = gsl::owner(new QLabel(this)); textLabelLine2->setText(newButton.description); textLabelLine2->setWordWrap(true); textLayout->addWidget(textLabelLine1); textLayout->addWidget(textLabelLine2); textLayout->setSpacing(0); mainLayout->addItem(textLayout); mainLayout->addStretch(); this->setMinimumHeight(newFileIconSize + cardSpacing); this->setMinimumWidth(newFileIconSize + cardLabelWith); updateStyle(); } void updateStyle() { QString style = QStringLiteral(""); if (qApp->styleSheet().isEmpty()) { style = fileCardStyle(); } setStyleSheet(style); // This will trigger a changeEvent } void changeEvent(QEvent* event) override { if (!changeInProgress && event->type() == QEvent::StyleChange) { changeInProgress = true; // Block recursive calls. updateStyle(); changeInProgress = false; } QPushButton::changeEvent(event); } QString fileCardStyle() const { auto hGrp = App::GetApplication().GetParameterGroupByPath( "User parameter:BaseApp/Preferences/Mod/Start"); auto getUserColor = [&hGrp](QColor color, const char* parameter) { uint32_t packed = Base::Color::asPackedRGB(color); packed = hGrp->GetUnsigned(parameter, packed); color = Base::Color::fromPackedRGB(packed); return color; }; QColor background(221, 221, 221); // NOLINT background = getUserColor(background, "FileCardBackgroundColor"); QColor hovered(98, 160, 234); // NOLINT hovered = getUserColor(hovered, "FileCardBorderColor"); QColor pressed(38, 162, 105); // NOLINT pressed = getUserColor(pressed, "FileCardSelectionColor"); return QStringLiteral("QPushButton {" " background-color: rgb(%1, %2, %3);" " border-radius: 8px;" "}" "QPushButton:hover {" " border: 2px solid rgb(%4, %5, %6);" "}" "QPushButton:pressed {" " border: 2px solid rgb(%7, %8, %9);" "}") .arg(background.red()) .arg(background.green()) .arg(background.blue()) .arg(hovered.red()) .arg(hovered.green()) .arg(hovered.blue()) .arg(pressed.red()) .arg(pressed.green()) .arg(pressed.blue()); } private: bool changeInProgress = false; }; } // namespace 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); retranslateUi(); } void StartView::configureNewFileButtons(QLayout* layout) const { auto newEmptyFile = gsl::owner(new NewFileButton({tr("Empty file"), tr("Create a new empty FreeCAD file"), QLatin1String(":/icons/document-new.svg")})); auto openFile = gsl::owner(new NewFileButton({tr("Open File"), tr("Open an existing CAD file or 3D model"), QLatin1String(":/icons/document-open.svg")})); auto partDesign = gsl::owner( new NewFileButton({tr("Parametric Part"), tr("Create a part with the Part Design workbench"), QLatin1String(":/icons/PartDesignWorkbench.svg")})); auto assembly = gsl::owner( new NewFileButton({tr("Assembly"), tr("Create an assembly project"), QLatin1String(":/icons/AssemblyWorkbench.svg")})); auto draft = gsl::owner( new NewFileButton({tr("2D Draft"), tr("Create a 2D Draft with the Draft workbench"), QLatin1String(":/icons/DraftWorkbench.svg")})); auto arch = gsl::owner(new NewFileButton({tr("BIM/Architecture"), tr("Create 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() const { Gui::Application::Instance->commandManager().runCommandByName("Std_New"); postStart(PostStartBehavior::switchWorkbench); } void StartView::newPartDesignFile() const { 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() const { 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() const { 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() const { 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() const { Gui::Application::Instance->commandManager().runCommandByName("Std_New"); try { Gui::Application::Instance->activateWorkbench("BIMWorkbench"); } catch (...) { Gui::Application::Instance->activateWorkbench("ArchWorkbench"); } 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) const { 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()); } } auto closeStart = hGrp->GetBool("closeStart", false); if (closeStart) { this->window()->close(); } } 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) { _openFirstStart->setEnabled(true); Gui::Document* doc = Gui::Application::Instance->activeDocument(); if (doc) { Gui::View3DInventor* view = static_cast(doc->getActiveView()); if (view) { Gui::View3DInventorViewer* viewer = view->getViewer(); if (viewer->isEditing()) { _openFirstStart->setEnabled(false); } } } if (event->type() == QEvent::LanguageChange) { this->retranslateUi(); } Gui::MDIView::changeEvent(event); } 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); _customFolderLabel->setText(h1Start + tr("Custom Folder") + h1End); QString application = QString::fromUtf8(App::Application::Config()["ExeName"].c_str()); _openFirstStart->setText(tr("Open first start setup")); _showOnStartupCheckBox->setText( tr("Don't show this Start page again (start with blank screen)")); }