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