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
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):

View File

@ -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 );

View File

@ -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 )
{

View File

@ -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 );

View File

@ -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()

View File

@ -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 );
}

View File

@ -12,46 +12,108 @@
#include "utils/CalamaresUtilsGui.h"
#include "3rdparty/waitingspinnerwidget.h"
#include <QBoxLayout>
#include <QLabel>
#include <QTimer>
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();
}
}

View File

@ -10,20 +10,61 @@
#ifndef WAITINGWIDGET_H
#define WAITINGWIDGET_H
#include <QWidget>
#include "widgets/waitingspinnerwidget.h"
#include <chrono>
#include <memory>
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

View File

@ -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 <QPainter>
#include <QTimer>
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<qreal>(360 * i) / static_cast<qreal>(_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() {

View File

@ -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;
};

View File

@ -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

View File

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

View File

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

View File

@ -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

View File

@ -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;
}

View File

@ -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 <QDBusConnection>
#include <QDBusInterface>
#include <QDir>
@ -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;
}

View File

@ -18,6 +18,7 @@
#include "utils/Logger.h"
#include "utils/Retranslator.h"
#include "widgets/FixedAspectRatioLabel.h"
#include "widgets/WaitingWidget.h"
#include <QAbstractButton>
#include <QDialog>
@ -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"

View File

@ -17,7 +17,11 @@
#include <QWidget>
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

View File

@ -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