1 /* === This file is part of Calamares - <https://calamares.io> ===
2  *
3  *   SPDX-FileCopyrightText: 2014-2017 Teo Mrnjavac <teo@kde.org>
4  *   SPDX-FileCopyrightText: 2017-2019 Adriaan de Groot <groot@kde.org>
5  *   SPDX-FileCopyrightText: 2019 Collabora Ltd
6  *   SPDX-License-Identifier: GPL-3.0-or-later
7  *
8  *   Calamares is Free Software: see the License-Identifier above.
9  *
10  */
11 
12 #include "ChoicePage.h"
13 
14 #include "Config.h"
15 
16 #include "core/BootLoaderModel.h"
17 #include "core/DeviceModel.h"
18 #include "core/KPMHelpers.h"
19 #include "core/OsproberEntry.h"
20 #include "core/PartUtils.h"
21 #include "core/PartitionActions.h"
22 #include "core/PartitionCoreModule.h"
23 #include "core/PartitionInfo.h"
24 #include "core/PartitionModel.h"
25 #include "gui/BootInfoWidget.h"
26 #include "gui/DeviceInfoWidget.h"
27 #include "gui/PartitionBarsView.h"
28 #include "gui/PartitionLabelsView.h"
29 #include "gui/PartitionSplitterWidget.h"
30 #include "gui/ReplaceWidget.h"
31 #include "gui/ScanningDialog.h"
32 
33 #include "Branding.h"
34 #include "GlobalStorage.h"
35 #include "JobQueue.h"
36 #include "partition/PartitionIterator.h"
37 #include "partition/PartitionQuery.h"
38 #include "utils/CalamaresUtilsGui.h"
39 #include "utils/Logger.h"
40 #include "utils/Retranslator.h"
41 #include "utils/Units.h"
42 #include "widgets/PrettyRadioButton.h"
43 
44 #include <kpmcore/core/device.h>
45 #include <kpmcore/core/partition.h>
46 #ifdef WITH_KPMCORE4API
47 #include <kpmcore/core/softwareraid.h>
48 #endif
49 
50 #include <QBoxLayout>
51 #include <QButtonGroup>
52 #include <QComboBox>
53 #include <QDir>
54 #include <QFutureWatcher>
55 #include <QLabel>
56 #include <QListView>
57 #include <QtConcurrent/QtConcurrent>
58 
59 using Calamares::PrettyRadioButton;
60 using CalamaresUtils::Partition::findPartitionByPath;
61 using CalamaresUtils::Partition::isPartitionFreeSpace;
62 using CalamaresUtils::Partition::PartitionIterator;
63 using InstallChoice = Config::InstallChoice;
64 using SwapChoice = Config::SwapChoice;
65 
66 /**
67  * @brief ChoicePage::ChoicePage is the default constructor. Called on startup as part of
68  *      the module loading code path.
69  * @param parent the QWidget parent.
70  */
ChoicePage(Config * config,QWidget * parent)71 ChoicePage::ChoicePage( Config* config, QWidget* parent )
72     : QWidget( parent )
73     , m_config( config )
74     , m_nextEnabled( false )
75     , m_core( nullptr )
76     , m_isEfi( false )
77     , m_grp( nullptr )
78     , m_alongsideButton( nullptr )
79     , m_eraseButton( nullptr )
80     , m_replaceButton( nullptr )
81     , m_somethingElseButton( nullptr )
82     , m_eraseSwapChoiceComboBox( nullptr )
83     , m_deviceInfoWidget( nullptr )
84     , m_beforePartitionBarsView( nullptr )
85     , m_beforePartitionLabelsView( nullptr )
86     , m_bootloaderComboBox( nullptr )
87     , m_enableEncryptionWidget( true )
88 {
89     setupUi( this );
90 
91     auto gs = Calamares::JobQueue::instance()->globalStorage();
92 
93     m_requiredPartitionTableType = gs->value( "requiredPartitionTableType" ).toStringList();
94     m_enableEncryptionWidget = gs->value( "enableLuksAutomatedPartitioning" ).toBool();
95 
96     // Set up drives combo
97     m_mainLayout->setDirection( QBoxLayout::TopToBottom );
98     m_drivesLayout->setDirection( QBoxLayout::LeftToRight );
99 
100     BootInfoWidget* bootInfoWidget = new BootInfoWidget( this );
101     m_drivesLayout->insertWidget( 0, bootInfoWidget );
102     m_drivesLayout->insertSpacing( 1, CalamaresUtils::defaultFontHeight() / 2 );
103 
104     m_drivesCombo = new QComboBox( this );
105     m_mainLayout->setStretchFactor( m_drivesLayout, 0 );
106     m_mainLayout->setStretchFactor( m_rightLayout, 1 );
107     m_drivesLabel->setBuddy( m_drivesCombo );
108 
109     m_drivesLayout->addWidget( m_drivesCombo );
110 
111     m_deviceInfoWidget = new DeviceInfoWidget;
112     m_drivesLayout->addWidget( m_deviceInfoWidget );
113     m_drivesLayout->addStretch();
114 
115     m_messageLabel->setWordWrap( true );
116     m_messageLabel->hide();
117 
118     CalamaresUtils::unmarginLayout( m_itemsLayout );
119 
120     // Drive selector + preview
121     CALAMARES_RETRANSLATE_SLOT( &ChoicePage::retranslate );
122 
123     m_previewBeforeFrame->setSizePolicy( QSizePolicy::Preferred, QSizePolicy::Expanding );
124     m_previewAfterFrame->setSizePolicy( QSizePolicy::Preferred, QSizePolicy::Expanding );
125     m_previewAfterLabel->hide();
126     m_previewAfterFrame->hide();
127     m_encryptWidget->hide();
128     m_reuseHomeCheckBox->hide();
129     gs->insert( "reuseHome", false );
130 }
131 
132 
~ChoicePage()133 ChoicePage::~ChoicePage() {}
134 
135 void
retranslate()136 ChoicePage::retranslate()
137 {
138     retranslateUi( this );
139     m_drivesLabel->setText( tr( "Select storage de&vice:" ) );
140     m_previewBeforeLabel->setText( tr( "Current:" ) );
141     m_previewAfterLabel->setText( tr( "After:" ) );
142 
143     updateSwapChoicesTr();
144     updateChoiceButtonsTr();
145 }
146 
147 
148 /** @brief Sets the @p model for the given @p box and adjusts UI sizes to match.
149  *
150  * The model provides data for drawing the items in the model; the
151  * drawing itself is done by the delegate, which may end up drawing a
152  * different width in the popup than in the collapsed combo box.
153  *
154  * Make the box wide enough to accomodate the whole expanded delegate;
155  * this avoids cases where the popup would truncate data being drawn
156  * because the overall box is sized too narrow.
157  */
158 void
setModelToComboBox(QComboBox * box,QAbstractItemModel * model)159 setModelToComboBox( QComboBox* box, QAbstractItemModel* model )
160 {
161     box->setModel( model );
162     if ( model->rowCount() > 0 )
163     {
164         QStyleOptionViewItem options;
165         options.initFrom( box );
166         auto delegateSize = box->itemDelegate()->sizeHint( options, model->index( 0, 0 ) );
167         box->setMinimumWidth( delegateSize.width() );
168     }
169 }
170 
171 void
init(PartitionCoreModule * core)172 ChoicePage::init( PartitionCoreModule* core )
173 {
174     m_core = core;
175     m_isEfi = PartUtils::isEfiSystem();
176 
177     setupChoices();
178 
179 
180     // We need to do this because a PCM revert invalidates the deviceModel.
181     connect( core, &PartitionCoreModule::reverted, this, [=] {
182         setModelToComboBox( m_drivesCombo, core->deviceModel() );
183         m_drivesCombo->setCurrentIndex( m_lastSelectedDeviceIndex );
184     } );
185     setModelToComboBox( m_drivesCombo, core->deviceModel() );
186 
187     connect( m_drivesCombo, qOverload< int >( &QComboBox::currentIndexChanged ), this, &ChoicePage::applyDeviceChoice );
188 
189     connect( m_encryptWidget, &EncryptWidget::stateChanged, this, &ChoicePage::onEncryptWidgetStateChanged );
190     connect( m_reuseHomeCheckBox, &QCheckBox::stateChanged, this, &ChoicePage::onHomeCheckBoxStateChanged );
191 
192     ChoicePage::applyDeviceChoice();
193 }
194 
195 
196 /** @brief Creates a combobox with the given choices in it.
197  *
198  * Pre-selects the choice given by @p dflt.
199  * No texts are set -- that happens later by the translator functions.
200  */
201 static inline QComboBox*
createCombo(const QSet<SwapChoice> & s,SwapChoice dflt)202 createCombo( const QSet< SwapChoice >& s, SwapChoice dflt )
203 {
204     QComboBox* box = new QComboBox;
205     for ( SwapChoice c : { SwapChoice::NoSwap,
206                            SwapChoice::SmallSwap,
207                            SwapChoice::FullSwap,
208                            SwapChoice::ReuseSwap,
209                            SwapChoice::SwapFile } )
210         if ( s.contains( c ) )
211         {
212             box->addItem( QString(), c );
213         }
214 
215     int dfltIndex = box->findData( dflt );
216     if ( dfltIndex >= 0 )
217     {
218         box->setCurrentIndex( dfltIndex );
219     }
220 
221     return box;
222 }
223 
224 /**
225  * @brief ChoicePage::setupChoices creates PrettyRadioButton objects for the action
226  *      choices.
227  * @warning This must only run ONCE because it creates signal-slot connections for the
228  *      actions. When an action is triggered, it runs action-specific code that may
229  *      change the internal state of the PCM, and it updates the bottom preview (or
230  *      split) widget.
231  *      Synchronous loading ends here.
232  */
233 void
setupChoices()234 ChoicePage::setupChoices()
235 {
236     // sample os-prober output:
237     // /dev/sda2:Windows 7 (loader):Windows:chain
238     // /dev/sda6::Arch:linux
239     //
240     // There are three possibilities we have to consider:
241     //  - There are no operating systems present
242     //  - There is one operating system present
243     //  - There are multiple operating systems present
244     //
245     // There are three outcomes we have to provide:
246     //  1) Wipe+autopartition
247     //  2) Resize+autopartition
248     //  3) Manual
249     //  TBD: upgrade option?
250 
251     QSize iconSize( CalamaresUtils::defaultIconSize().width() * 2, CalamaresUtils::defaultIconSize().height() * 2 );
252     m_grp = new QButtonGroup( this );
253 
254     m_alongsideButton = new PrettyRadioButton;
255     m_alongsideButton->setIconSize( iconSize );
256     m_alongsideButton->setIcon(
257         CalamaresUtils::defaultPixmap( CalamaresUtils::PartitionAlongside, CalamaresUtils::Original, iconSize ) );
258     m_alongsideButton->addToGroup( m_grp, InstallChoice::Alongside );
259 
260     m_eraseButton = new PrettyRadioButton;
261     m_eraseButton->setIconSize( iconSize );
262     m_eraseButton->setIcon(
263         CalamaresUtils::defaultPixmap( CalamaresUtils::PartitionEraseAuto, CalamaresUtils::Original, iconSize ) );
264     m_eraseButton->addToGroup( m_grp, InstallChoice::Erase );
265 
266     m_replaceButton = new PrettyRadioButton;
267 
268     m_replaceButton->setIconSize( iconSize );
269     m_replaceButton->setIcon(
270         CalamaresUtils::defaultPixmap( CalamaresUtils::PartitionReplaceOs, CalamaresUtils::Original, iconSize ) );
271     m_replaceButton->addToGroup( m_grp, InstallChoice::Replace );
272 
273     // Fill up swap options
274     if ( m_config->swapChoices().count() > 1 )
275     {
276         m_eraseSwapChoiceComboBox = createCombo( m_config->swapChoices(), m_config->swapChoice() );
277         m_eraseButton->addOptionsComboBox( m_eraseSwapChoiceComboBox );
278     }
279 
280     if ( m_config->eraseFsTypes().count() > 1 )
281     {
282         m_eraseFsTypesChoiceComboBox = new QComboBox;
283         m_eraseFsTypesChoiceComboBox->addItems( m_config->eraseFsTypes() );
284         connect(
285             m_eraseFsTypesChoiceComboBox, &QComboBox::currentTextChanged, m_config, &Config::setEraseFsTypeChoice );
286         connect( m_config, &Config::eraseModeFilesystemChanged, this, &ChoicePage::onActionChanged );
287         m_eraseButton->addOptionsComboBox( m_eraseFsTypesChoiceComboBox );
288     }
289 
290     m_itemsLayout->addWidget( m_alongsideButton );
291     m_itemsLayout->addWidget( m_replaceButton );
292     m_itemsLayout->addWidget( m_eraseButton );
293 
294     m_somethingElseButton = new PrettyRadioButton;
295     m_somethingElseButton->setIconSize( iconSize );
296     m_somethingElseButton->setIcon(
297         CalamaresUtils::defaultPixmap( CalamaresUtils::PartitionManual, CalamaresUtils::Original, iconSize ) );
298     m_itemsLayout->addWidget( m_somethingElseButton );
299     m_somethingElseButton->addToGroup( m_grp, InstallChoice::Manual );
300 
301     m_itemsLayout->addStretch();
302 
303 #if ( QT_VERSION < QT_VERSION_CHECK( 5, 15, 0 ) )
304     auto buttonSignal = QOverload< int, bool >::of( &QButtonGroup::buttonToggled );
305 #else
306     auto buttonSignal = &QButtonGroup::idToggled;
307 #endif
308     connect( m_grp, buttonSignal, this, [this]( int id, bool checked ) {
309         if ( checked )  // An action was picked.
310         {
311             m_config->setInstallChoice( id );
312             updateNextEnabled();
313 
314             Q_EMIT actionChosen();
315         }
316         else  // An action was unpicked, either on its own or because of another selection.
317         {
318             if ( m_grp->checkedButton() == nullptr )  // If no other action is chosen, we must
319             {
320                 // set m_choice to NoChoice and reset previews.
321                 m_config->setInstallChoice( InstallChoice::NoChoice );
322                 updateNextEnabled();
323 
324                 Q_EMIT actionChosen();
325             }
326         }
327     } );
328 
329     m_rightLayout->setStretchFactor( m_itemsLayout, 1 );
330     m_rightLayout->setStretchFactor( m_previewBeforeFrame, 0 );
331     m_rightLayout->setStretchFactor( m_previewAfterFrame, 0 );
332 
333     connect( this, &ChoicePage::actionChosen, this, &ChoicePage::onActionChanged );
334     if ( m_eraseSwapChoiceComboBox )
335     {
336         connect( m_eraseSwapChoiceComboBox,
337                  QOverload< int >::of( &QComboBox::currentIndexChanged ),
338                  this,
339                  &ChoicePage::onEraseSwapChoiceChanged );
340     }
341 
342     updateSwapChoicesTr();
343     updateChoiceButtonsTr();
344 }
345 
346 
347 /**
348  * @brief ChoicePage::selectedDevice queries the device picker (which may be a combo or
349  *      a list view) to get a pointer to the currently selected Device.
350  * @return a Device pointer, valid in the current state of the PCM, or nullptr if
351  *      something goes wrong.
352  */
353 Device*
selectedDevice()354 ChoicePage::selectedDevice()
355 {
356     Device* currentDevice = nullptr;
357     currentDevice
358         = m_core->deviceModel()->deviceForIndex( m_core->deviceModel()->index( m_drivesCombo->currentIndex() ) );
359 
360     return currentDevice;
361 }
362 
363 
364 void
hideButtons()365 ChoicePage::hideButtons()
366 {
367     m_eraseButton->hide();
368     m_replaceButton->hide();
369     m_alongsideButton->hide();
370     m_somethingElseButton->hide();
371 }
372 
373 void
checkInstallChoiceRadioButton(InstallChoice c)374 ChoicePage::checkInstallChoiceRadioButton( InstallChoice c )
375 {
376     QSignalBlocker b( m_grp );
377     m_grp->setExclusive( false );
378     // If c == InstallChoice::NoChoice none will match and all are deselected
379     m_eraseButton->setChecked( InstallChoice::Erase == c );
380     m_replaceButton->setChecked( InstallChoice::Replace == c );
381     m_alongsideButton->setChecked( InstallChoice::Alongside == c );
382     m_somethingElseButton->setChecked( InstallChoice::Manual == c );
383     m_grp->setExclusive( true );
384 }
385 
386 
387 /**
388  * @brief ChoicePage::applyDeviceChoice handler for the selected event of the device
389  *      picker. Calls ChoicePage::selectedDevice() to get the current Device*, then
390  *      updates the preview widget for the on-disk state (calls ChoicePage::
391  *      updateDeviceStatePreview()) and finally sets up the available actions and their
392  *      text by calling ChoicePage::setupActions().
393  */
394 void
applyDeviceChoice()395 ChoicePage::applyDeviceChoice()
396 {
397     if ( !selectedDevice() )
398     {
399         hideButtons();
400         return;
401     }
402 
403     if ( m_core->isDirty() )
404     {
405         ScanningDialog::run(
406             QtConcurrent::run( [=] {
407                 QMutexLocker locker( &m_coreMutex );
408                 m_core->revertAllDevices();
409             } ),
410             [this] { continueApplyDeviceChoice(); },
411             this );
412     }
413     else
414     {
415         continueApplyDeviceChoice();
416     }
417 }
418 
419 
420 void
continueApplyDeviceChoice()421 ChoicePage::continueApplyDeviceChoice()
422 {
423     Device* currd = selectedDevice();
424 
425     // The device should only be nullptr immediately after a PCM reset.
426     // applyDeviceChoice() will be called again momentarily as soon as we handle the
427     // PartitionCoreModule::reverted signal.
428     if ( !currd )
429     {
430         hideButtons();
431         return;
432     }
433 
434     updateDeviceStatePreview();
435 
436     // Preview setup done. Now we show/hide choices as needed.
437     setupActions();
438 
439     cDebug() << "Previous device" << m_lastSelectedDeviceIndex << "new device" << m_drivesCombo->currentIndex();
440     if ( m_lastSelectedDeviceIndex != m_drivesCombo->currentIndex() )
441     {
442         m_lastSelectedDeviceIndex = m_drivesCombo->currentIndex();
443         m_lastSelectedActionIndex = -1;
444         m_config->setInstallChoice( m_config->initialInstallChoice() );
445         checkInstallChoiceRadioButton( m_config->installChoice() );
446     }
447 
448     Q_EMIT actionChosen();
449     Q_EMIT deviceChosen();
450 }
451 
452 
453 void
onActionChanged()454 ChoicePage::onActionChanged()
455 {
456     Device* currd = selectedDevice();
457     if ( currd )
458     {
459         applyActionChoice( m_config->installChoice() );
460     }
461 }
462 
463 void
onEraseSwapChoiceChanged()464 ChoicePage::onEraseSwapChoiceChanged()
465 {
466     if ( m_eraseSwapChoiceComboBox )
467     {
468         m_config->setSwapChoice( m_eraseSwapChoiceComboBox->currentData().toInt() );
469         onActionChanged();
470     }
471 }
472 
473 void
applyActionChoice(InstallChoice choice)474 ChoicePage::applyActionChoice( InstallChoice choice )
475 {
476     cDebug() << "Prev" << m_lastSelectedActionIndex << "InstallChoice" << choice
477              << Config::installChoiceNames().find( choice );
478     m_beforePartitionBarsView->selectionModel()->disconnect( SIGNAL( currentRowChanged( QModelIndex, QModelIndex ) ) );
479     m_beforePartitionBarsView->selectionModel()->clearSelection();
480     m_beforePartitionBarsView->selectionModel()->clearCurrentIndex();
481 
482     switch ( choice )
483     {
484     case InstallChoice::Erase:
485     {
486         auto gs = Calamares::JobQueue::instance()->globalStorage();
487         PartitionActions::Choices::AutoPartitionOptions options { gs->value( "defaultPartitionTableType" ).toString(),
488                                                                   m_config->eraseFsType(),
489                                                                   m_encryptWidget->passphrase(),
490                                                                   gs->value( "efiSystemPartition" ).toString(),
491                                                                   CalamaresUtils::GiBtoBytes(
492                                                                       gs->value( "requiredStorageGiB" ).toDouble() ),
493                                                                   m_config->swapChoice() };
494 
495         if ( m_core->isDirty() )
496         {
497             ScanningDialog::run(
498                 QtConcurrent::run( [=] {
499                     QMutexLocker locker( &m_coreMutex );
500                     m_core->revertDevice( selectedDevice() );
501                 } ),
502                 [=] {
503                     PartitionActions::doAutopartition( m_core, selectedDevice(), options );
504                     Q_EMIT deviceChosen();
505                 },
506                 this );
507         }
508         else
509         {
510             PartitionActions::doAutopartition( m_core, selectedDevice(), options );
511             Q_EMIT deviceChosen();
512         }
513     }
514     break;
515     case InstallChoice::Replace:
516         if ( m_core->isDirty() )
517         {
518             ScanningDialog::run(
519                 QtConcurrent::run( [=] {
520                     QMutexLocker locker( &m_coreMutex );
521                     m_core->revertDevice( selectedDevice() );
522                 } ),
523                 [] {},
524                 this );
525         }
526         connect( m_beforePartitionBarsView->selectionModel(),
527                  SIGNAL( currentRowChanged( QModelIndex, QModelIndex ) ),
528                  this,
529                  SLOT( onPartitionToReplaceSelected( QModelIndex, QModelIndex ) ),
530                  Qt::UniqueConnection );
531         break;
532 
533     case InstallChoice::Alongside:
534         if ( m_core->isDirty() )
535         {
536             ScanningDialog::run(
537                 QtConcurrent::run( [=] {
538                     QMutexLocker locker( &m_coreMutex );
539                     m_core->revertDevice( selectedDevice() );
540                 } ),
541                 [this] {
542                     // We need to reupdate after reverting because the splitter widget is
543                     // not a true view.
544                     updateActionChoicePreview( m_config->installChoice() );
545                     updateNextEnabled();
546                 },
547                 this );
548         }
549 
550         connect( m_beforePartitionBarsView->selectionModel(),
551                  SIGNAL( currentRowChanged( QModelIndex, QModelIndex ) ),
552                  this,
553                  SLOT( doAlongsideSetupSplitter( QModelIndex, QModelIndex ) ),
554                  Qt::UniqueConnection );
555         break;
556     case InstallChoice::NoChoice:
557     case InstallChoice::Manual:
558         break;
559     }
560     updateNextEnabled();
561     updateActionChoicePreview( choice );
562 }
563 
564 
565 void
doAlongsideSetupSplitter(const QModelIndex & current,const QModelIndex & previous)566 ChoicePage::doAlongsideSetupSplitter( const QModelIndex& current, const QModelIndex& previous )
567 {
568     Q_UNUSED( previous )
569     if ( !current.isValid() )
570     {
571         return;
572     }
573 
574     if ( !m_afterPartitionSplitterWidget )
575     {
576         return;
577     }
578 
579     const PartitionModel* modl = qobject_cast< const PartitionModel* >( current.model() );
580     if ( !modl )
581     {
582         return;
583     }
584 
585     Partition* part = modl->partitionForIndex( current );
586     if ( !part )
587     {
588         cDebug() << "Partition not found for index" << current;
589         return;
590     }
591 
592     double requiredStorageGB
593         = Calamares::JobQueue::instance()->globalStorage()->value( "requiredStorageGiB" ).toDouble();
594 
595     qint64 requiredStorageB = CalamaresUtils::GiBtoBytes( requiredStorageGB + 0.1 + 2.0 );
596 
597     m_afterPartitionSplitterWidget->setSplitPartition( part->partitionPath(),
598                                                        qRound64( part->used() * 1.1 ),
599                                                        part->capacity() - requiredStorageB,
600                                                        part->capacity() / 2 );
601 
602     if ( m_isEfi )
603     {
604         setupEfiSystemPartitionSelector();
605     }
606 
607     cDebug() << "Partition selected for Alongside.";
608 
609     updateNextEnabled();
610 }
611 
612 
613 void
onEncryptWidgetStateChanged()614 ChoicePage::onEncryptWidgetStateChanged()
615 {
616     EncryptWidget::Encryption state = m_encryptWidget->state();
617     if ( m_config->installChoice() == InstallChoice::Erase )
618     {
619         if ( state == EncryptWidget::Encryption::Confirmed || state == EncryptWidget::Encryption::Disabled )
620         {
621             applyActionChoice( m_config->installChoice() );
622         }
623     }
624     else if ( m_config->installChoice() == InstallChoice::Replace )
625     {
626         if ( m_beforePartitionBarsView && m_beforePartitionBarsView->selectionModel()->currentIndex().isValid()
627              && ( state == EncryptWidget::Encryption::Confirmed || state == EncryptWidget::Encryption::Disabled ) )
628         {
629             doReplaceSelectedPartition( m_beforePartitionBarsView->selectionModel()->currentIndex() );
630         }
631     }
632     updateNextEnabled();
633 }
634 
635 
636 void
onHomeCheckBoxStateChanged()637 ChoicePage::onHomeCheckBoxStateChanged()
638 {
639     if ( m_config->installChoice() == InstallChoice::Replace
640          && m_beforePartitionBarsView->selectionModel()->currentIndex().isValid() )
641     {
642         doReplaceSelectedPartition( m_beforePartitionBarsView->selectionModel()->currentIndex() );
643     }
644 }
645 
646 
647 void
onLeave()648 ChoicePage::onLeave()
649 {
650     if ( m_config->installChoice() == InstallChoice::Alongside )
651     {
652         doAlongsideApply();
653     }
654 
655     if ( m_isEfi
656          && ( m_config->installChoice() == InstallChoice::Alongside
657               || m_config->installChoice() == InstallChoice::Replace ) )
658     {
659         QList< Partition* > efiSystemPartitions = m_core->efiSystemPartitions();
660         if ( efiSystemPartitions.count() == 1 )
661         {
662             PartitionInfo::setMountPoint(
663                 efiSystemPartitions.first(),
664                 Calamares::JobQueue::instance()->globalStorage()->value( "efiSystemPartition" ).toString() );
665         }
666         else if ( efiSystemPartitions.count() > 1 && m_efiComboBox )
667         {
668             PartitionInfo::setMountPoint(
669                 efiSystemPartitions.at( m_efiComboBox->currentIndex() ),
670                 Calamares::JobQueue::instance()->globalStorage()->value( "efiSystemPartition" ).toString() );
671         }
672         else
673         {
674             cError() << "cannot set up EFI system partition.\nESP count:" << efiSystemPartitions.count()
675                      << "\nm_efiComboBox:" << m_efiComboBox;
676         }
677     }
678     else  // installPath is then passed to the bootloader module for MBR setup
679     {
680         if ( !m_isEfi )
681         {
682             if ( m_bootloaderComboBox.isNull() )
683             {
684                 auto d_p = selectedDevice();
685                 if ( d_p )
686                 {
687                     m_core->setBootLoaderInstallPath( d_p->deviceNode() );
688                 }
689                 else
690                 {
691                     cWarning() << "No device selected for bootloader.";
692                 }
693             }
694             else
695             {
696                 QVariant var = m_bootloaderComboBox->currentData( BootLoaderModel::BootLoaderPathRole );
697                 if ( !var.isValid() )
698                 {
699                     return;
700                 }
701                 m_core->setBootLoaderInstallPath( var.toString() );
702             }
703         }
704     }
705 }
706 
707 
708 void
doAlongsideApply()709 ChoicePage::doAlongsideApply()
710 {
711     Q_ASSERT( m_afterPartitionSplitterWidget->splitPartitionSize() >= 0 );
712     Q_ASSERT( m_afterPartitionSplitterWidget->newPartitionSize() >= 0 );
713 
714     QMutexLocker locker( &m_coreMutex );
715 
716     QString path = m_beforePartitionBarsView->selectionModel()
717                        ->currentIndex()
718                        .data( PartitionModel::PartitionPathRole )
719                        .toString();
720 
721     DeviceModel* dm = m_core->deviceModel();
722     for ( int i = 0; i < dm->rowCount(); ++i )
723     {
724         Device* dev = dm->deviceForIndex( dm->index( i ) );
725         Partition* candidate = findPartitionByPath( { dev }, path );
726         if ( candidate )
727         {
728             qint64 firstSector = candidate->firstSector();
729             qint64 oldLastSector = candidate->lastSector();
730             qint64 newLastSector
731                 = firstSector + m_afterPartitionSplitterWidget->splitPartitionSize() / dev->logicalSize();
732 
733             m_core->resizePartition( dev, candidate, firstSector, newLastSector );
734             m_core->layoutApply( dev,
735                                  newLastSector + 2,
736                                  oldLastSector,
737                                  m_encryptWidget->passphrase(),
738                                  candidate->parent(),
739                                  candidate->roles() );
740             m_core->dumpQueue();
741 
742             break;
743         }
744     }
745 }
746 
747 
748 void
onPartitionToReplaceSelected(const QModelIndex & current,const QModelIndex & previous)749 ChoicePage::onPartitionToReplaceSelected( const QModelIndex& current, const QModelIndex& previous )
750 {
751     Q_UNUSED( previous )
752     if ( !current.isValid() )
753     {
754         return;
755     }
756 
757     // Reset state on selection regardless of whether this will be used.
758     m_reuseHomeCheckBox->setChecked( false );
759 
760     doReplaceSelectedPartition( current );
761 }
762 
763 
764 void
doReplaceSelectedPartition(const QModelIndex & current)765 ChoicePage::doReplaceSelectedPartition( const QModelIndex& current )
766 {
767     if ( !current.isValid() )
768     {
769         return;
770     }
771 
772     // This will be deleted by the second lambda, below.
773     QString* homePartitionPath = new QString();
774 
775     ScanningDialog::run(
776         QtConcurrent::run(
777             [this, current, homePartitionPath]( bool doReuseHomePartition ) {
778                 QMutexLocker locker( &m_coreMutex );
779 
780                 if ( m_core->isDirty() )
781                 {
782                     m_core->revertDevice( selectedDevice() );
783                 }
784 
785                 // if the partition is unallocated(free space), we don't replace it but create new one
786                 // with the same first and last sector
787                 Partition* selectedPartition
788                     = static_cast< Partition* >( current.data( PartitionModel::PartitionPtrRole ).value< void* >() );
789                 if ( isPartitionFreeSpace( selectedPartition ) )
790                 {
791                     //NOTE: if the selected partition is free space, we don't deal with
792                     //      a separate /home partition at all because there's no existing
793                     //      rootfs to read it from.
794                     PartitionRole newRoles = PartitionRole( PartitionRole::Primary );
795                     PartitionNode* newParent = selectedDevice()->partitionTable();
796 
797                     if ( selectedPartition->parent() )
798                     {
799                         Partition* parent = dynamic_cast< Partition* >( selectedPartition->parent() );
800                         if ( parent && parent->roles().has( PartitionRole::Extended ) )
801                         {
802                             newRoles = PartitionRole( PartitionRole::Logical );
803                             newParent = findPartitionByPath( { selectedDevice() }, parent->partitionPath() );
804                         }
805                     }
806 
807                     m_core->layoutApply( selectedDevice(),
808                                          selectedPartition->firstSector(),
809                                          selectedPartition->lastSector(),
810                                          m_encryptWidget->passphrase(),
811                                          newParent,
812                                          newRoles );
813                 }
814                 else
815                 {
816                     // We can't use the PartitionPtrRole because we need to make changes to the
817                     // main DeviceModel, not the immutable copy.
818                     QString partPath = current.data( PartitionModel::PartitionPathRole ).toString();
819                     selectedPartition = findPartitionByPath( { selectedDevice() }, partPath );
820                     if ( selectedPartition )
821                     {
822                         // Find out is the selected partition has a rootfs. If yes, then make the
823                         // m_reuseHomeCheckBox visible and set its text to something meaningful.
824                         homePartitionPath->clear();
825                         for ( const OsproberEntry& osproberEntry : m_core->osproberEntries() )
826                             if ( osproberEntry.path == partPath )
827                             {
828                                 *homePartitionPath = osproberEntry.homePath;
829                             }
830                         if ( homePartitionPath->isEmpty() )
831                         {
832                             doReuseHomePartition = false;
833                         }
834 
835                         Calamares::GlobalStorage* gs = Calamares::JobQueue::instance()->globalStorage();
836 
837                         PartitionActions::doReplacePartition( m_core,
838                                                               selectedDevice(),
839                                                               selectedPartition,
840                                                               { gs->value( "defaultPartitionType" ).toString(),
841                                                                 gs->value( "defaultFileSystemType" ).toString(),
842                                                                 m_encryptWidget->passphrase() } );
843                         Partition* homePartition = findPartitionByPath( { selectedDevice() }, *homePartitionPath );
844 
845                         if ( homePartition && doReuseHomePartition )
846                         {
847                             PartitionInfo::setMountPoint( homePartition, "/home" );
848                             gs->insert( "reuseHome", true );
849                         }
850                         else
851                         {
852                             gs->insert( "reuseHome", false );
853                         }
854                     }
855                 }
856             },
857             m_reuseHomeCheckBox->isChecked() ),
858         [this, homePartitionPath] {
859             m_reuseHomeCheckBox->setVisible( !homePartitionPath->isEmpty() );
860             if ( !homePartitionPath->isEmpty() )
861                 m_reuseHomeCheckBox->setText( tr( "Reuse %1 as home partition for %2." )
862                                                   .arg( *homePartitionPath )
863                                                   .arg( Calamares::Branding::instance()->shortProductName() ) );
864             delete homePartitionPath;
865 
866             if ( m_isEfi )
867                 setupEfiSystemPartitionSelector();
868 
869             updateNextEnabled();
870             if ( !m_bootloaderComboBox.isNull() && m_bootloaderComboBox->currentIndex() < 0 )
871                 m_bootloaderComboBox->setCurrentIndex( m_lastSelectedDeviceIndex );
872         },
873         this );
874 }
875 
876 
877 /**
878  * @brief clear and then rebuild the contents of the preview widget
879  *
880  * The preview widget for the current disk is completely re-constructed
881  * based on the on-disk state. This also triggers a rescan in the
882  * PCM to get a Device* copy that's unaffected by subsequent PCM changes.
883  */
884 void
updateDeviceStatePreview()885 ChoicePage::updateDeviceStatePreview()
886 {
887     //FIXME: this needs to be made async because the rescan can block the UI thread for
888     //       a while. --Teo 10/2015
889     Device* currentDevice = selectedDevice();
890     Q_ASSERT( currentDevice );
891     QMutexLocker locker( &m_previewsMutex );
892 
893     cDebug() << "Updating partitioning state widgets.";
894     qDeleteAll( m_previewBeforeFrame->children() );
895 
896     auto layout = m_previewBeforeFrame->layout();
897     if ( layout )
898     {
899         layout->deleteLater();  // Doesn't like nullptr
900     }
901 
902     layout = new QVBoxLayout;
903     m_previewBeforeFrame->setLayout( layout );
904     CalamaresUtils::unmarginLayout( layout );
905     layout->setSpacing( 6 );
906 
907     PartitionBarsView::NestedPartitionsMode mode
908         = Calamares::JobQueue::instance()->globalStorage()->value( "drawNestedPartitions" ).toBool()
909         ? PartitionBarsView::DrawNestedPartitions
910         : PartitionBarsView::NoNestedPartitions;
911     m_beforePartitionBarsView = new PartitionBarsView( m_previewBeforeFrame );
912     m_beforePartitionBarsView->setNestedPartitionsMode( mode );
913     m_beforePartitionLabelsView = new PartitionLabelsView( m_previewBeforeFrame );
914     m_beforePartitionLabelsView->setExtendedPartitionHidden( mode == PartitionBarsView::NoNestedPartitions );
915 
916     Device* deviceBefore = m_core->immutableDeviceCopy( currentDevice );
917 
918     PartitionModel* model = new PartitionModel( m_beforePartitionBarsView );
919     model->init( deviceBefore, m_core->osproberEntries() );
920 
921     m_beforePartitionBarsView->setModel( model );
922     m_beforePartitionLabelsView->setModel( model );
923 
924     // Make the bars and labels view use the same selectionModel.
925     auto sm = m_beforePartitionLabelsView->selectionModel();
926     m_beforePartitionLabelsView->setSelectionModel( m_beforePartitionBarsView->selectionModel() );
927     if ( sm )
928     {
929         sm->deleteLater();
930     }
931 
932     switch ( m_config->installChoice() )
933     {
934     case InstallChoice::Replace:
935     case InstallChoice::Alongside:
936         m_beforePartitionBarsView->setSelectionMode( QAbstractItemView::SingleSelection );
937         m_beforePartitionLabelsView->setSelectionMode( QAbstractItemView::SingleSelection );
938         break;
939     case InstallChoice::NoChoice:
940     case InstallChoice::Erase:
941     case InstallChoice::Manual:
942         m_beforePartitionBarsView->setSelectionMode( QAbstractItemView::NoSelection );
943         m_beforePartitionLabelsView->setSelectionMode( QAbstractItemView::NoSelection );
944     }
945 
946     layout->addWidget( m_beforePartitionBarsView );
947     layout->addWidget( m_beforePartitionLabelsView );
948 }
949 
950 
951 /**
952  * @brief rebuild the contents of the preview for the PCM-proposed state.
953  *
954  * No rescans here, this should be immediate.
955  *
956  * @param choice the chosen partitioning action.
957  */
958 void
updateActionChoicePreview(InstallChoice choice)959 ChoicePage::updateActionChoicePreview( InstallChoice choice )
960 {
961     Device* currentDevice = selectedDevice();
962     Q_ASSERT( currentDevice );
963 
964     QMutexLocker locker( &m_previewsMutex );
965 
966     cDebug() << "Updating partitioning preview widgets.";
967     qDeleteAll( m_previewAfterFrame->children() );
968 
969     auto oldlayout = m_previewAfterFrame->layout();
970     if ( oldlayout )
971     {
972         oldlayout->deleteLater();
973     }
974 
975     QVBoxLayout* layout = new QVBoxLayout;
976     m_previewAfterFrame->setLayout( layout );
977     CalamaresUtils::unmarginLayout( layout );
978     layout->setSpacing( 6 );
979 
980     PartitionBarsView::NestedPartitionsMode mode
981         = Calamares::JobQueue::instance()->globalStorage()->value( "drawNestedPartitions" ).toBool()
982         ? PartitionBarsView::DrawNestedPartitions
983         : PartitionBarsView::NoNestedPartitions;
984 
985     m_reuseHomeCheckBox->hide();
986     Calamares::JobQueue::instance()->globalStorage()->insert( "reuseHome", false );
987 
988     switch ( choice )
989     {
990     case InstallChoice::Alongside:
991     {
992         if ( m_enableEncryptionWidget )
993         {
994             m_encryptWidget->show();
995         }
996         m_previewBeforeLabel->setText( tr( "Current:" ) );
997         m_selectLabel->setText( tr( "<strong>Select a partition to shrink, "
998                                     "then drag the bottom bar to resize</strong>" ) );
999         m_selectLabel->show();
1000 
1001         m_afterPartitionSplitterWidget = new PartitionSplitterWidget( m_previewAfterFrame );
1002         m_afterPartitionSplitterWidget->init( selectedDevice(), mode == PartitionBarsView::DrawNestedPartitions );
1003         layout->addWidget( m_afterPartitionSplitterWidget );
1004 
1005         QLabel* sizeLabel = new QLabel( m_previewAfterFrame );
1006         layout->addWidget( sizeLabel );
1007         sizeLabel->setWordWrap( true );
1008         connect( m_afterPartitionSplitterWidget,
1009                  &PartitionSplitterWidget::partitionResized,
1010                  this,
1011                  [this, sizeLabel]( const QString& path, qint64 size, qint64 sizeNext ) {
1012                      Q_UNUSED( path )
1013                      sizeLabel->setText(
1014                          tr( "%1 will be shrunk to %2MiB and a new "
1015                              "%3MiB partition will be created for %4." )
1016                              .arg( m_beforePartitionBarsView->selectionModel()->currentIndex().data().toString() )
1017                              .arg( CalamaresUtils::BytesToMiB( size ) )
1018                              .arg( CalamaresUtils::BytesToMiB( sizeNext ) )
1019                              .arg( Calamares::Branding::instance()->shortProductName() ) );
1020                  } );
1021 
1022         m_previewAfterFrame->show();
1023         m_previewAfterLabel->show();
1024 
1025         SelectionFilter filter = []( const QModelIndex& index ) {
1026             return PartUtils::canBeResized(
1027                 static_cast< Partition* >( index.data( PartitionModel::PartitionPtrRole ).value< void* >() ),
1028                 Logger::Once() );
1029         };
1030         m_beforePartitionBarsView->setSelectionFilter( filter );
1031         m_beforePartitionLabelsView->setSelectionFilter( filter );
1032 
1033         break;
1034     }
1035     case InstallChoice::Erase:
1036     case InstallChoice::Replace:
1037     {
1038         if ( m_enableEncryptionWidget )
1039         {
1040             m_encryptWidget->show();
1041         }
1042         m_previewBeforeLabel->setText( tr( "Current:" ) );
1043         m_afterPartitionBarsView = new PartitionBarsView( m_previewAfterFrame );
1044         m_afterPartitionBarsView->setNestedPartitionsMode( mode );
1045         m_afterPartitionLabelsView = new PartitionLabelsView( m_previewAfterFrame );
1046         m_afterPartitionLabelsView->setExtendedPartitionHidden( mode == PartitionBarsView::NoNestedPartitions );
1047         m_afterPartitionLabelsView->setCustomNewRootLabel(
1048             Calamares::Branding::instance()->string( Calamares::Branding::BootloaderEntryName ) );
1049 
1050         PartitionModel* model = m_core->partitionModelForDevice( selectedDevice() );
1051 
1052         // The QObject parents tree is meaningful for memory management here,
1053         // see qDeleteAll above.
1054         m_afterPartitionBarsView->setModel( model );
1055         m_afterPartitionLabelsView->setModel( model );
1056         m_afterPartitionBarsView->setSelectionMode( QAbstractItemView::NoSelection );
1057         m_afterPartitionLabelsView->setSelectionMode( QAbstractItemView::NoSelection );
1058 
1059         layout->addWidget( m_afterPartitionBarsView );
1060         layout->addWidget( m_afterPartitionLabelsView );
1061 
1062         if ( !m_isEfi )
1063         {
1064             QWidget* eraseWidget = new QWidget;
1065 
1066             QHBoxLayout* eraseLayout = new QHBoxLayout;
1067             eraseWidget->setLayout( eraseLayout );
1068             eraseLayout->setContentsMargins( 0, 0, 0, 0 );
1069             QLabel* eraseBootloaderLabel = new QLabel( eraseWidget );
1070             eraseLayout->addWidget( eraseBootloaderLabel );
1071             eraseBootloaderLabel->setText( tr( "Boot loader location:" ) );
1072 
1073             m_bootloaderComboBox = createBootloaderComboBox( eraseWidget );
1074             connect( m_core->bootLoaderModel(), &QAbstractItemModel::modelReset, [this]() {
1075                 if ( !m_bootloaderComboBox.isNull() )
1076                 {
1077                     Calamares::restoreSelectedBootLoader( *m_bootloaderComboBox, m_core->bootLoaderInstallPath() );
1078                 }
1079             } );
1080             connect(
1081                 m_core,
1082                 &PartitionCoreModule::deviceReverted,
1083                 this,
1084                 [this]( Device* dev ) {
1085                     Q_UNUSED( dev )
1086                     if ( !m_bootloaderComboBox.isNull() )
1087                     {
1088                         if ( m_bootloaderComboBox->model() != m_core->bootLoaderModel() )
1089                         {
1090                             m_bootloaderComboBox->setModel( m_core->bootLoaderModel() );
1091                         }
1092 
1093                         m_bootloaderComboBox->setCurrentIndex( m_lastSelectedDeviceIndex );
1094                     }
1095                 },
1096                 Qt::QueuedConnection );
1097             // ^ Must be Queued so it's sure to run when the widget is already visible.
1098 
1099             eraseLayout->addWidget( m_bootloaderComboBox );
1100             eraseBootloaderLabel->setBuddy( m_bootloaderComboBox );
1101             eraseLayout->addStretch();
1102 
1103             layout->addWidget( eraseWidget );
1104         }
1105 
1106         m_previewAfterFrame->show();
1107         m_previewAfterLabel->show();
1108 
1109         if ( m_config->installChoice() == InstallChoice::Erase )
1110         {
1111             m_selectLabel->hide();
1112         }
1113         else
1114         {
1115             SelectionFilter filter = []( const QModelIndex& index ) {
1116                 return PartUtils::canBeReplaced(
1117                     static_cast< Partition* >( index.data( PartitionModel::PartitionPtrRole ).value< void* >() ),
1118                     Logger::Once() );
1119             };
1120             m_beforePartitionBarsView->setSelectionFilter( filter );
1121             m_beforePartitionLabelsView->setSelectionFilter( filter );
1122 
1123             m_selectLabel->show();
1124             m_selectLabel->setText( tr( "<strong>Select a partition to install on</strong>" ) );
1125         }
1126 
1127         break;
1128     }
1129     case InstallChoice::NoChoice:
1130     case InstallChoice::Manual:
1131         m_selectLabel->hide();
1132         m_previewAfterFrame->hide();
1133         m_previewBeforeLabel->setText( tr( "Current:" ) );
1134         m_previewAfterLabel->hide();
1135         m_encryptWidget->hide();
1136         break;
1137     }
1138 
1139     if ( m_isEfi
1140          && ( m_config->installChoice() == InstallChoice::Alongside
1141               || m_config->installChoice() == InstallChoice::Replace ) )
1142     {
1143         QHBoxLayout* efiLayout = new QHBoxLayout;
1144         layout->addLayout( efiLayout );
1145         m_efiLabel = new QLabel( m_previewAfterFrame );
1146         efiLayout->addWidget( m_efiLabel );
1147         m_efiComboBox = new QComboBox( m_previewAfterFrame );
1148         efiLayout->addWidget( m_efiComboBox );
1149         m_efiLabel->setBuddy( m_efiComboBox );
1150         m_efiComboBox->hide();
1151         efiLayout->addStretch();
1152     }
1153 
1154     // Also handle selection behavior on beforeFrame.
1155     QAbstractItemView::SelectionMode previewSelectionMode = QAbstractItemView::NoSelection;
1156     switch ( m_config->installChoice() )
1157     {
1158     case InstallChoice::Replace:
1159     case InstallChoice::Alongside:
1160         previewSelectionMode = QAbstractItemView::SingleSelection;
1161         break;
1162     case InstallChoice::NoChoice:
1163     case InstallChoice::Erase:
1164     case InstallChoice::Manual:
1165         previewSelectionMode = QAbstractItemView::NoSelection;
1166     }
1167 
1168     m_beforePartitionBarsView->setSelectionMode( previewSelectionMode );
1169     m_beforePartitionLabelsView->setSelectionMode( previewSelectionMode );
1170 }
1171 
1172 
1173 void
setupEfiSystemPartitionSelector()1174 ChoicePage::setupEfiSystemPartitionSelector()
1175 {
1176     Q_ASSERT( m_isEfi );
1177 
1178     // Only the already existing ones:
1179     QList< Partition* > efiSystemPartitions = m_core->efiSystemPartitions();
1180 
1181     if ( efiSystemPartitions.count() == 0 )  //should never happen
1182     {
1183         m_efiLabel->setText( tr( "An EFI system partition cannot be found anywhere "
1184                                  "on this system. Please go back and use manual "
1185                                  "partitioning to set up %1." )
1186                                  .arg( Calamares::Branding::instance()->shortProductName() ) );
1187         updateNextEnabled();
1188     }
1189     else if ( efiSystemPartitions.count() == 1 )  //probably most usual situation
1190     {
1191         m_efiLabel->setText( tr( "The EFI system partition at %1 will be used for "
1192                                  "starting %2." )
1193                                  .arg( efiSystemPartitions.first()->partitionPath() )
1194                                  .arg( Calamares::Branding::instance()->shortProductName() ) );
1195     }
1196     else
1197     {
1198         m_efiComboBox->show();
1199         m_efiLabel->setText( tr( "EFI system partition:" ) );
1200         for ( int i = 0; i < efiSystemPartitions.count(); ++i )
1201         {
1202             Partition* efiPartition = efiSystemPartitions.at( i );
1203             m_efiComboBox->addItem( efiPartition->partitionPath(), i );
1204 
1205             // We pick an ESP on the currently selected device, if possible
1206             if ( efiPartition->devicePath() == selectedDevice()->deviceNode() && efiPartition->number() == 1 )
1207             {
1208                 m_efiComboBox->setCurrentIndex( i );
1209             }
1210         }
1211     }
1212 }
1213 
1214 
1215 QComboBox*
createBootloaderComboBox(QWidget * parent)1216 ChoicePage::createBootloaderComboBox( QWidget* parent )
1217 {
1218     QComboBox* comboForBootloader = new QComboBox( parent );
1219     comboForBootloader->setModel( m_core->bootLoaderModel() );
1220 
1221     // When the chosen bootloader device changes, we update the choice in the PCM
1222     connect( comboForBootloader, QOverload< int >::of( &QComboBox::currentIndexChanged ), this, [this]( int newIndex ) {
1223         QComboBox* bootloaderCombo = qobject_cast< QComboBox* >( sender() );
1224         if ( bootloaderCombo )
1225         {
1226             QVariant var = bootloaderCombo->itemData( newIndex, BootLoaderModel::BootLoaderPathRole );
1227             if ( !var.isValid() )
1228             {
1229                 return;
1230             }
1231             m_core->setBootLoaderInstallPath( var.toString() );
1232         }
1233     } );
1234 
1235     return comboForBootloader;
1236 }
1237 
1238 
1239 static inline void
force_uncheck(QButtonGroup * grp,PrettyRadioButton * button)1240 force_uncheck( QButtonGroup* grp, PrettyRadioButton* button )
1241 {
1242     button->hide();
1243     grp->setExclusive( false );
1244     button->setChecked( false );
1245     grp->setExclusive( true );
1246 }
1247 
1248 static inline QDebug&
operator <<(QDebug & s,PartitionIterator & it)1249 operator<<( QDebug& s, PartitionIterator& it )
1250 {
1251     s << ( ( *it ) ? ( *it )->deviceNode() : QString( "<null device>" ) );
1252     return s;
1253 }
1254 
1255 /**
1256  * @brief ChoicePage::setupActions happens every time a new Device* is selected in the
1257  *      device picker. Sets up the text and visibility of the partitioning actions based
1258  *      on the currently selected Device*, bootloader and os-prober output.
1259  */
1260 void
setupActions()1261 ChoicePage::setupActions()
1262 {
1263     Logger::Once o;
1264 
1265     Device* currentDevice = selectedDevice();
1266     OsproberEntryList osproberEntriesForCurrentDevice = getOsproberEntriesForDevice( currentDevice );
1267 
1268     cDebug() << o << "Setting up actions for" << currentDevice->deviceNode() << "with"
1269              << osproberEntriesForCurrentDevice.count() << "entries.";
1270 
1271     if ( currentDevice->partitionTable() )
1272     {
1273         m_deviceInfoWidget->setPartitionTableType( currentDevice->partitionTable()->type() );
1274     }
1275     else
1276     {
1277         m_deviceInfoWidget->setPartitionTableType( PartitionTable::unknownTableType );
1278     }
1279 
1280     if ( m_config->allowManualPartitioning() )
1281     {
1282         m_somethingElseButton->show();
1283     }
1284     else
1285     {
1286         force_uncheck( m_grp, m_somethingElseButton );
1287     }
1288 
1289     bool atLeastOneCanBeResized = false;
1290     bool atLeastOneCanBeReplaced = false;
1291     bool atLeastOneIsMounted = false;  // Suppress 'erase' if so
1292     bool isInactiveRAID = false;
1293     bool matchTableType = false;
1294 
1295 #ifdef WITH_KPMCORE4API
1296     if ( currentDevice->type() == Device::Type::SoftwareRAID_Device
1297          && static_cast< SoftwareRAID* >( currentDevice )->status() == SoftwareRAID::Status::Inactive )
1298     {
1299         cDebug() << Logger::SubEntry << "part of an inactive RAID device";
1300         isInactiveRAID = true;
1301     }
1302 #endif
1303 
1304     PartitionTable::TableType tableType = PartitionTable::unknownTableType;
1305     if ( currentDevice->partitionTable() )
1306     {
1307         tableType = currentDevice->partitionTable()->type();
1308         matchTableType = m_requiredPartitionTableType.size() == 0
1309             || m_requiredPartitionTableType.contains( PartitionTable::tableTypeToName( tableType ) );
1310     }
1311 
1312     for ( auto it = PartitionIterator::begin( currentDevice ); it != PartitionIterator::end( currentDevice ); ++it )
1313     {
1314         if ( PartUtils::canBeResized( *it, o ) )
1315         {
1316             cDebug() << Logger::SubEntry << "contains resizable" << it;
1317             atLeastOneCanBeResized = true;
1318         }
1319         if ( PartUtils::canBeReplaced( *it, o ) )
1320         {
1321             cDebug() << Logger::SubEntry << "contains replaceable" << it;
1322             atLeastOneCanBeReplaced = true;
1323         }
1324         if ( ( *it )->isMounted() )
1325         {
1326             atLeastOneIsMounted = true;
1327         }
1328     }
1329 
1330     if ( osproberEntriesForCurrentDevice.count() == 0 )
1331     {
1332         CALAMARES_RETRANSLATE(
1333             cDebug() << "Setting texts for 0 osprober entries";
1334             m_messageLabel->setText( tr( "This storage device does not seem to have an operating system on it. "
1335                                          "What would you like to do?<br/>"
1336                                          "You will be able to review and confirm your choices "
1337                                          "before any change is made to the storage device." ) );
1338 
1339             m_eraseButton->setText( tr( "<strong>Erase disk</strong><br/>"
1340                                         "This will <font color=\"red\">delete</font> all data "
1341                                         "currently present on the selected storage device." ) );
1342 
1343             m_alongsideButton->setText( tr( "<strong>Install alongside</strong><br/>"
1344                                             "The installer will shrink a partition to make room for %1." )
1345                                             .arg( Calamares::Branding::instance()->shortVersionedName() ) );
1346 
1347             m_replaceButton->setText( tr( "<strong>Replace a partition</strong><br/>"
1348                                           "Replaces a partition with %1." )
1349                                           .arg( Calamares::Branding::instance()->shortVersionedName() ) ); );
1350 
1351         m_replaceButton->hide();
1352         m_alongsideButton->hide();
1353         m_grp->setExclusive( false );
1354         m_replaceButton->setChecked( false );
1355         m_alongsideButton->setChecked( false );
1356         m_grp->setExclusive( true );
1357     }
1358     else if ( osproberEntriesForCurrentDevice.count() == 1 )
1359     {
1360         QString osName = osproberEntriesForCurrentDevice.first().prettyName;
1361 
1362         if ( !osName.isEmpty() )
1363         {
1364             CALAMARES_RETRANSLATE(
1365                 cDebug() << "Setting texts for 1 non-empty osprober entry";
1366                 m_messageLabel->setText( tr( "This storage device has %1 on it. "
1367                                              "What would you like to do?<br/>"
1368                                              "You will be able to review and confirm your choices "
1369                                              "before any change is made to the storage device." )
1370                                              .arg( osName ) );
1371 
1372                 m_alongsideButton->setText( tr( "<strong>Install alongside</strong><br/>"
1373                                                 "The installer will shrink a partition to make room for %1." )
1374                                                 .arg( Calamares::Branding::instance()->shortVersionedName() ) );
1375 
1376                 m_eraseButton->setText( tr( "<strong>Erase disk</strong><br/>"
1377                                             "This will <font color=\"red\">delete</font> all data "
1378                                             "currently present on the selected storage device." ) );
1379 
1380 
1381                 m_replaceButton->setText( tr( "<strong>Replace a partition</strong><br/>"
1382                                               "Replaces a partition with %1." )
1383                                               .arg( Calamares::Branding::instance()->shortVersionedName() ) ); );
1384         }
1385         else
1386         {
1387             CALAMARES_RETRANSLATE(
1388                 cDebug() << "Setting texts for 1 empty osprober entry";
1389                 m_messageLabel->setText( tr( "This storage device already has an operating system on it. "
1390                                              "What would you like to do?<br/>"
1391                                              "You will be able to review and confirm your choices "
1392                                              "before any change is made to the storage device." ) );
1393 
1394                 m_alongsideButton->setText( tr( "<strong>Install alongside</strong><br/>"
1395                                                 "The installer will shrink a partition to make room for %1." )
1396                                                 .arg( Calamares::Branding::instance()->shortVersionedName() ) );
1397 
1398                 m_eraseButton->setText( tr( "<strong>Erase disk</strong><br/>"
1399                                             "This will <font color=\"red\">delete</font> all data "
1400                                             "currently present on the selected storage device." ) );
1401 
1402                 m_replaceButton->setText( tr( "<strong>Replace a partition</strong><br/>"
1403                                               "Replaces a partition with %1." )
1404                                               .arg( Calamares::Branding::instance()->shortVersionedName() ) ); );
1405         }
1406     }
1407     else
1408     {
1409         // osproberEntriesForCurrentDevice has at least 2 items.
1410 
1411         CALAMARES_RETRANSLATE(
1412             cDebug() << "Setting texts for >= 2 osprober entries";
1413 
1414             m_messageLabel->setText( tr( "This storage device has multiple operating systems on it. "
1415                                          "What would you like to do?<br/>"
1416                                          "You will be able to review and confirm your choices "
1417                                          "before any change is made to the storage device." ) );
1418 
1419             m_alongsideButton->setText( tr( "<strong>Install alongside</strong><br/>"
1420                                             "The installer will shrink a partition to make room for %1." )
1421                                             .arg( Calamares::Branding::instance()->shortVersionedName() ) );
1422 
1423             m_eraseButton->setText( tr( "<strong>Erase disk</strong><br/>"
1424                                         "This will <font color=\"red\">delete</font> all data "
1425                                         "currently present on the selected storage device." ) );
1426 
1427             m_replaceButton->setText( tr( "<strong>Replace a partition</strong><br/>"
1428                                           "Replaces a partition with %1." )
1429                                           .arg( Calamares::Branding::instance()->shortVersionedName() ) ); );
1430     }
1431 
1432 #ifdef DEBUG_PARTITION_UNSAFE
1433 #ifdef DEBUG_PARTITION_LAME
1434     // If things can't be broken, allow all the buttons
1435     atLeastOneCanBeReplaced = true;
1436     atLeastOneCanBeResized = true;
1437     atLeastOneIsMounted = false;
1438     isInactiveRAID = false;
1439 #endif
1440 #endif
1441 
1442     if ( atLeastOneCanBeReplaced )
1443     {
1444         m_replaceButton->show();
1445     }
1446     else
1447     {
1448         cDebug() << "No partitions available for replace-action.";
1449         force_uncheck( m_grp, m_replaceButton );
1450     }
1451 
1452     if ( atLeastOneCanBeResized )
1453     {
1454         m_alongsideButton->show();
1455     }
1456     else
1457     {
1458         cDebug() << "No partitions available for resize-action.";
1459         force_uncheck( m_grp, m_alongsideButton );
1460     }
1461 
1462     if ( !atLeastOneIsMounted && !isInactiveRAID )
1463     {
1464         m_eraseButton->show();  // None mounted
1465     }
1466     else
1467     {
1468         cDebug() << "No partitions ("
1469                  << "any-mounted?" << atLeastOneIsMounted << "is-raid?" << isInactiveRAID << ") for erase-action.";
1470         force_uncheck( m_grp, m_eraseButton );
1471     }
1472 
1473     bool isEfi = PartUtils::isEfiSystem();
1474     bool efiSystemPartitionFound = !m_core->efiSystemPartitions().isEmpty();
1475 
1476     if ( isEfi && !efiSystemPartitionFound )
1477     {
1478         cWarning() << "System is EFI but there's no EFI system partition, "
1479                       "DISABLING alongside and replace features.";
1480         m_alongsideButton->hide();
1481         m_replaceButton->hide();
1482     }
1483 
1484     if ( tableType != PartitionTable::unknownTableType && !matchTableType )
1485     {
1486         m_messageLabel->setText( tr( "This storage device already has an operating system on it, "
1487                                      "but the partition table <strong>%1</strong> is different from the "
1488                                      "needed <strong>%2</strong>.<br/>" )
1489                                      .arg( PartitionTable::tableTypeToName( tableType ) )
1490                                      .arg( m_requiredPartitionTableType.join( " or " ) ) );
1491         m_messageLabel->show();
1492 
1493         cWarning() << "Partition table" << PartitionTable::tableTypeToName( tableType )
1494                    << "does not match the requirement " << m_requiredPartitionTableType.join( " or " )
1495                    << ", ENABLING erase feature and DISABLING alongside, replace and manual features.";
1496         m_eraseButton->show();
1497         m_alongsideButton->hide();
1498         m_replaceButton->hide();
1499         m_somethingElseButton->hide();
1500         cDebug() << "Replace button suppressed because partition table type mismatch.";
1501         force_uncheck( m_grp, m_replaceButton );
1502     }
1503 
1504     if ( m_somethingElseButton->isHidden() && m_alongsideButton->isHidden() && m_replaceButton->isHidden()
1505          && m_eraseButton->isHidden() )
1506     {
1507         if ( atLeastOneIsMounted )
1508         {
1509             m_messageLabel->setText( tr( "This storage device has one of its partitions <strong>mounted</strong>." ) );
1510         }
1511         else
1512         {
1513             m_messageLabel->setText(
1514                 tr( "This storage device is a part of an <strong>inactive RAID</strong> device." ) );
1515         }
1516 
1517         m_messageLabel->show();
1518         cWarning() << "No buttons available"
1519                    << "replaced?" << atLeastOneCanBeReplaced << "resized?" << atLeastOneCanBeResized
1520                    << "erased? (not-mounted and not-raid)" << !atLeastOneIsMounted << "and" << !isInactiveRAID;
1521     }
1522 }
1523 
1524 
1525 OsproberEntryList
getOsproberEntriesForDevice(Device * device) const1526 ChoicePage::getOsproberEntriesForDevice( Device* device ) const
1527 {
1528     OsproberEntryList eList;
1529     for ( const OsproberEntry& entry : m_core->osproberEntries() )
1530     {
1531         if ( entry.path.startsWith( device->deviceNode() ) )
1532         {
1533             eList.append( entry );
1534         }
1535     }
1536     return eList;
1537 }
1538 
1539 
1540 bool
isNextEnabled() const1541 ChoicePage::isNextEnabled() const
1542 {
1543     return m_nextEnabled;
1544 }
1545 
1546 
1547 bool
calculateNextEnabled() const1548 ChoicePage::calculateNextEnabled() const
1549 {
1550     bool enabled = false;
1551     auto sm_p = m_beforePartitionBarsView ? m_beforePartitionBarsView->selectionModel() : nullptr;
1552 
1553     switch ( m_config->installChoice() )
1554     {
1555     case InstallChoice::NoChoice:
1556         cDebug() << "No partitioning choice";
1557         return false;
1558     case InstallChoice::Replace:
1559     case InstallChoice::Alongside:
1560         if ( !( sm_p && sm_p->currentIndex().isValid() ) )
1561         {
1562             cDebug() << "No partition selected";
1563             return false;
1564         }
1565         enabled = true;
1566         break;
1567     case InstallChoice::Erase:
1568     case InstallChoice::Manual:
1569         enabled = true;
1570     }
1571 
1572     if ( !enabled )
1573     {
1574         cDebug() << "No valid choice made";
1575         return false;
1576     }
1577 
1578 
1579     if ( m_isEfi
1580          && ( m_config->installChoice() == InstallChoice::Alongside
1581               || m_config->installChoice() == InstallChoice::Replace ) )
1582     {
1583         if ( m_core->efiSystemPartitions().count() == 0 )
1584         {
1585             cDebug() << "No EFI partition for alongside or replace";
1586             return false;
1587         }
1588     }
1589 
1590     if ( m_config->installChoice() != InstallChoice::Manual && m_encryptWidget->isVisible() )
1591     {
1592         switch ( m_encryptWidget->state() )
1593         {
1594         case EncryptWidget::Encryption::Unconfirmed:
1595             cDebug() << "No passphrase provided";
1596             return false;
1597         case EncryptWidget::Encryption::Disabled:
1598         case EncryptWidget::Encryption::Confirmed:
1599             // Checkbox not checked, **or** passphrases match
1600             break;
1601         }
1602     }
1603 
1604     return true;
1605 }
1606 
1607 
1608 void
updateNextEnabled()1609 ChoicePage::updateNextEnabled()
1610 {
1611     bool enabled = calculateNextEnabled();
1612 
1613     if ( enabled != m_nextEnabled )
1614     {
1615         m_nextEnabled = enabled;
1616         Q_EMIT nextStatusChanged( enabled );
1617     }
1618 }
1619 
1620 void
updateSwapChoicesTr()1621 ChoicePage::updateSwapChoicesTr()
1622 {
1623     if ( !m_eraseSwapChoiceComboBox )
1624     {
1625         return;
1626     }
1627 
1628     static_assert( SwapChoice::NoSwap == 0, "Enum values out-of-sync" );
1629     for ( int index = 0; index < m_eraseSwapChoiceComboBox->count(); ++index )
1630     {
1631         bool ok = false;
1632         int value = 0;
1633 
1634         switch ( value = m_eraseSwapChoiceComboBox->itemData( index ).toInt( &ok ) )
1635         {
1636         // case 0:
1637         case SwapChoice::NoSwap:
1638             // toInt() returns 0 on failure, so check for ok
1639             if ( ok )  // It was explicitly set to 0
1640             {
1641                 m_eraseSwapChoiceComboBox->setItemText( index, tr( "No Swap" ) );
1642             }
1643             else
1644             {
1645                 cWarning() << "Box item" << index << m_eraseSwapChoiceComboBox->itemText( index ) << "has non-integer role.";
1646             }
1647             break;
1648         case SwapChoice::ReuseSwap:
1649             m_eraseSwapChoiceComboBox->setItemText( index, tr( "Reuse Swap" ) );
1650             break;
1651         case SwapChoice::SmallSwap:
1652             m_eraseSwapChoiceComboBox->setItemText( index, tr( "Swap (no Hibernate)" ) );
1653             break;
1654         case SwapChoice::FullSwap:
1655             m_eraseSwapChoiceComboBox->setItemText( index, tr( "Swap (with Hibernate)" ) );
1656             break;
1657         case SwapChoice::SwapFile:
1658             m_eraseSwapChoiceComboBox->setItemText( index, tr( "Swap to file" ) );
1659             break;
1660         default:
1661             cWarning() << "Box item" << index << m_eraseSwapChoiceComboBox->itemText( index ) << "has role" << value;
1662         }
1663     }
1664 }
1665 
1666 void
updateChoiceButtonsTr()1667 ChoicePage::updateChoiceButtonsTr()
1668 {
1669     if ( m_somethingElseButton )
1670     {
1671         m_somethingElseButton->setText( tr( "<strong>Manual partitioning</strong><br/>"
1672                                             "You can create or resize partitions yourself." ) );
1673     }
1674 }
1675 
1676 int
lastSelectedDeviceIndex()1677 ChoicePage::lastSelectedDeviceIndex()
1678 {
1679     return m_lastSelectedDeviceIndex;
1680 }
1681 
1682 void
setLastSelectedDeviceIndex(int index)1683 ChoicePage::setLastSelectedDeviceIndex( int index )
1684 {
1685     m_lastSelectedDeviceIndex = index;
1686     m_drivesCombo->setCurrentIndex( m_lastSelectedDeviceIndex );
1687 }
1688