diff --git a/CHANGES-3.2 b/CHANGES-3.2 index 82a342ecb..ffc8e0010 100644 --- a/CHANGES-3.2 +++ b/CHANGES-3.2 @@ -8,6 +8,20 @@ contributors are listed. Note that Calamares does not have a historical changelog -- this log starts with version 3.2.0. The release notes on the website will have to do for older versions. +# 3.2.56 (unreleased) # + +This release contains contributions from (alphabetically by first name): + - Victor Fuentes (new contributor! Welcome!) + +## Core ## + - No core changes yet + +## Modules ## + - *users* module sets global storage key *fullname* to the full name + of the user (e.g. what is entered in the "your full name" box on the + users page). #1923 (Thanks Victor) + + # 3.2.55 (2022-04-11) # This release contains contributions from (alphabetically by first name): diff --git a/src/libcalamares/modulesystem/RequirementsChecker.cpp b/src/libcalamares/modulesystem/RequirementsChecker.cpp index 32c7254de..d0d6e74fe 100644 --- a/src/libcalamares/modulesystem/RequirementsChecker.cpp +++ b/src/libcalamares/modulesystem/RequirementsChecker.cpp @@ -32,6 +32,7 @@ RequirementsChecker::RequirementsChecker( QVector< Module* > modules, Requiremen , m_progressTimer( nullptr ) , m_progressTimeouts( 0 ) { + m_model->clear(); m_watchers.reserve( m_modules.count() ); connect( this, &RequirementsChecker::requirementsProgress, model, &RequirementsModel::setProgressMessage ); } @@ -63,9 +64,9 @@ RequirementsChecker::finished() static QMutex finishedMutex; QMutexLocker lock( &finishedMutex ); - if ( m_progressTimer && std::all_of( m_watchers.cbegin(), m_watchers.cend(), []( const Watcher* w ) { - return w && w->isFinished(); - } ) ) + if ( m_progressTimer + && std::all_of( + m_watchers.cbegin(), m_watchers.cend(), []( const Watcher* w ) { return w && w->isFinished(); } ) ) { cDebug() << "All requirements have been checked."; if ( m_progressTimer ) @@ -100,14 +101,17 @@ RequirementsChecker::reportProgress() m_progressTimeouts++; QStringList remainingNames; - auto remaining = std::count_if( m_watchers.cbegin(), m_watchers.cend(), [&]( const Watcher* w ) { - if ( w && !w->isFinished() ) - { - remainingNames << w->objectName(); - return true; - } - return false; - } ); + auto remaining = std::count_if( m_watchers.cbegin(), + m_watchers.cend(), + [ & ]( const Watcher* w ) + { + if ( w && !w->isFinished() ) + { + remainingNames << w->objectName(); + return true; + } + return false; + } ); if ( remaining > 0 ) { cDebug() << "Remaining modules:" << remaining << Logger::DebugList( remainingNames ); diff --git a/src/libcalamares/modulesystem/RequirementsModel.cpp b/src/libcalamares/modulesystem/RequirementsModel.cpp index 6a7e0a5b4..f21f7051c 100644 --- a/src/libcalamares/modulesystem/RequirementsModel.cpp +++ b/src/libcalamares/modulesystem/RequirementsModel.cpp @@ -15,6 +15,16 @@ namespace Calamares { +void +RequirementsModel::clear() +{ + QMutexLocker l( &m_addLock ); + emit beginResetModel(); + m_requirements.clear(); + changeRequirementsList(); + emit endResetModel(); +} + void RequirementsModel::addRequirementsList( const Calamares::RequirementsList& requirements ) { diff --git a/src/libcalamares/modulesystem/RequirementsModel.h b/src/libcalamares/modulesystem/RequirementsModel.h index 5f3e13cbb..d1842760c 100644 --- a/src/libcalamares/modulesystem/RequirementsModel.h +++ b/src/libcalamares/modulesystem/RequirementsModel.h @@ -77,6 +77,10 @@ signals: protected: QHash< int, QByteArray > roleNames() const override; + + ///@brief Clears the requirements; resets the model + void clear(); + ///@brief Append some requirements; resets the model void addRequirementsList( const Calamares::RequirementsList& requirements ); diff --git a/src/libcalamaresui/CMakeLists.txt b/src/libcalamaresui/CMakeLists.txt index ea9d27558..406bd3ce4 100644 --- a/src/libcalamaresui/CMakeLists.txt +++ b/src/libcalamaresui/CMakeLists.txt @@ -30,16 +30,11 @@ set(calamaresui_SOURCES widgets/LogWidget.cpp widgets/TranslationFix.cpp widgets/WaitingWidget.cpp - ${CMAKE_SOURCE_DIR}/3rdparty/waitingspinnerwidget.cpp + widgets/waitingspinnerwidget.cpp Branding.cpp ViewManager.cpp ) -# Don't warn about third-party sources -mark_thirdparty_code( - ${CMAKE_SOURCE_DIR}/3rdparty/waitingspinnerwidget.cpp -) - if(WITH_PYTHON) list(APPEND calamaresui_SOURCES modulesystem/PythonJobModule.cpp) endif() diff --git a/src/libcalamaresui/modulesystem/ModuleManager.cpp b/src/libcalamaresui/modulesystem/ModuleManager.cpp index 1e397b340..1233b1115 100644 --- a/src/libcalamaresui/modulesystem/ModuleManager.cpp +++ b/src/libcalamaresui/modulesystem/ModuleManager.cpp @@ -349,7 +349,18 @@ ModuleManager::checkRequirements() connect( rq, &RequirementsChecker::done, this, - [ = ]() { this->requirementsComplete( m_requirementsModel->satisfiedMandatory() ); } ); + [ = ]() + { + if ( m_requirementsModel->satisfiedMandatory() ) + { + /* we're done */ this->requirementsComplete( true ); + } + else + { + this->requirementsComplete( false ); + QTimer::singleShot( std::chrono::seconds( 5 ), this, &ModuleManager::checkRequirements ); + } + } ); QTimer::singleShot( 0, rq, &RequirementsChecker::run ); } diff --git a/src/libcalamaresui/widgets/WaitingWidget.cpp b/src/libcalamaresui/widgets/WaitingWidget.cpp index aef5aecf5..18acc11b7 100644 --- a/src/libcalamaresui/widgets/WaitingWidget.cpp +++ b/src/libcalamaresui/widgets/WaitingWidget.cpp @@ -12,46 +12,108 @@ #include "utils/CalamaresUtilsGui.h" -#include "3rdparty/waitingspinnerwidget.h" - #include #include +#include WaitingWidget::WaitingWidget( const QString& text, QWidget* parent ) - : QWidget( parent ) + : WaitingSpinnerWidget( parent, false, false ) { - QBoxLayout* waitingLayout = new QVBoxLayout; - setLayout( waitingLayout ); - waitingLayout->addStretch(); - QBoxLayout* pbLayout = new QHBoxLayout; - waitingLayout->addLayout( pbLayout ); - pbLayout->addStretch(); - - WaitingSpinnerWidget* spnr = new WaitingSpinnerWidget(); - pbLayout->addWidget( spnr ); - - pbLayout->addStretch(); - - m_waitingLabel = new QLabel( text ); - - int spnrSize = m_waitingLabel->fontMetrics().height() * 4; - spnr->setFixedSize( spnrSize, spnrSize ); - spnr->setInnerRadius( spnrSize / 2 ); - spnr->setLineLength( spnrSize / 2 ); - spnr->setLineWidth( spnrSize / 8 ); - spnr->start(); - - m_waitingLabel->setAlignment( Qt::AlignCenter ); - waitingLayout->addSpacing( spnrSize / 2 ); - waitingLayout->addWidget( m_waitingLabel ); - waitingLayout->addStretch(); - - CalamaresUtils::unmarginLayout( waitingLayout ); + int spnrSize = CalamaresUtils::defaultFontHeight() * 4; + setFixedSize( spnrSize, spnrSize ); + setInnerRadius( spnrSize / 2 ); + setLineLength( spnrSize / 2 ); + setLineWidth( spnrSize / 8 ); + setAlignment( Qt::AlignmentFlag::AlignBottom ); + setText( text ); + start(); } +WaitingWidget::~WaitingWidget() {} + +struct CountdownWaitingWidget::Private +{ + std::chrono::seconds duration; + // int because we count down, need to be able to show a 0, + // and then wrap around to duration a second later. + int count = 0; + QTimer* timer = nullptr; + + Private( std::chrono::seconds seconds, QWidget* parent ) + : duration( seconds ) + , timer( new QTimer( parent ) ) + { + } +}; + +CountdownWaitingWidget::CountdownWaitingWidget( std::chrono::seconds duration, QWidget* parent ) + : WaitingSpinnerWidget( parent, false, false ) + , d( std::make_unique< Private >( duration, this ) ) +{ + // Set up the label first for sizing + const int labelHeight = qBound( 16, CalamaresUtils::defaultFontHeight() * 3 / 2, 64 ); + + // Set up the spinner + setFixedSize( labelHeight, labelHeight ); + setRevolutionsPerSecond( 1.0 / double( duration.count() ) ); + setInnerRadius( labelHeight / 2 ); + setLineLength( labelHeight / 2 ); + setLineWidth( labelHeight / 8 ); + setAlignment( Qt::AlignmentFlag::AlignVCenter ); + + // Last because it updates the text + setInterval( duration ); + + d->timer->setInterval( std::chrono::seconds( 1 ) ); + connect( d->timer, &QTimer::timeout, this, &CountdownWaitingWidget::tick ); +} + +CountdownWaitingWidget::~CountdownWaitingWidget() +{ + d->timer->stop(); +} void -WaitingWidget::setText( const QString& text ) +CountdownWaitingWidget::setInterval( std::chrono::seconds duration ) { - m_waitingLabel->setText( text ); + d->duration = duration; + d->count = int( duration.count() ); + tick(); +} + +void +CountdownWaitingWidget::start() +{ + // start it from the top + if ( d->count <= 0 ) + { + d->count = int( d->duration.count() ); + tick(); + } + d->timer->start(); + WaitingSpinnerWidget::start(); +} + +void +CountdownWaitingWidget::stop() +{ + d->timer->stop(); + WaitingSpinnerWidget::stop(); +} + +void +CountdownWaitingWidget::tick() +{ + // We do want to **display** a 0 which is why we wrap around only + // after counting down from 0. + d->count--; + if ( d->count < 0 ) + { + d->count = int( d->duration.count() ); + } + setText( QString::number( d->count ) ); + if ( d->count == 0 ) + { + timeout(); + } } diff --git a/src/libcalamaresui/widgets/WaitingWidget.h b/src/libcalamaresui/widgets/WaitingWidget.h index 850b81ca9..1b78809de 100644 --- a/src/libcalamaresui/widgets/WaitingWidget.h +++ b/src/libcalamaresui/widgets/WaitingWidget.h @@ -10,20 +10,61 @@ #ifndef WAITINGWIDGET_H #define WAITINGWIDGET_H -#include +#include "widgets/waitingspinnerwidget.h" + +#include +#include class QLabel; +class QTimer; -class WaitingWidget : public QWidget +/** @brief A spinner and a label below it + * + * The spinner has a fixed size of 4* the font height, + * and the text is displayed centered below it. Use this + * to display a long-term waiting situation with a status report. + */ +class WaitingWidget : public WaitingSpinnerWidget +{ +public: + /// Create a WaitingWidget with initial @p text label. + explicit WaitingWidget( const QString& text, QWidget* parent = nullptr ); + ~WaitingWidget() override; +}; + +/** @brief A spinner and a countdown next to it + * + * The spinner is sized to the text-height and displays a + * numeric countdown next to it. The countdown is updated + * every second. The signal timeout() is sent every time + * the countdown reaches 0. + */ +class CountdownWaitingWidget : public WaitingSpinnerWidget { Q_OBJECT public: - explicit WaitingWidget( const QString& text, QWidget* parent = nullptr ); + /// Create a countdown widget with a given @p duration + explicit CountdownWaitingWidget( std::chrono::seconds duration = std::chrono::seconds( 5 ), + QWidget* parent = nullptr ); + ~CountdownWaitingWidget() override; - void setText( const QString& text ); + /// Changes the duration used and resets the countdown + void setInterval( std::chrono::seconds duration ); + + /// Start the countdown, resets to the full duration + void start(); + /// Stop the countdown + void stop(); + +Q_SIGNALS: + void timeout(); + +protected Q_SLOTS: + void tick(); private: - QLabel* m_waitingLabel; + struct Private; + std::unique_ptr< Private > d; }; #endif // WAITINGWIDGET_H diff --git a/3rdparty/waitingspinnerwidget.cpp b/src/libcalamaresui/widgets/waitingspinnerwidget.cpp similarity index 75% rename from 3rdparty/waitingspinnerwidget.cpp rename to src/libcalamaresui/widgets/waitingspinnerwidget.cpp index 98931a6ad..a4bb9b79c 100644 --- a/3rdparty/waitingspinnerwidget.cpp +++ b/src/libcalamaresui/widgets/waitingspinnerwidget.cpp @@ -2,6 +2,7 @@ * SPDX-FileCopyrightText: 2012-2014 Alexander Turkin * SPDX-FileCopyrightText: 2014 William Hallatt * SPDX-FileCopyrightText: 2015 Jacob Dawid + * SPDX-FileCopyrightText: 2018 huxingyi * SPDX-License-Identifier: MIT */ @@ -38,49 +39,41 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. #include #include +static bool isAlignCenter(Qt::AlignmentFlag a) +{ + return a == Qt::AlignmentFlag::AlignVCenter; +} + WaitingSpinnerWidget::WaitingSpinnerWidget(QWidget *parent, bool centerOnParent, bool disableParentWhenSpinning) - : QWidget(parent), - _centerOnParent(centerOnParent), - _disableParentWhenSpinning(disableParentWhenSpinning) { - initialize(); -} + : WaitingSpinnerWidget(Qt::WindowModality::NonModal, parent, centerOnParent, disableParentWhenSpinning) +{} WaitingSpinnerWidget::WaitingSpinnerWidget(Qt::WindowModality modality, QWidget *parent, bool centerOnParent, bool disableParentWhenSpinning) - : QWidget(parent, Qt::Dialog | Qt::FramelessWindowHint), + : QWidget(parent, modality == Qt::WindowModality::NonModal ? Qt::WindowFlags() : Qt::Dialog | Qt::FramelessWindowHint), _centerOnParent(centerOnParent), - _disableParentWhenSpinning(disableParentWhenSpinning){ - initialize(); - - // We need to set the window modality AFTER we've hidden the - // widget for the first time since changing this property while - // the widget is visible has no effect. - setWindowModality(modality); - setAttribute(Qt::WA_TranslucentBackground); -} - -void WaitingSpinnerWidget::initialize() { - _color = Qt::black; - _roundness = 100.0; - _minimumTrailOpacity = 3.14159265358979323846; - _trailFadePercentage = 80.0; - _revolutionsPerSecond = 1.57079632679489661923; - _numberOfLines = 20; - _lineLength = 10; - _lineWidth = 2; - _innerRadius = 10; - _currentCounter = 0; - _isSpinning = false; - + _disableParentWhenSpinning(disableParentWhenSpinning) +{ _timer = new QTimer(this); connect(_timer, SIGNAL(timeout()), this, SLOT(rotate())); updateSize(); updateTimer(); hide(); + + // We need to set the window modality AFTER we've hidden the + // widget for the first time since changing this property while + // the widget is visible has no effect. + // + // Non-modal windows don't need any work + if ( modality != Qt::WindowModality::NonModal ) + { + setWindowModality(modality); + setAttribute(Qt::WA_TranslucentBackground); + } } void WaitingSpinnerWidget::paintEvent(QPaintEvent *) { @@ -98,6 +91,7 @@ void WaitingSpinnerWidget::paintEvent(QPaintEvent *) { painter.save(); painter.translate(_innerRadius + _lineLength, _innerRadius + _lineLength); + painter.translate((width() - _imageSize.width()) / 2, 0); qreal rotateAngle = static_cast(360 * i) / static_cast(_numberOfLines); painter.rotate(rotateAngle); @@ -114,6 +108,17 @@ void WaitingSpinnerWidget::paintEvent(QPaintEvent *) { _roundness, Qt::RelativeSize); painter.restore(); } + + if (!_text.isEmpty()) { + painter.setPen(QPen(_textColor)); + if (isAlignCenter(alignment())) { + painter.drawText(QRect(0, 0, width(), height()), + Qt::AlignVCenter | Qt::AlignHCenter, _text); + } else { + painter.drawText(QRect(0, _imageSize.height(), width(), height() - _imageSize.height()), + Qt::AlignBottom | Qt::AlignHCenter, _text); + } + } } void WaitingSpinnerWidget::start() { @@ -166,39 +171,58 @@ void WaitingSpinnerWidget::setInnerRadius(int radius) { updateSize(); } -QColor WaitingSpinnerWidget::color() { +void WaitingSpinnerWidget::setText(const QString& text) { + _text = text; + updateSize(); +} + +void WaitingSpinnerWidget::setAlignment(Qt::AlignmentFlag align) +{ + _alignment = align; + updateSize(); +} + +QColor WaitingSpinnerWidget::color() const { return _color; } -qreal WaitingSpinnerWidget::roundness() { +QColor WaitingSpinnerWidget::textColor() const { + return _textColor; +} + +QString WaitingSpinnerWidget::text() const { + return _text; +} + +qreal WaitingSpinnerWidget::roundness() const { return _roundness; } -qreal WaitingSpinnerWidget::minimumTrailOpacity() { +qreal WaitingSpinnerWidget::minimumTrailOpacity() const { return _minimumTrailOpacity; } -qreal WaitingSpinnerWidget::trailFadePercentage() { +qreal WaitingSpinnerWidget::trailFadePercentage() const { return _trailFadePercentage; } -qreal WaitingSpinnerWidget::revolutionsPersSecond() { +qreal WaitingSpinnerWidget::revolutionsPersSecond() const { return _revolutionsPerSecond; } -int WaitingSpinnerWidget::numberOfLines() { +int WaitingSpinnerWidget::numberOfLines() const { return _numberOfLines; } -int WaitingSpinnerWidget::lineLength() { +int WaitingSpinnerWidget::lineLength() const { return _lineLength; } -int WaitingSpinnerWidget::lineWidth() { +int WaitingSpinnerWidget::lineWidth() const { return _lineWidth; } -int WaitingSpinnerWidget::innerRadius() { +int WaitingSpinnerWidget::innerRadius() const { return _innerRadius; } @@ -214,6 +238,10 @@ void WaitingSpinnerWidget::setColor(QColor color) { _color = color; } +void WaitingSpinnerWidget::setTextColor(QColor color) { + _textColor = color; +} + void WaitingSpinnerWidget::setRevolutionsPerSecond(qreal revolutionsPerSecond) { _revolutionsPerSecond = revolutionsPerSecond; updateTimer(); @@ -237,7 +265,14 @@ void WaitingSpinnerWidget::rotate() { void WaitingSpinnerWidget::updateSize() { int size = (_innerRadius + _lineLength) * 2; - setFixedSize(size, size); + _imageSize = QSize(size, size); + if (_text.isEmpty() || isAlignCenter(alignment())) { + setFixedSize(size, size); + } else { + QFontMetrics fm(font()); + QSize textSize = QSize(fm.width(_text), fm.height()); + setFixedSize(std::max(size, textSize.width()), size + size / 4 + textSize.height()); + } } void WaitingSpinnerWidget::updateTimer() { diff --git a/3rdparty/waitingspinnerwidget.h b/src/libcalamaresui/widgets/waitingspinnerwidget.h similarity index 50% rename from 3rdparty/waitingspinnerwidget.h rename to src/libcalamaresui/widgets/waitingspinnerwidget.h index d171e9beb..1ecc33a87 100644 --- a/3rdparty/waitingspinnerwidget.h +++ b/src/libcalamaresui/widgets/waitingspinnerwidget.h @@ -2,6 +2,7 @@ * SPDX-FileCopyrightText: 2012-2014 Alexander Turkin * SPDX-FileCopyrightText: 2014 William Hallatt * SPDX-FileCopyrightText: 2015 Jacob Dawid + * SPDX-FileCopyrightText: 2018 huxingyi * SPDX-License-Identifier: MIT */ @@ -37,28 +38,32 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. class WaitingSpinnerWidget : public QWidget { Q_OBJECT public: - /*! Constructor for "standard" widget behaviour - use this - * constructor if you wish to, e.g. embed your widget in another. */ + /** @brief Constructor for "standard" widget behaviour + * + * Use this constructor if you wish to, e.g. embed your widget in another. + */ WaitingSpinnerWidget(QWidget *parent = nullptr, bool centerOnParent = true, bool disableParentWhenSpinning = true); - /*! Constructor - use this constructor to automatically create a modal - * ("blocking") spinner on top of the calling widget/window. If a valid - * parent widget is provided, "centreOnParent" will ensure that - * QtWaitingSpinner automatically centres itself on it, if not, - * "centreOnParent" is ignored. */ + /** @brief Constructor + * + * Use this constructor to automatically create a modal + * ("blocking") spinner on top of the calling widget/window. If a valid + * parent widget is provided, "centreOnParent" will ensure that + * QtWaitingSpinner automatically centres itself on it, if not, + * @p centerOnParent is ignored. + */ WaitingSpinnerWidget(Qt::WindowModality modality, QWidget *parent = nullptr, bool centerOnParent = true, bool disableParentWhenSpinning = true); -public slots: - void start(); - void stop(); + WaitingSpinnerWidget(const WaitingSpinnerWidget&) = delete; + WaitingSpinnerWidget& operator=(const WaitingSpinnerWidget&) = delete; -public: void setColor(QColor color); + void setTextColor(QColor color); void setRoundness(qreal roundness); void setMinimumTrailOpacity(qreal minimumTrailOpacity); void setTrailFadePercentage(qreal trail); @@ -67,21 +72,45 @@ public: void setLineLength(int length); void setLineWidth(int width); void setInnerRadius(int radius); - void setText(QString text); - QColor color(); - qreal roundness(); - qreal minimumTrailOpacity(); - qreal trailFadePercentage(); - qreal revolutionsPersSecond(); - int numberOfLines(); - int lineLength(); - int lineWidth(); - int innerRadius(); + /** @brief Sets the text displayed in or below the spinner + * + * If the text is empty, no text is displayed. The text is displayed + * in or below the spinner depending on the value of alignment(). + * With AlignBottom, the text is displayed below the spinner, + * centered horizontally relative to the spinner; any other alignment + * will put the text in the middle of the spinner itself. + */ + void setText(const QString& text); + /** @brief Sets the alignment of text for the spinner + * + * The only meaningful values are AlignBottom and AlignVCenter, + * for text below the spinner and text in the middle. + */ + void setAlignment(Qt::AlignmentFlag align); + /// Convenience to set text-in-the-middle (@c true) or text-at-bottom (@c false) + void setCenteredText(bool centered) { setAlignment(centered ? Qt::AlignmentFlag::AlignVCenter : Qt::AlignmentFlag::AlignBottom ); } + + QColor color() const; + QColor textColor() const; + QString text() const; + Qt::AlignmentFlag alignment() const { return _alignment; } + qreal roundness() const; + qreal minimumTrailOpacity() const; + qreal trailFadePercentage() const; + qreal revolutionsPersSecond() const; + int numberOfLines() const; + int lineLength() const; + int lineWidth() const; + int innerRadius() const; bool isSpinning() const; -private slots: +public Q_SLOTS: + void start(); + void stop(); + +private Q_SLOTS: void rotate(); protected: @@ -94,29 +123,37 @@ private: qreal trailFadePerc, qreal minOpacity, QColor color); - void initialize(); void updateSize(); void updateTimer(); void updatePosition(); private: - QColor _color; - qreal _roundness; // 0..100 - qreal _minimumTrailOpacity; - qreal _trailFadePercentage; - qreal _revolutionsPerSecond; - int _numberOfLines; - int _lineLength; - int _lineWidth; - int _innerRadius; + // PI, leading to a full fade in one whole revolution + static constexpr const auto radian = 3.14159265358979323846; -private: - WaitingSpinnerWidget(const WaitingSpinnerWidget&); - WaitingSpinnerWidget& operator=(const WaitingSpinnerWidget&); + // Spinner-wheel related settings + QColor _color = Qt::black; + qreal _roundness = 100.0; // 0..100 + qreal _minimumTrailOpacity = radian; + qreal _trailFadePercentage = 80.0; + qreal _revolutionsPerSecond = radian / 2; + int _numberOfLines = 20; + int _lineLength = 10; + int _lineWidth = 2; + int _innerRadius = 10; + QSize _imageSize; - QTimer *_timer; - bool _centerOnParent; - bool _disableParentWhenSpinning; - int _currentCounter; - bool _isSpinning; + // Text-related settings + Qt::AlignmentFlag _alignment = Qt::AlignmentFlag::AlignBottom; + QString _text; + QColor _textColor = Qt::black; + + // Environment settings + bool _centerOnParent = true; + bool _disableParentWhenSpinning = true; + + // Internal bits + QTimer *_timer = nullptr; + int _currentCounter = 0; + bool _isSpinning = false; }; diff --git a/src/modules/README.md b/src/modules/README.md index 4d3803a75..e5bd2e7b6 100644 --- a/src/modules/README.md +++ b/src/modules/README.md @@ -58,10 +58,10 @@ Module descriptors for C++ modules **may** have the following key: Module descriptors for Python modules **must** have the following key: - *script* (the name of the Python script to load, nearly always `main.py`) -Module descriptors for process modules **must** have the following key: -- *command* (the command to run) +Module descriptors for process modules **must** have the following key: +- *command* (the command to run) -Module descriptors for process modules **may** have the following keys: +Module descriptors for process modules **may** have the following keys: - *timeout* (how long, in seconds, to wait for the command to run) - *chroot* (if true, run the command in the target system rather than the host) Note that process modules are not recommended. @@ -181,23 +181,25 @@ for determining the relative weights there. ## Global storage keys + Some modules place values in global storage so that they can be referenced later by other modules or even other parts of the same module. The following table represents a partial list of the values available as well as where they originate from and which module consume them. -Key|Source|Consumers|Description ----|---|---|--- -btrfsSubvolumes|mount|fstab|List of maps containing the mountpoint and btrtfs subvolume -btrfsRootSubvolume|mount|bootloader, luksopenswaphook|String containing the subvolume mounted at root -efiSystemPartition|partition|bootloader, fstab|String containing the path to the ESP relative to the installed system -extraMounts|mount|unpackfs|List of maps holding metadata for the temporary mountpoints used by the installer -hostname|users||A string containing the hostname of the new system -netinstallAdd|packagechooser|netinstall|Data to add to netinstall tree. Same format as netinstall.yaml -netinstallSelect|packagechooser|netinstall|List of group names to select in the netinstall tree -partitions|partition, rawfs|numerous modules|List of maps of metadata about each partition -rootMountPoint|mount|numerous modules|A string with the absolute path to the root mountpoint -username|users|networkcfg, plasmainf, preservefiles|A string containing the username of the new user -zfsDatasets|zfs|bootloader, grubcfg, mount|List of maps of zfs datasets including the name and mount information -zfsInfo|partition|mount, zfs|List of encrypted zfs partitions and the encription info -zfsPoolInfo|zfs|mount, umount|List of maps of zfs pool info including the name and mountpoint +Key |Source |Consumers|Description +------------------|----------------|---|--- +btrfsSubvolumes |mount |fstab|List of maps containing the mountpoint and btrtfs subvolume +btrfsRootSubvolume|mount |bootloader, luksopenswaphook|String containing the subvolume mounted at root +efiSystemPartition|partition |bootloader, fstab|String containing the path to the ESP relative to the installed system +extraMounts |mount |unpackfs|List of maps holding metadata for the temporary mountpoints used by the installer +fullname |users ||The full username (e.g. "Jane Q. Public") +hostname |users ||A string containing the hostname of the new system +netinstallAdd |packagechooser |netinstall|Data to add to netinstall tree. Same format as netinstall.yaml +netinstallSelect |packagechooser |netinstall|List of group names to select in the netinstall tree +partitions |partition, rawfs|numerous modules|List of maps of metadata about each partition +rootMountPoint |mount |numerous modules|A string with the absolute path to the root mountpoint +username |users |networkcfg, plasmainf, preservefiles|A string containing the username of the new user +zfsDatasets |zfs |bootloader, grubcfg, mount|List of maps of zfs datasets including the name and mount information +zfsInfo |partition |mount, zfs|List of encrypted zfs partitions and the encription info +zfsPoolInfo |zfs |mount, umount|List of maps of zfs pool info including the name and mountpoint ## C++ modules diff --git a/src/modules/locale/LocaleViewStep.cpp b/src/modules/locale/LocaleViewStep.cpp index 2145ad201..03d1d4f5e 100644 --- a/src/modules/locale/LocaleViewStep.cpp +++ b/src/modules/locale/LocaleViewStep.cpp @@ -11,7 +11,6 @@ #include "LocaleViewStep.h" #include "LocalePage.h" -#include "widgets/WaitingWidget.h" #include "GlobalStorage.h" #include "JobQueue.h" diff --git a/src/modules/partition/gui/ScanningDialog.cpp b/src/modules/partition/gui/ScanningDialog.cpp index 56133e21f..7dd85ff86 100644 --- a/src/modules/partition/gui/ScanningDialog.cpp +++ b/src/modules/partition/gui/ScanningDialog.cpp @@ -10,7 +10,7 @@ #include "ScanningDialog.h" -#include "3rdparty/waitingspinnerwidget.h" +#include "widgets/waitingspinnerwidget.h" #include #include diff --git a/src/modules/users/Config.cpp b/src/modules/users/Config.cpp index eae57a868..27bc80938 100644 --- a/src/modules/users/Config.cpp +++ b/src/modules/users/Config.cpp @@ -445,6 +445,15 @@ Config::setFullName( const QString& name ) if ( name != m_fullName ) { m_fullName = name; + Calamares::GlobalStorage* gs = Calamares::JobQueue::instance()->globalStorage(); + if ( name.isEmpty() ) + { + gs->remove( "fullname" ); + } + else + { + gs->insert( "fullname", name ); + } emit fullNameChanged( name ); // Build login and hostname, if needed diff --git a/src/modules/welcome/checker/CheckerContainer.cpp b/src/modules/welcome/checker/CheckerContainer.cpp index 23055c2e0..dd5a6680f 100644 --- a/src/modules/welcome/checker/CheckerContainer.cpp +++ b/src/modules/welcome/checker/CheckerContainer.cpp @@ -65,13 +65,18 @@ CheckerContainer::requirementsComplete( bool ok ) } } - layout()->removeWidget( m_waitingWidget ); - m_waitingWidget->deleteLater(); - m_waitingWidget = nullptr; // Don't delete in destructor - - m_checkerWidget = new ResultsListWidget( m_config, this ); - m_checkerWidget->setObjectName( "requirementsChecker" ); - layout()->addWidget( m_checkerWidget ); + if ( m_waitingWidget ) + { + layout()->removeWidget( m_waitingWidget ); + m_waitingWidget->deleteLater(); + m_waitingWidget = nullptr; // Don't delete in destructor + } + if ( !m_checkerWidget ) + { + m_checkerWidget = new ResultsListWidget( m_config, this ); + m_checkerWidget->setObjectName( "requirementsChecker" ); + layout()->addWidget( m_checkerWidget ); + } m_verdict = ok; } diff --git a/src/modules/welcome/checker/GeneralRequirements.cpp b/src/modules/welcome/checker/GeneralRequirements.cpp index ca7219ca4..7780b8753 100644 --- a/src/modules/welcome/checker/GeneralRequirements.cpp +++ b/src/modules/welcome/checker/GeneralRequirements.cpp @@ -15,6 +15,9 @@ #include "CheckerContainer.h" #include "partman_devices.h" +#include "CalamaresVersion.h" // For development-or-not +#include "GlobalStorage.h" +#include "JobQueue.h" #include "Settings.h" #include "modulesystem/Requirement.h" #include "network/Manager.h" @@ -26,9 +29,6 @@ #include "utils/Variant.h" #include "widgets/WaitingWidget.h" -#include "GlobalStorage.h" -#include "JobQueue.h" - #include #include #include @@ -148,28 +148,29 @@ GeneralRequirements::checkRequirements() Calamares::RequirementsList checkEntries; foreach ( const QString& entry, m_entriesToCheck ) { + const bool required = m_entriesToRequire.contains( entry ); if ( entry == "storage" ) { checkEntries.append( { entry, - [req = m_requiredStorageGiB] { return tr( "has at least %1 GiB available drive space" ).arg( req ); }, - [req = m_requiredStorageGiB] { - return tr( "There is not enough drive space. At least %1 GiB is required." ).arg( req ); - }, + [ req = m_requiredStorageGiB ] + { return tr( "has at least %1 GiB available drive space" ).arg( req ); }, + [ req = m_requiredStorageGiB ] + { return tr( "There is not enough drive space. At least %1 GiB is required." ).arg( req ); }, enoughStorage, - m_entriesToRequire.contains( entry ) } ); + required } ); } else if ( entry == "ram" ) { checkEntries.append( { entry, - [req = m_requiredRamGiB] { return tr( "has at least %1 GiB working memory" ).arg( req ); }, - [req = m_requiredRamGiB] { + [ req = m_requiredRamGiB ] { return tr( "has at least %1 GiB working memory" ).arg( req ); }, + [ req = m_requiredRamGiB ] { return tr( "The system does not have enough working memory. At least %1 GiB is required." ) .arg( req ); }, enoughRam, - m_entriesToRequire.contains( entry ) } ); + required } ); } else if ( entry == "power" ) { @@ -177,7 +178,7 @@ GeneralRequirements::checkRequirements() [] { return tr( "is plugged in to a power source" ); }, [] { return tr( "The system is not plugged in to a power source." ); }, hasPower, - m_entriesToRequire.contains( entry ) } ); + required } ); } else if ( entry == "internet" ) { @@ -185,32 +186,65 @@ GeneralRequirements::checkRequirements() [] { return tr( "is connected to the Internet" ); }, [] { return tr( "The system is not connected to the Internet." ); }, hasInternet, - m_entriesToRequire.contains( entry ) } ); + required } ); } else if ( entry == "root" ) { checkEntries.append( { entry, [] { return tr( "is running the installer as an administrator (root)" ); }, - [] { + [] + { return Calamares::Settings::instance()->isSetupMode() ? tr( "The setup program is not running with administrator rights." ) : tr( "The installer is not running with administrator rights." ); }, isRoot, - m_entriesToRequire.contains( entry ) } ); + required } ); } else if ( entry == "screen" ) { checkEntries.append( { entry, [] { return tr( "has a screen large enough to show the whole installer" ); }, - [] { + [] + { return Calamares::Settings::instance()->isSetupMode() ? tr( "The screen is too small to display the setup program." ) : tr( "The screen is too small to display the installer." ); }, enoughScreen, - false } ); + required } ); } +#ifdef CALAMARES_VERSION_RC + if ( entry == "false" ) + { + checkEntries.append( { entry, + [] { return tr( "is always false" ); }, + [] { return tr( "The computer says no." ); }, + false, + required } ); + } + if ( entry == "true" ) + { + checkEntries.append( { entry, + [] { return tr( "is always true" ); }, + [] { return tr( "The computer says yes." ); }, + true, + required } ); + } + if ( entry == "snark" ) + { + static unsigned int snark_count = 0; + checkEntries.append( { entry, + [] { return tr( "is checked three times." ); }, + [] + { + return tr( "The snark has not been checked three times.", + "The (some mythological beast) has not been checked three times." ); + }, + ++snark_count > 3, + required } ); + } +#endif } return checkEntries; } diff --git a/src/modules/welcome/checker/ResultsListWidget.cpp b/src/modules/welcome/checker/ResultsListWidget.cpp index acbb48e42..4c802acfc 100644 --- a/src/modules/welcome/checker/ResultsListWidget.cpp +++ b/src/modules/welcome/checker/ResultsListWidget.cpp @@ -18,6 +18,7 @@ #include "utils/Logger.h" #include "utils/Retranslator.h" #include "widgets/FixedAspectRatioLabel.h" +#include "widgets/WaitingWidget.h" #include #include @@ -110,10 +111,11 @@ ResultsListDialog::ResultsListDialog( const Calamares::RequirementsModel& model, m_title = new QLabel( this ); m_title->setObjectName( "resultDialogTitle" ); - createResultWidgets( - entriesLayout, m_resultWidgets, model, []( const Calamares::RequirementsModel& m, QModelIndex i ) { - return m.data( i, Calamares::RequirementsModel::HasDetails ).toBool(); - } ); + createResultWidgets( entriesLayout, + m_resultWidgets, + model, + []( const Calamares::RequirementsModel& m, QModelIndex i ) + { return m.data( i, Calamares::RequirementsModel::HasDetails ).toBool(); } ); QDialogButtonBox* buttonBox = new QDialogButtonBox( QDialogButtonBox::Close, Qt::Horizontal, this ); buttonBox->setObjectName( "resultDialogButtons" ); @@ -154,47 +156,117 @@ ResultsListWidget::ResultsListWidget( Config* config, QWidget* parent ) { setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Expanding ); - QBoxLayout* mainLayout = new QVBoxLayout; - QBoxLayout* entriesLayout = new QVBoxLayout; + m_mainLayout = new QVBoxLayout; + m_entriesLayout = new QVBoxLayout; - setLayout( mainLayout ); + setLayout( m_mainLayout ); int paddingSize = qBound( 32, CalamaresUtils::defaultFontHeight() * 4, 128 ); QHBoxLayout* spacerLayout = new QHBoxLayout; - mainLayout->addLayout( spacerLayout ); + m_mainLayout->addLayout( spacerLayout ); spacerLayout->addSpacing( paddingSize ); - spacerLayout->addLayout( entriesLayout ); + spacerLayout->addLayout( m_entriesLayout ); spacerLayout->addSpacing( paddingSize ); CalamaresUtils::unmarginLayout( spacerLayout ); - auto* explanation = new QLabel( m_config->warningMessage() ); - explanation->setWordWrap( true ); - explanation->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Preferred ); - explanation->setOpenExternalLinks( false ); - explanation->setObjectName( "resultsExplanation" ); - entriesLayout->addWidget( explanation ); + QHBoxLayout* explanationLayout = new QHBoxLayout; + m_explanation = new QLabel( m_config->warningMessage() ); + m_explanation->setWordWrap( true ); + m_explanation->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Preferred ); + m_explanation->setOpenExternalLinks( false ); + m_explanation->setObjectName( "resultsExplanation" ); + explanationLayout->addWidget( m_explanation ); + m_countdown = new CountdownWaitingWidget; + explanationLayout->addWidget( m_countdown ); + m_countdown->start(); - connect( config, &Config::warningMessageChanged, explanation, &QLabel::setText ); - connect( explanation, &QLabel::linkActivated, this, &ResultsListWidget::linkClicked ); + m_entriesLayout->addLayout( explanationLayout ); + m_entriesLayout->insertSpacing( 1, CalamaresUtils::defaultFontHeight() / 2 ); + m_mainLayout->addStretch(); + + requirementsChanged(); + + connect( config, + &Config::warningMessageChanged, + [ = ]( QString s ) + { + if ( isModelFilled() ) + { + m_explanation->setText( s ); + } + } ); + connect( m_explanation, &QLabel::linkActivated, this, &ResultsListWidget::linkClicked ); + connect( config->requirementsModel(), + &Calamares::RequirementsModel::modelReset, + this, + &ResultsListWidget::requirementsChanged ); + + CALAMARES_RETRANSLATE_SLOT( &ResultsListWidget::retranslate ); +} + + +void +ResultsListWidget::linkClicked( const QString& link ) +{ + if ( link == "#details" ) + { + auto* dialog = new ResultsListDialog( *( m_config->requirementsModel() ), this ); + dialog->exec(); + dialog->deleteLater(); + } +} + +void +ResultsListWidget::retranslate() +{ + const auto& model = *( m_config->requirementsModel() ); + // Retranslate the widgets that there **are**; + // these remain in-order relative to the model. + for ( auto i = 0; i < model.count() && i < m_resultWidgets.count(); i++ ) + { + if ( m_resultWidgets[ i ] ) + { + m_resultWidgets[ i ]->setText( + model.data( model.index( i ), Calamares::RequirementsModel::NegatedText ).toString() ); + } + } +} + +void +ResultsListWidget::requirementsChanged() +{ + if ( !isModelFilled() ) + { + return; + } // Check that all are satisfied (gives warnings if not) and // all *mandatory* entries are satisfied (gives errors if not). - const bool requirementsSatisfied = config->requirementsModel()->satisfiedRequirements(); - auto isUnSatisfied = []( const Calamares::RequirementsModel& m, QModelIndex i ) { - return !m.data( i, Calamares::RequirementsModel::Satisfied ).toBool(); - }; + const bool requirementsSatisfied = m_config->requirementsModel()->satisfiedRequirements(); + auto isUnSatisfied = []( const Calamares::RequirementsModel& m, QModelIndex i ) + { return !m.data( i, Calamares::RequirementsModel::Satisfied ).toBool(); }; - createResultWidgets( entriesLayout, m_resultWidgets, *( config->requirementsModel() ), isUnSatisfied ); + + std::for_each( m_resultWidgets.begin(), + m_resultWidgets.end(), + []( QWidget* w ) + { + if ( w ) + { + w->deleteLater(); + } + } ); if ( !requirementsSatisfied ) { - entriesLayout->insertSpacing( 1, CalamaresUtils::defaultFontHeight() / 2 ); - mainLayout->addStretch(); + createResultWidgets( m_entriesLayout, m_resultWidgets, *( m_config->requirementsModel() ), isUnSatisfied ); } else { + m_countdown->stop(); + m_countdown->hide(); if ( !Calamares::Branding::instance()->imagePath( Calamares::Branding::ProductWelcome ).isEmpty() ) { QPixmap theImage @@ -218,40 +290,26 @@ ResultsListWidget::ResultsListWidget( Config* config, QWidget* parent ) imageLabel->setAlignment( Qt::AlignCenter ); imageLabel->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Expanding ); imageLabel->setObjectName( "welcomeLogo" ); - mainLayout->addWidget( imageLabel ); + m_mainLayout->addWidget( imageLabel ); } } - explanation->setAlignment( Qt::AlignCenter ); + m_explanation->setAlignment( Qt::AlignCenter ); } - CALAMARES_RETRANSLATE_SLOT( &ResultsListWidget::retranslate ); + retranslate(); } - -void -ResultsListWidget::linkClicked( const QString& link ) +bool +ResultsListWidget::isModelFilled() { - if ( link == "#details" ) + if ( m_config->requirementsModel()->count() < m_requirementsSeen ) { - auto* dialog = new ResultsListDialog( *( m_config->requirementsModel() ), this ); - dialog->exec(); - dialog->deleteLater(); + return false; } + m_requirementsSeen = m_config->requirementsModel()->count(); + return true; } -void -ResultsListWidget::retranslate() -{ - const auto& model = *( m_config->requirementsModel() ); - for ( auto i = 0; i < model.count(); i++ ) - { - if ( m_resultWidgets[ i ] ) - { - m_resultWidgets[ i ]->setText( - model.data( model.index( i ), Calamares::RequirementsModel::NegatedText ).toString() ); - } - } -} #include "utils/moc-warnings.h" diff --git a/src/modules/welcome/checker/ResultsListWidget.h b/src/modules/welcome/checker/ResultsListWidget.h index 5e96b74a0..ca47b3a13 100644 --- a/src/modules/welcome/checker/ResultsListWidget.h +++ b/src/modules/welcome/checker/ResultsListWidget.h @@ -17,7 +17,11 @@ #include +class CountdownWaitingWidget; + +class QBoxLayout; class QLabel; + class ResultsListWidget : public QWidget { Q_OBJECT @@ -27,10 +31,38 @@ public: private: /// @brief A link in the explanatory text has been clicked void linkClicked( const QString& link ); + /// @brief The model of requirements changed + void requirementsChanged(); + void retranslate(); - QList< ResultWidget* > m_resultWidgets; ///< One widget for each unsatisfied entry + /** @brief The model can be reset and re-filled, is it full yet? + * + * We count how many requirements we have seen; since the model + * does not shrink, we can avoid reacting to model-is-cleared + * events because the size of the model is then (briefly) smaller + * than what we expect. + * + * Returns true if the model contains at least m_requirementsSeen + * elements, and updates m_requirementsSeen. (Which is why the + * method is not const) + */ + bool isModelFilled(); + + /** @brief A list of widgets, one per entry in the requirements model + * + * Unsatisfied entries have a non-null widget pointer, while requirements + * entries that **are** satisfied have no widget. + */ + QList< ResultWidget* > m_resultWidgets; Config* m_config = nullptr; + + // UI parts, which need updating when the model changes + QLabel* m_explanation = nullptr; + CountdownWaitingWidget* m_countdown = nullptr; + QBoxLayout* m_mainLayout = nullptr; + QBoxLayout* m_entriesLayout = nullptr; + int m_requirementsSeen = 0; }; #endif // CHECKER_RESULTSLISTWIDGET_H diff --git a/src/modules/welcome/welcome.conf b/src/modules/welcome/welcome.conf index 6e11817bf..b86231c3f 100644 --- a/src/modules/welcome/welcome.conf +++ b/src/modules/welcome/welcome.conf @@ -64,6 +64,14 @@ requirements: # the host system satisfying the condition. # # This sample file lists all the conditions that are known. + # + # Note that the last three checks are for testing-purposes only, + # and shouldn't be used in production (they are only available + # when building Calamares in development mode): + # - *false* is a check that is always false (unsatisfied) + # - *true* is a check that is always true (satisfied) + # - *snark* is a check that is only satisfied once it has been checked + # at least three times ("what I tell you three times is true"). check: - storage - ram @@ -71,6 +79,9 @@ requirements: - internet - root - screen + - false + - true + - snark # List conditions that **must** be satisfied (from the list # of conditions, above) for installation to proceed. # If any of these conditions are not met, the user cannot