Merge branch 'issue-1924' into work-3.3

This is a rather clunky implementation of re-check requirements.

"Clunky" because the UI parts are re-created each time, rather
than fishing from a model of checked (or unchecked) requirements.
The Widgets parts should be updated to use a full model, rather
than the recreate-list-of-Widgets implementation they have now.

Unrelated changes pull in a bunch of improvements to the
waiting spinner widget.
This commit is contained in:
Adriaan de Groot 2022-04-16 12:34:40 +02:00
commit 7e5df42fc0
19 changed files with 589 additions and 226 deletions

View File

@ -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 changelog -- this log starts with version 3.2.0. The release notes on the
website will have to do for older versions. 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) # # 3.2.55 (2022-04-11) #
This release contains contributions from (alphabetically by first name): This release contains contributions from (alphabetically by first name):

View File

@ -32,6 +32,7 @@ RequirementsChecker::RequirementsChecker( QVector< Module* > modules, Requiremen
, m_progressTimer( nullptr ) , m_progressTimer( nullptr )
, m_progressTimeouts( 0 ) , m_progressTimeouts( 0 )
{ {
m_model->clear();
m_watchers.reserve( m_modules.count() ); m_watchers.reserve( m_modules.count() );
connect( this, &RequirementsChecker::requirementsProgress, model, &RequirementsModel::setProgressMessage ); connect( this, &RequirementsChecker::requirementsProgress, model, &RequirementsModel::setProgressMessage );
} }
@ -63,9 +64,9 @@ RequirementsChecker::finished()
static QMutex finishedMutex; static QMutex finishedMutex;
QMutexLocker lock( &finishedMutex ); QMutexLocker lock( &finishedMutex );
if ( m_progressTimer && std::all_of( m_watchers.cbegin(), m_watchers.cend(), []( const Watcher* w ) { if ( m_progressTimer
return w && w->isFinished(); && std::all_of(
} ) ) m_watchers.cbegin(), m_watchers.cend(), []( const Watcher* w ) { return w && w->isFinished(); } ) )
{ {
cDebug() << "All requirements have been checked."; cDebug() << "All requirements have been checked.";
if ( m_progressTimer ) if ( m_progressTimer )
@ -100,7 +101,10 @@ RequirementsChecker::reportProgress()
m_progressTimeouts++; m_progressTimeouts++;
QStringList remainingNames; QStringList remainingNames;
auto remaining = std::count_if( m_watchers.cbegin(), m_watchers.cend(), [&]( const Watcher* w ) { auto remaining = std::count_if( m_watchers.cbegin(),
m_watchers.cend(),
[ & ]( const Watcher* w )
{
if ( w && !w->isFinished() ) if ( w && !w->isFinished() )
{ {
remainingNames << w->objectName(); remainingNames << w->objectName();

View File

@ -15,6 +15,16 @@
namespace Calamares namespace Calamares
{ {
void
RequirementsModel::clear()
{
QMutexLocker l( &m_addLock );
emit beginResetModel();
m_requirements.clear();
changeRequirementsList();
emit endResetModel();
}
void void
RequirementsModel::addRequirementsList( const Calamares::RequirementsList& requirements ) RequirementsModel::addRequirementsList( const Calamares::RequirementsList& requirements )
{ {

View File

@ -77,6 +77,10 @@ signals:
protected: protected:
QHash< int, QByteArray > roleNames() const override; QHash< int, QByteArray > roleNames() const override;
///@brief Clears the requirements; resets the model
void clear();
///@brief Append some requirements; resets the model ///@brief Append some requirements; resets the model
void addRequirementsList( const Calamares::RequirementsList& requirements ); void addRequirementsList( const Calamares::RequirementsList& requirements );

View File

@ -30,16 +30,11 @@ set(calamaresui_SOURCES
widgets/LogWidget.cpp widgets/LogWidget.cpp
widgets/TranslationFix.cpp widgets/TranslationFix.cpp
widgets/WaitingWidget.cpp widgets/WaitingWidget.cpp
${CMAKE_SOURCE_DIR}/3rdparty/waitingspinnerwidget.cpp widgets/waitingspinnerwidget.cpp
Branding.cpp Branding.cpp
ViewManager.cpp ViewManager.cpp
) )
# Don't warn about third-party sources
mark_thirdparty_code(
${CMAKE_SOURCE_DIR}/3rdparty/waitingspinnerwidget.cpp
)
if(WITH_PYTHON) if(WITH_PYTHON)
list(APPEND calamaresui_SOURCES modulesystem/PythonJobModule.cpp) list(APPEND calamaresui_SOURCES modulesystem/PythonJobModule.cpp)
endif() endif()

View File

@ -349,7 +349,18 @@ ModuleManager::checkRequirements()
connect( rq, connect( rq,
&RequirementsChecker::done, &RequirementsChecker::done,
this, 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 ); QTimer::singleShot( 0, rq, &RequirementsChecker::run );
} }

View File

@ -12,46 +12,108 @@
#include "utils/CalamaresUtilsGui.h" #include "utils/CalamaresUtilsGui.h"
#include "3rdparty/waitingspinnerwidget.h"
#include <QBoxLayout> #include <QBoxLayout>
#include <QLabel> #include <QLabel>
#include <QTimer>
WaitingWidget::WaitingWidget( const QString& text, QWidget* parent ) WaitingWidget::WaitingWidget( const QString& text, QWidget* parent )
: QWidget( parent ) : WaitingSpinnerWidget( parent, false, false )
{ {
QBoxLayout* waitingLayout = new QVBoxLayout; int spnrSize = CalamaresUtils::defaultFontHeight() * 4;
setLayout( waitingLayout ); setFixedSize( spnrSize, spnrSize );
waitingLayout->addStretch(); setInnerRadius( spnrSize / 2 );
QBoxLayout* pbLayout = new QHBoxLayout; setLineLength( spnrSize / 2 );
waitingLayout->addLayout( pbLayout ); setLineWidth( spnrSize / 8 );
pbLayout->addStretch(); setAlignment( Qt::AlignmentFlag::AlignBottom );
setText( text );
WaitingSpinnerWidget* spnr = new WaitingSpinnerWidget(); start();
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 );
} }
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 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();
}
} }

View File

@ -10,20 +10,61 @@
#ifndef WAITINGWIDGET_H #ifndef WAITINGWIDGET_H
#define WAITINGWIDGET_H #define WAITINGWIDGET_H
#include <QWidget> #include "widgets/waitingspinnerwidget.h"
#include <chrono>
#include <memory>
class QLabel; 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 Q_OBJECT
public: 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: private:
QLabel* m_waitingLabel; struct Private;
std::unique_ptr< Private > d;
}; };
#endif // WAITINGWIDGET_H #endif // WAITINGWIDGET_H

View File

@ -2,6 +2,7 @@
* SPDX-FileCopyrightText: 2012-2014 Alexander Turkin * SPDX-FileCopyrightText: 2012-2014 Alexander Turkin
* SPDX-FileCopyrightText: 2014 William Hallatt * SPDX-FileCopyrightText: 2014 William Hallatt
* SPDX-FileCopyrightText: 2015 Jacob Dawid * SPDX-FileCopyrightText: 2015 Jacob Dawid
* SPDX-FileCopyrightText: 2018 huxingyi
* SPDX-License-Identifier: MIT * SPDX-License-Identifier: MIT
*/ */
@ -38,49 +39,41 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#include <QPainter> #include <QPainter>
#include <QTimer> #include <QTimer>
static bool isAlignCenter(Qt::AlignmentFlag a)
{
return a == Qt::AlignmentFlag::AlignVCenter;
}
WaitingSpinnerWidget::WaitingSpinnerWidget(QWidget *parent, WaitingSpinnerWidget::WaitingSpinnerWidget(QWidget *parent,
bool centerOnParent, bool centerOnParent,
bool disableParentWhenSpinning) bool disableParentWhenSpinning)
: QWidget(parent), : WaitingSpinnerWidget(Qt::WindowModality::NonModal, parent, centerOnParent, disableParentWhenSpinning)
_centerOnParent(centerOnParent), {}
_disableParentWhenSpinning(disableParentWhenSpinning) {
initialize();
}
WaitingSpinnerWidget::WaitingSpinnerWidget(Qt::WindowModality modality, WaitingSpinnerWidget::WaitingSpinnerWidget(Qt::WindowModality modality,
QWidget *parent, QWidget *parent,
bool centerOnParent, bool centerOnParent,
bool disableParentWhenSpinning) bool disableParentWhenSpinning)
: QWidget(parent, Qt::Dialog | Qt::FramelessWindowHint), : QWidget(parent, modality == Qt::WindowModality::NonModal ? Qt::WindowFlags() : Qt::Dialog | Qt::FramelessWindowHint),
_centerOnParent(centerOnParent), _centerOnParent(centerOnParent),
_disableParentWhenSpinning(disableParentWhenSpinning){ _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;
_timer = new QTimer(this); _timer = new QTimer(this);
connect(_timer, SIGNAL(timeout()), this, SLOT(rotate())); connect(_timer, SIGNAL(timeout()), this, SLOT(rotate()));
updateSize(); updateSize();
updateTimer(); updateTimer();
hide(); 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 *) { void WaitingSpinnerWidget::paintEvent(QPaintEvent *) {
@ -98,6 +91,7 @@ void WaitingSpinnerWidget::paintEvent(QPaintEvent *) {
painter.save(); painter.save();
painter.translate(_innerRadius + _lineLength, painter.translate(_innerRadius + _lineLength,
_innerRadius + _lineLength); _innerRadius + _lineLength);
painter.translate((width() - _imageSize.width()) / 2, 0);
qreal rotateAngle = qreal rotateAngle =
static_cast<qreal>(360 * i) / static_cast<qreal>(_numberOfLines); static_cast<qreal>(360 * i) / static_cast<qreal>(_numberOfLines);
painter.rotate(rotateAngle); painter.rotate(rotateAngle);
@ -114,6 +108,17 @@ void WaitingSpinnerWidget::paintEvent(QPaintEvent *) {
_roundness, Qt::RelativeSize); _roundness, Qt::RelativeSize);
painter.restore(); 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() { void WaitingSpinnerWidget::start() {
@ -166,39 +171,58 @@ void WaitingSpinnerWidget::setInnerRadius(int radius) {
updateSize(); 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; return _color;
} }
qreal WaitingSpinnerWidget::roundness() { QColor WaitingSpinnerWidget::textColor() const {
return _textColor;
}
QString WaitingSpinnerWidget::text() const {
return _text;
}
qreal WaitingSpinnerWidget::roundness() const {
return _roundness; return _roundness;
} }
qreal WaitingSpinnerWidget::minimumTrailOpacity() { qreal WaitingSpinnerWidget::minimumTrailOpacity() const {
return _minimumTrailOpacity; return _minimumTrailOpacity;
} }
qreal WaitingSpinnerWidget::trailFadePercentage() { qreal WaitingSpinnerWidget::trailFadePercentage() const {
return _trailFadePercentage; return _trailFadePercentage;
} }
qreal WaitingSpinnerWidget::revolutionsPersSecond() { qreal WaitingSpinnerWidget::revolutionsPersSecond() const {
return _revolutionsPerSecond; return _revolutionsPerSecond;
} }
int WaitingSpinnerWidget::numberOfLines() { int WaitingSpinnerWidget::numberOfLines() const {
return _numberOfLines; return _numberOfLines;
} }
int WaitingSpinnerWidget::lineLength() { int WaitingSpinnerWidget::lineLength() const {
return _lineLength; return _lineLength;
} }
int WaitingSpinnerWidget::lineWidth() { int WaitingSpinnerWidget::lineWidth() const {
return _lineWidth; return _lineWidth;
} }
int WaitingSpinnerWidget::innerRadius() { int WaitingSpinnerWidget::innerRadius() const {
return _innerRadius; return _innerRadius;
} }
@ -214,6 +238,10 @@ void WaitingSpinnerWidget::setColor(QColor color) {
_color = color; _color = color;
} }
void WaitingSpinnerWidget::setTextColor(QColor color) {
_textColor = color;
}
void WaitingSpinnerWidget::setRevolutionsPerSecond(qreal revolutionsPerSecond) { void WaitingSpinnerWidget::setRevolutionsPerSecond(qreal revolutionsPerSecond) {
_revolutionsPerSecond = revolutionsPerSecond; _revolutionsPerSecond = revolutionsPerSecond;
updateTimer(); updateTimer();
@ -237,7 +265,14 @@ void WaitingSpinnerWidget::rotate() {
void WaitingSpinnerWidget::updateSize() { void WaitingSpinnerWidget::updateSize() {
int size = (_innerRadius + _lineLength) * 2; int size = (_innerRadius + _lineLength) * 2;
_imageSize = QSize(size, size);
if (_text.isEmpty() || isAlignCenter(alignment())) {
setFixedSize(size, size); 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() { void WaitingSpinnerWidget::updateTimer() {

View File

@ -2,6 +2,7 @@
* SPDX-FileCopyrightText: 2012-2014 Alexander Turkin * SPDX-FileCopyrightText: 2012-2014 Alexander Turkin
* SPDX-FileCopyrightText: 2014 William Hallatt * SPDX-FileCopyrightText: 2014 William Hallatt
* SPDX-FileCopyrightText: 2015 Jacob Dawid * SPDX-FileCopyrightText: 2015 Jacob Dawid
* SPDX-FileCopyrightText: 2018 huxingyi
* SPDX-License-Identifier: MIT * 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 { class WaitingSpinnerWidget : public QWidget {
Q_OBJECT Q_OBJECT
public: public:
/*! Constructor for "standard" widget behaviour - use this /** @brief Constructor for "standard" widget behaviour
* constructor if you wish to, e.g. embed your widget in another. */ *
* Use this constructor if you wish to, e.g. embed your widget in another.
*/
WaitingSpinnerWidget(QWidget *parent = nullptr, WaitingSpinnerWidget(QWidget *parent = nullptr,
bool centerOnParent = true, bool centerOnParent = true,
bool disableParentWhenSpinning = true); bool disableParentWhenSpinning = true);
/*! Constructor - use this constructor to automatically create a modal /** @brief Constructor
*
* Use this constructor to automatically create a modal
* ("blocking") spinner on top of the calling widget/window. If a valid * ("blocking") spinner on top of the calling widget/window. If a valid
* parent widget is provided, "centreOnParent" will ensure that * parent widget is provided, "centreOnParent" will ensure that
* QtWaitingSpinner automatically centres itself on it, if not, * QtWaitingSpinner automatically centres itself on it, if not,
* "centreOnParent" is ignored. */ * @p centerOnParent is ignored.
*/
WaitingSpinnerWidget(Qt::WindowModality modality, WaitingSpinnerWidget(Qt::WindowModality modality,
QWidget *parent = nullptr, QWidget *parent = nullptr,
bool centerOnParent = true, bool centerOnParent = true,
bool disableParentWhenSpinning = true); bool disableParentWhenSpinning = true);
public slots: WaitingSpinnerWidget(const WaitingSpinnerWidget&) = delete;
void start(); WaitingSpinnerWidget& operator=(const WaitingSpinnerWidget&) = delete;
void stop();
public:
void setColor(QColor color); void setColor(QColor color);
void setTextColor(QColor color);
void setRoundness(qreal roundness); void setRoundness(qreal roundness);
void setMinimumTrailOpacity(qreal minimumTrailOpacity); void setMinimumTrailOpacity(qreal minimumTrailOpacity);
void setTrailFadePercentage(qreal trail); void setTrailFadePercentage(qreal trail);
@ -67,21 +72,45 @@ public:
void setLineLength(int length); void setLineLength(int length);
void setLineWidth(int width); void setLineWidth(int width);
void setInnerRadius(int radius); void setInnerRadius(int radius);
void setText(QString text);
QColor color(); /** @brief Sets the text displayed in or below the spinner
qreal roundness(); *
qreal minimumTrailOpacity(); * If the text is empty, no text is displayed. The text is displayed
qreal trailFadePercentage(); * in or below the spinner depending on the value of alignment().
qreal revolutionsPersSecond(); * With AlignBottom, the text is displayed below the spinner,
int numberOfLines(); * centered horizontally relative to the spinner; any other alignment
int lineLength(); * will put the text in the middle of the spinner itself.
int lineWidth(); */
int innerRadius(); 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; bool isSpinning() const;
private slots: public Q_SLOTS:
void start();
void stop();
private Q_SLOTS:
void rotate(); void rotate();
protected: protected:
@ -94,29 +123,37 @@ private:
qreal trailFadePerc, qreal minOpacity, qreal trailFadePerc, qreal minOpacity,
QColor color); QColor color);
void initialize();
void updateSize(); void updateSize();
void updateTimer(); void updateTimer();
void updatePosition(); void updatePosition();
private: private:
QColor _color; // PI, leading to a full fade in one whole revolution
qreal _roundness; // 0..100 static constexpr const auto radian = 3.14159265358979323846;
qreal _minimumTrailOpacity;
qreal _trailFadePercentage;
qreal _revolutionsPerSecond;
int _numberOfLines;
int _lineLength;
int _lineWidth;
int _innerRadius;
private: // Spinner-wheel related settings
WaitingSpinnerWidget(const WaitingSpinnerWidget&); QColor _color = Qt::black;
WaitingSpinnerWidget& operator=(const WaitingSpinnerWidget&); 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; // Text-related settings
bool _centerOnParent; Qt::AlignmentFlag _alignment = Qt::AlignmentFlag::AlignBottom;
bool _disableParentWhenSpinning; QString _text;
int _currentCounter; QColor _textColor = Qt::black;
bool _isSpinning;
// Environment settings
bool _centerOnParent = true;
bool _disableParentWhenSpinning = true;
// Internal bits
QTimer *_timer = nullptr;
int _currentCounter = 0;
bool _isSpinning = false;
}; };

View File

@ -181,23 +181,25 @@ for determining the relative weights there.
## Global storage keys ## 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. 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 Key |Source |Consumers|Description
---|---|---|--- ------------------|----------------|---|---
btrfsSubvolumes|mount|fstab|List of maps containing the mountpoint and btrtfs subvolume btrfsSubvolumes |mount |fstab|List of maps containing the mountpoint and btrtfs subvolume
btrfsRootSubvolume|mount|bootloader, luksopenswaphook|String containing the subvolume mounted at root 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 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 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 fullname |users ||The full username (e.g. "Jane Q. Public")
netinstallAdd|packagechooser|netinstall|Data to add to netinstall tree. Same format as netinstall.yaml hostname |users ||A string containing the hostname of the new system
netinstallSelect|packagechooser|netinstall|List of group names to select in the netinstall tree netinstallAdd |packagechooser |netinstall|Data to add to netinstall tree. Same format as netinstall.yaml
partitions|partition, rawfs|numerous modules|List of maps of metadata about each partition netinstallSelect |packagechooser |netinstall|List of group names to select in the netinstall tree
rootMountPoint|mount|numerous modules|A string with the absolute path to the root mountpoint partitions |partition, rawfs|numerous modules|List of maps of metadata about each partition
username|users|networkcfg, plasmainf, preservefiles|A string containing the username of the new user rootMountPoint |mount |numerous modules|A string with the absolute path to the root mountpoint
zfsDatasets|zfs|bootloader, grubcfg, mount|List of maps of zfs datasets including the name and mount information username |users |networkcfg, plasmainf, preservefiles|A string containing the username of the new user
zfsInfo|partition|mount, zfs|List of encrypted zfs partitions and the encription info zfsDatasets |zfs |bootloader, grubcfg, mount|List of maps of zfs datasets including the name and mount information
zfsPoolInfo|zfs|mount, umount|List of maps of zfs pool info including the name and mountpoint 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 ## C++ modules

View File

@ -11,7 +11,6 @@
#include "LocaleViewStep.h" #include "LocaleViewStep.h"
#include "LocalePage.h" #include "LocalePage.h"
#include "widgets/WaitingWidget.h"
#include "GlobalStorage.h" #include "GlobalStorage.h"
#include "JobQueue.h" #include "JobQueue.h"

View File

@ -10,7 +10,7 @@
#include "ScanningDialog.h" #include "ScanningDialog.h"
#include "3rdparty/waitingspinnerwidget.h" #include "widgets/waitingspinnerwidget.h"
#include <QBoxLayout> #include <QBoxLayout>
#include <QFutureWatcher> #include <QFutureWatcher>

View File

@ -445,6 +445,15 @@ Config::setFullName( const QString& name )
if ( name != m_fullName ) if ( name != m_fullName )
{ {
m_fullName = name; m_fullName = name;
Calamares::GlobalStorage* gs = Calamares::JobQueue::instance()->globalStorage();
if ( name.isEmpty() )
{
gs->remove( "fullname" );
}
else
{
gs->insert( "fullname", name );
}
emit fullNameChanged( name ); emit fullNameChanged( name );
// Build login and hostname, if needed // Build login and hostname, if needed

View File

@ -65,13 +65,18 @@ CheckerContainer::requirementsComplete( bool ok )
} }
} }
if ( m_waitingWidget )
{
layout()->removeWidget( m_waitingWidget ); layout()->removeWidget( m_waitingWidget );
m_waitingWidget->deleteLater(); m_waitingWidget->deleteLater();
m_waitingWidget = nullptr; // Don't delete in destructor m_waitingWidget = nullptr; // Don't delete in destructor
}
if ( !m_checkerWidget )
{
m_checkerWidget = new ResultsListWidget( m_config, this ); m_checkerWidget = new ResultsListWidget( m_config, this );
m_checkerWidget->setObjectName( "requirementsChecker" ); m_checkerWidget->setObjectName( "requirementsChecker" );
layout()->addWidget( m_checkerWidget ); layout()->addWidget( m_checkerWidget );
}
m_verdict = ok; m_verdict = ok;
} }

View File

@ -15,6 +15,9 @@
#include "CheckerContainer.h" #include "CheckerContainer.h"
#include "partman_devices.h" #include "partman_devices.h"
#include "CalamaresVersion.h" // For development-or-not
#include "GlobalStorage.h"
#include "JobQueue.h"
#include "Settings.h" #include "Settings.h"
#include "modulesystem/Requirement.h" #include "modulesystem/Requirement.h"
#include "network/Manager.h" #include "network/Manager.h"
@ -26,9 +29,6 @@
#include "utils/Variant.h" #include "utils/Variant.h"
#include "widgets/WaitingWidget.h" #include "widgets/WaitingWidget.h"
#include "GlobalStorage.h"
#include "JobQueue.h"
#include <QDBusConnection> #include <QDBusConnection>
#include <QDBusInterface> #include <QDBusInterface>
#include <QDir> #include <QDir>
@ -148,28 +148,29 @@ GeneralRequirements::checkRequirements()
Calamares::RequirementsList checkEntries; Calamares::RequirementsList checkEntries;
foreach ( const QString& entry, m_entriesToCheck ) foreach ( const QString& entry, m_entriesToCheck )
{ {
const bool required = m_entriesToRequire.contains( entry );
if ( entry == "storage" ) if ( entry == "storage" )
{ {
checkEntries.append( checkEntries.append(
{ entry, { entry,
[req = m_requiredStorageGiB] { return tr( "has at least %1 GiB available drive space" ).arg( req ); }, [ req = m_requiredStorageGiB ]
[req = m_requiredStorageGiB] { { return tr( "has at least %1 GiB available drive space" ).arg( req ); },
return tr( "There is not enough drive space. At least %1 GiB is required." ).arg( req ); [ req = m_requiredStorageGiB ]
}, { return tr( "There is not enough drive space. At least %1 GiB is required." ).arg( req ); },
enoughStorage, enoughStorage,
m_entriesToRequire.contains( entry ) } ); required } );
} }
else if ( entry == "ram" ) else if ( entry == "ram" )
{ {
checkEntries.append( checkEntries.append(
{ entry, { entry,
[req = m_requiredRamGiB] { return tr( "has at least %1 GiB working memory" ).arg( req ); }, [ req = m_requiredRamGiB ] { return tr( "has at least %1 GiB working memory" ).arg( req ); },
[req = m_requiredRamGiB] { [ req = m_requiredRamGiB ] {
return tr( "The system does not have enough working memory. At least %1 GiB is required." ) return tr( "The system does not have enough working memory. At least %1 GiB is required." )
.arg( req ); .arg( req );
}, },
enoughRam, enoughRam,
m_entriesToRequire.contains( entry ) } ); required } );
} }
else if ( entry == "power" ) else if ( entry == "power" )
{ {
@ -177,7 +178,7 @@ GeneralRequirements::checkRequirements()
[] { return tr( "is plugged in to a power source" ); }, [] { return tr( "is plugged in to a power source" ); },
[] { return tr( "The system is not plugged in to a power source." ); }, [] { return tr( "The system is not plugged in to a power source." ); },
hasPower, hasPower,
m_entriesToRequire.contains( entry ) } ); required } );
} }
else if ( entry == "internet" ) else if ( entry == "internet" )
{ {
@ -185,32 +186,65 @@ GeneralRequirements::checkRequirements()
[] { return tr( "is connected to the Internet" ); }, [] { return tr( "is connected to the Internet" ); },
[] { return tr( "The system is not connected to the Internet." ); }, [] { return tr( "The system is not connected to the Internet." ); },
hasInternet, hasInternet,
m_entriesToRequire.contains( entry ) } ); required } );
} }
else if ( entry == "root" ) else if ( entry == "root" )
{ {
checkEntries.append( { entry, checkEntries.append( { entry,
[] { return tr( "is running the installer as an administrator (root)" ); }, [] { return tr( "is running the installer as an administrator (root)" ); },
[] { []
{
return Calamares::Settings::instance()->isSetupMode() return Calamares::Settings::instance()->isSetupMode()
? tr( "The setup program is not running with administrator rights." ) ? tr( "The setup program is not running with administrator rights." )
: tr( "The installer is not running with administrator rights." ); : tr( "The installer is not running with administrator rights." );
}, },
isRoot, isRoot,
m_entriesToRequire.contains( entry ) } ); required } );
} }
else if ( entry == "screen" ) else if ( entry == "screen" )
{ {
checkEntries.append( { entry, checkEntries.append( { entry,
[] { return tr( "has a screen large enough to show the whole installer" ); }, [] { return tr( "has a screen large enough to show the whole installer" ); },
[] { []
{
return Calamares::Settings::instance()->isSetupMode() 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 setup program." )
: tr( "The screen is too small to display the installer." ); : tr( "The screen is too small to display the installer." );
}, },
enoughScreen, 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; return checkEntries;
} }

View File

@ -18,6 +18,7 @@
#include "utils/Logger.h" #include "utils/Logger.h"
#include "utils/Retranslator.h" #include "utils/Retranslator.h"
#include "widgets/FixedAspectRatioLabel.h" #include "widgets/FixedAspectRatioLabel.h"
#include "widgets/WaitingWidget.h"
#include <QAbstractButton> #include <QAbstractButton>
#include <QDialog> #include <QDialog>
@ -110,10 +111,11 @@ ResultsListDialog::ResultsListDialog( const Calamares::RequirementsModel& model,
m_title = new QLabel( this ); m_title = new QLabel( this );
m_title->setObjectName( "resultDialogTitle" ); m_title->setObjectName( "resultDialogTitle" );
createResultWidgets( createResultWidgets( entriesLayout,
entriesLayout, m_resultWidgets, model, []( const Calamares::RequirementsModel& m, QModelIndex i ) { m_resultWidgets,
return m.data( i, Calamares::RequirementsModel::HasDetails ).toBool(); model,
} ); []( const Calamares::RequirementsModel& m, QModelIndex i )
{ return m.data( i, Calamares::RequirementsModel::HasDetails ).toBool(); } );
QDialogButtonBox* buttonBox = new QDialogButtonBox( QDialogButtonBox::Close, Qt::Horizontal, this ); QDialogButtonBox* buttonBox = new QDialogButtonBox( QDialogButtonBox::Close, Qt::Horizontal, this );
buttonBox->setObjectName( "resultDialogButtons" ); buttonBox->setObjectName( "resultDialogButtons" );
@ -154,47 +156,117 @@ ResultsListWidget::ResultsListWidget( Config* config, QWidget* parent )
{ {
setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Expanding ); setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Expanding );
QBoxLayout* mainLayout = new QVBoxLayout; m_mainLayout = new QVBoxLayout;
QBoxLayout* entriesLayout = new QVBoxLayout; m_entriesLayout = new QVBoxLayout;
setLayout( mainLayout ); setLayout( m_mainLayout );
int paddingSize = qBound( 32, CalamaresUtils::defaultFontHeight() * 4, 128 ); int paddingSize = qBound( 32, CalamaresUtils::defaultFontHeight() * 4, 128 );
QHBoxLayout* spacerLayout = new QHBoxLayout; QHBoxLayout* spacerLayout = new QHBoxLayout;
mainLayout->addLayout( spacerLayout ); m_mainLayout->addLayout( spacerLayout );
spacerLayout->addSpacing( paddingSize ); spacerLayout->addSpacing( paddingSize );
spacerLayout->addLayout( entriesLayout ); spacerLayout->addLayout( m_entriesLayout );
spacerLayout->addSpacing( paddingSize ); spacerLayout->addSpacing( paddingSize );
CalamaresUtils::unmarginLayout( spacerLayout ); CalamaresUtils::unmarginLayout( spacerLayout );
auto* explanation = new QLabel( m_config->warningMessage() ); QHBoxLayout* explanationLayout = new QHBoxLayout;
explanation->setWordWrap( true ); m_explanation = new QLabel( m_config->warningMessage() );
explanation->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Preferred ); m_explanation->setWordWrap( true );
explanation->setOpenExternalLinks( false ); m_explanation->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Preferred );
explanation->setObjectName( "resultsExplanation" ); m_explanation->setOpenExternalLinks( false );
entriesLayout->addWidget( explanation ); 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 ); m_entriesLayout->addLayout( explanationLayout );
connect( explanation, &QLabel::linkActivated, this, &ResultsListWidget::linkClicked ); 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 // Check that all are satisfied (gives warnings if not) and
// all *mandatory* entries are satisfied (gives errors if not). // all *mandatory* entries are satisfied (gives errors if not).
const bool requirementsSatisfied = config->requirementsModel()->satisfiedRequirements(); const bool requirementsSatisfied = m_config->requirementsModel()->satisfiedRequirements();
auto isUnSatisfied = []( const Calamares::RequirementsModel& m, QModelIndex i ) { auto isUnSatisfied = []( const Calamares::RequirementsModel& m, QModelIndex i )
return !m.data( i, Calamares::RequirementsModel::Satisfied ).toBool(); { 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 ) if ( !requirementsSatisfied )
{ {
entriesLayout->insertSpacing( 1, CalamaresUtils::defaultFontHeight() / 2 ); createResultWidgets( m_entriesLayout, m_resultWidgets, *( m_config->requirementsModel() ), isUnSatisfied );
mainLayout->addStretch();
} }
else else
{ {
m_countdown->stop();
m_countdown->hide();
if ( !Calamares::Branding::instance()->imagePath( Calamares::Branding::ProductWelcome ).isEmpty() ) if ( !Calamares::Branding::instance()->imagePath( Calamares::Branding::ProductWelcome ).isEmpty() )
{ {
QPixmap theImage QPixmap theImage
@ -218,40 +290,26 @@ ResultsListWidget::ResultsListWidget( Config* config, QWidget* parent )
imageLabel->setAlignment( Qt::AlignCenter ); imageLabel->setAlignment( Qt::AlignCenter );
imageLabel->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Expanding ); imageLabel->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Expanding );
imageLabel->setObjectName( "welcomeLogo" ); 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();
} }
bool
void ResultsListWidget::isModelFilled()
ResultsListWidget::linkClicked( const QString& link )
{ {
if ( link == "#details" ) if ( m_config->requirementsModel()->count() < m_requirementsSeen )
{ {
auto* dialog = new ResultsListDialog( *( m_config->requirementsModel() ), this ); return false;
dialog->exec();
dialog->deleteLater();
} }
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" #include "utils/moc-warnings.h"

View File

@ -17,7 +17,11 @@
#include <QWidget> #include <QWidget>
class CountdownWaitingWidget;
class QBoxLayout;
class QLabel; class QLabel;
class ResultsListWidget : public QWidget class ResultsListWidget : public QWidget
{ {
Q_OBJECT Q_OBJECT
@ -27,10 +31,38 @@ public:
private: private:
/// @brief A link in the explanatory text has been clicked /// @brief A link in the explanatory text has been clicked
void linkClicked( const QString& link ); void linkClicked( const QString& link );
/// @brief The model of requirements changed
void requirementsChanged();
void retranslate(); 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; 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 #endif // CHECKER_RESULTSLISTWIDGET_H

View File

@ -64,6 +64,14 @@ requirements:
# the host system satisfying the condition. # the host system satisfying the condition.
# #
# This sample file lists all the conditions that are known. # 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: check:
- storage - storage
- ram - ram
@ -71,6 +79,9 @@ requirements:
- internet - internet
- root - root
- screen - screen
- false
- true
- snark
# List conditions that **must** be satisfied (from the list # List conditions that **must** be satisfied (from the list
# of conditions, above) for installation to proceed. # of conditions, above) for installation to proceed.
# If any of these conditions are not met, the user cannot # If any of these conditions are not met, the user cannot