/* === This file is part of Calamares - === * * SPDX-FileCopyrightText: 2014 Aurélien Gâteau * SPDX-FileCopyrightText: 2015-2016 Teo Mrnjavac * SPDX-License-Identifier: GPL-3.0-or-later * * Calamares is Free Software: see the License-Identifier above. * */ #include "gui/PartitionBarsView.h" #include "core/ColorUtils.h" #include "core/PartitionModel.h" #include "utils/CalamaresUtilsGui.h" #include "utils/Logger.h" #include #include #include #include #include static const int VIEW_HEIGHT = qMax( CalamaresUtils::defaultFontHeight() + 8, // wins out with big fonts int( CalamaresUtils::defaultFontHeight() * 0.6 ) + 22 ); // wins out with small fonts static constexpr int CORNER_RADIUS = 3; static const int EXTENDED_PARTITION_MARGIN = qMax( 4, VIEW_HEIGHT / 6 ); // The SELECTION_MARGIN is applied within a hardcoded 2px padding anyway, so // we start from EXTENDED_PARTITION_MARGIN - 2 in all cases. // Then we try to ensure the selection rectangle fits exactly between the extended // rectangle and the outer frame (the "/ 2" part), unless that's not possible, and in // that case we at least make sure we have a 1px gap between the selection rectangle // and the extended partition box (the "- 2" part). // At worst, on low DPI systems, this will mean in order: // 1px outer rect, 1 px gap, 1px selection rect, 1px gap, 1px extended partition rect. static const int SELECTION_MARGIN = qMin( ( EXTENDED_PARTITION_MARGIN - 2 ) / 2, ( EXTENDED_PARTITION_MARGIN - 2 ) - 2 ); PartitionBarsView::PartitionBarsView( QWidget* parent ) : QAbstractItemView( parent ) , m_nestedPartitionsMode( NoNestedPartitions ) , canBeSelected( []( const QModelIndex& ) { return true; } ) , m_hoveredIndex( QModelIndex() ) { this->setObjectName( "partitionBarView" ); setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Fixed ); setFrameStyle( QFrame::NoFrame ); setSelectionBehavior( QAbstractItemView::SelectRows ); setSelectionMode( QAbstractItemView::SingleSelection ); // Debug connect( this, &PartitionBarsView::clicked, this, [ = ]( const QModelIndex& index ) { cDebug() << "Clicked row" << index.row(); } ); setMouseTracking( true ); } PartitionBarsView::~PartitionBarsView() {} void PartitionBarsView::setNestedPartitionsMode( PartitionBarsView::NestedPartitionsMode mode ) { m_nestedPartitionsMode = mode; viewport()->repaint(); } QSize PartitionBarsView::minimumSizeHint() const { return sizeHint(); } QSize PartitionBarsView::sizeHint() const { return QSize( -1, VIEW_HEIGHT ); } void PartitionBarsView::paintEvent( QPaintEvent* event ) { QPainter painter( viewport() ); painter.fillRect( rect(), palette().window() ); painter.setRenderHint( QPainter::Antialiasing ); QRect partitionsRect = rect(); partitionsRect.setHeight( VIEW_HEIGHT ); painter.save(); drawPartitions( &painter, partitionsRect, QModelIndex() ); painter.restore(); } void PartitionBarsView::drawSection( QPainter* painter, const QRect& rect_, int x, int width, const QModelIndex& index ) { QColor color = index.isValid() ? index.data( Qt::DecorationRole ).value< QColor >() : ColorUtils::unknownDisklabelColor(); bool isFreeSpace = index.isValid() ? index.data( PartitionModel::IsFreeSpaceRole ).toBool() : true; QRect rect = rect_; const int y = rect.y(); const int height = rect.height(); const int radius = qMax( 1, CORNER_RADIUS - ( VIEW_HEIGHT - height ) / 2 ); painter->setClipRect( x, y, width, height ); painter->translate( 0.5, 0.5 ); rect.adjust( 0, 0, -1, -1 ); if ( selectionMode() != QAbstractItemView::NoSelection && // no hover without selection m_hoveredIndex.isValid() && index == m_hoveredIndex ) { if ( canBeSelected( index ) ) { painter->setBrush( color.lighter( 115 ) ); } else { painter->setBrush( color ); } } else { painter->setBrush( color ); } QColor borderColor = color.darker(); painter->setPen( borderColor ); painter->drawRoundedRect( rect, radius, radius ); // Draw shade if ( !isFreeSpace ) { rect.adjust( 2, 2, -2, -2 ); } QLinearGradient gradient( 0, 0, 0, height / 2 ); qreal c = isFreeSpace ? 0 : 1; gradient.setColorAt( 0, QColor::fromRgbF( c, c, c, 0.3 ) ); gradient.setColorAt( 1, QColor::fromRgbF( c, c, c, 0 ) ); painter->setPen( Qt::NoPen ); painter->setBrush( gradient ); painter->drawRoundedRect( rect, radius, radius ); if ( selectionMode() != QAbstractItemView::NoSelection && index.isValid() && selectionModel() && !selectionModel()->selectedIndexes().isEmpty() && selectionModel()->selectedIndexes().first() == index ) { painter->setPen( QPen( borderColor, 1 ) ); QColor highlightColor = QPalette().highlight().color(); highlightColor = highlightColor.lighter( 500 ); highlightColor.setAlpha( 120 ); painter->setBrush( highlightColor ); QRect selectionRect = rect; selectionRect.setX( x + 1 ); selectionRect.setWidth( width - 3 ); //account for the previous rect.adjust if ( rect.x() > selectionRect.x() ) //hack for first item { selectionRect.adjust( rect.x() - selectionRect.x(), 0, 0, 0 ); } if ( rect.right() < selectionRect.right() ) //hack for last item { selectionRect.adjust( 0, 0, -( selectionRect.right() - rect.right() ), 0 ); } selectionRect.adjust( SELECTION_MARGIN, SELECTION_MARGIN, -SELECTION_MARGIN, -SELECTION_MARGIN ); painter->drawRoundedRect( selectionRect, radius - 1, radius - 1 ); } painter->translate( -0.5, -0.5 ); } void PartitionBarsView::drawPartitions( QPainter* painter, const QRect& rect, const QModelIndex& parent ) { PartitionModel* modl = qobject_cast< PartitionModel* >( model() ); if ( !modl ) { return; } const int totalWidth = rect.width(); auto pair = computeItemsVector( parent ); QVector< PartitionBarsView::Item >& items = pair.first; qreal& total = pair.second; int x = rect.x(); for ( int row = 0; row < items.count(); ++row ) { const auto& item = items[ row ]; int width; if ( row < items.count() - 1 ) { width = totalWidth * ( item.size / total ); } else // Make sure we fill the last pixel column { width = rect.right() - x + 1; } drawSection( painter, rect, x, width, item.index ); if ( m_nestedPartitionsMode == DrawNestedPartitions && modl->hasChildren( item.index ) ) { QRect subRect( x + EXTENDED_PARTITION_MARGIN, rect.y() + EXTENDED_PARTITION_MARGIN, width - 2 * EXTENDED_PARTITION_MARGIN, rect.height() - 2 * EXTENDED_PARTITION_MARGIN ); drawPartitions( painter, subRect, item.index ); } x += width; } if ( !items.count() && !modl->device()->partitionTable() ) // No disklabel or unknown { int width = rect.right() - rect.x() + 1; drawSection( painter, rect, rect.x(), width, QModelIndex() ); } } QModelIndex PartitionBarsView::indexAt( const QPoint& point ) const { return indexAt( point, rect(), QModelIndex() ); } QModelIndex PartitionBarsView::indexAt( const QPoint& point, const QRect& rect, const QModelIndex& parent ) const { PartitionModel* modl = qobject_cast< PartitionModel* >( model() ); if ( !modl ) { return QModelIndex(); } const int totalWidth = rect.width(); auto pair = computeItemsVector( parent ); QVector< PartitionBarsView::Item >& items = pair.first; qreal& total = pair.second; int x = rect.x(); for ( int row = 0; row < items.count(); ++row ) { const auto& item = items[ row ]; int width; if ( row < items.count() - 1 ) { width = totalWidth * ( item.size / total ); } else // Make sure we fill the last pixel column { width = rect.right() - x + 1; } QRect thisItemRect( x, rect.y(), width, rect.height() ); if ( thisItemRect.contains( point ) ) { if ( m_nestedPartitionsMode == DrawNestedPartitions && modl->hasChildren( item.index ) ) { QRect subRect( x + EXTENDED_PARTITION_MARGIN, rect.y() + EXTENDED_PARTITION_MARGIN, width - 2 * EXTENDED_PARTITION_MARGIN, rect.height() - 2 * EXTENDED_PARTITION_MARGIN ); if ( subRect.contains( point ) ) { return indexAt( point, subRect, item.index ); } return item.index; } else // contains but no children, we win { return item.index; } } x += width; } return QModelIndex(); } QRect PartitionBarsView::visualRect( const QModelIndex& index ) const { return visualRect( index, rect(), QModelIndex() ); } QRect PartitionBarsView::visualRect( const QModelIndex& index, const QRect& rect, const QModelIndex& parent ) const { PartitionModel* modl = qobject_cast< PartitionModel* >( model() ); if ( !modl ) { return QRect(); } const int totalWidth = rect.width(); auto pair = computeItemsVector( parent ); QVector< PartitionBarsView::Item >& items = pair.first; qreal& total = pair.second; int x = rect.x(); for ( int row = 0; row < items.count(); ++row ) { const auto& item = items[ row ]; int width; if ( row < items.count() - 1 ) { width = totalWidth * ( item.size / total ); } else // Make sure we fill the last pixel column { width = rect.right() - x + 1; } QRect thisItemRect( x, rect.y(), width, rect.height() ); if ( item.index == index ) { return thisItemRect; } if ( m_nestedPartitionsMode == DrawNestedPartitions && modl->hasChildren( item.index ) && index.parent() == item.index ) { QRect subRect( x + EXTENDED_PARTITION_MARGIN, rect.y() + EXTENDED_PARTITION_MARGIN, width - 2 * EXTENDED_PARTITION_MARGIN, rect.height() - 2 * EXTENDED_PARTITION_MARGIN ); QRect candidateVisualRect = visualRect( index, subRect, item.index ); if ( !candidateVisualRect.isNull() ) { return candidateVisualRect; } } x += width; } return QRect(); } QRegion PartitionBarsView::visualRegionForSelection( const QItemSelection& selection ) const { return QRegion(); } int PartitionBarsView::horizontalOffset() const { return 0; } int PartitionBarsView::verticalOffset() const { return 0; } void PartitionBarsView::scrollTo( const QModelIndex& index, ScrollHint hint ) { Q_UNUSED( index ) Q_UNUSED( hint ) } void PartitionBarsView::setSelectionModel( QItemSelectionModel* selectionModel ) { QAbstractItemView::setSelectionModel( selectionModel ); connect( selectionModel, &QItemSelectionModel::selectionChanged, this, [ = ] { viewport()->repaint(); } ); } void PartitionBarsView::setSelectionFilter( std::function< bool( const QModelIndex& ) > canBeSelected ) { this->canBeSelected = canBeSelected; } QModelIndex PartitionBarsView::moveCursor( CursorAction, Qt::KeyboardModifiers ) { return QModelIndex(); } bool PartitionBarsView::isIndexHidden( const QModelIndex& ) const { return false; } void PartitionBarsView::setSelection( const QRect& rect, QItemSelectionModel::SelectionFlags flags ) { //HACK: this is an utterly awful workaround, which is unfortunately necessary. // QAbstractItemView::mousePressedEvent calls setSelection, but before that, // for some mental reason, it works under the assumption that every item is a // rectangle. This rectangle is provided by visualRect, and the idea mostly // works, except when the item is an extended partition item, which is of course // a rectangle with a rectangular hole in the middle. // QAbstractItemView::mousePressEvent builds a QRect with x1, y1 in the center // of said visualRect, and x2, y2 in the real QMouseEvent position. // This may very well yield a QRect with negative size, which is meaningless. // Therefore the QRect we get here is totally bogus, and its topLeft is outside // the actual area of the item we need. // What we need are the real coordinates of the QMouseEvent, and the only way to // get them is by fetching the private x2, y2 from the rect. // TL;DR: this sucks, look away. -- Teo 12/2015 int x1, y1, x2, y2; rect.getCoords( &x1, &y1, &x2, &y2 ); QModelIndex eventIndex = indexAt( QPoint( x2, y2 ) ); if ( canBeSelected( eventIndex ) ) { selectionModel()->select( eventIndex, flags ); } viewport()->repaint(); } void PartitionBarsView::mouseMoveEvent( QMouseEvent* event ) { QModelIndex candidateIndex = indexAt( event->pos() ); QPersistentModelIndex oldHoveredIndex = m_hoveredIndex; if ( candidateIndex.isValid() ) { m_hoveredIndex = candidateIndex; } else { m_hoveredIndex = QModelIndex(); QGuiApplication::restoreOverrideCursor(); } if ( oldHoveredIndex != m_hoveredIndex ) { if ( m_hoveredIndex.isValid() && !canBeSelected( m_hoveredIndex ) ) { QGuiApplication::setOverrideCursor( Qt::ForbiddenCursor ); } else { QGuiApplication::restoreOverrideCursor(); } viewport()->repaint(); } } void PartitionBarsView::leaveEvent( QEvent* ) { QGuiApplication::restoreOverrideCursor(); if ( m_hoveredIndex.isValid() ) { m_hoveredIndex = QModelIndex(); viewport()->repaint(); } } void PartitionBarsView::mousePressEvent( QMouseEvent* event ) { QModelIndex candidateIndex = indexAt( event->pos() ); if ( canBeSelected( candidateIndex ) ) { QAbstractItemView::mousePressEvent( event ); } else { event->accept(); } } void PartitionBarsView::updateGeometries() { updateGeometry(); //get a new rect() for redrawing all the labels } QPair< QVector< PartitionBarsView::Item >, qreal > PartitionBarsView::computeItemsVector( const QModelIndex& parent ) const { int count = model()->rowCount( parent ); QVector< PartitionBarsView::Item > items; qreal total = 0; for ( int row = 0; row < count; ++row ) { QModelIndex index = model()->index( row, 0, parent ); if ( m_nestedPartitionsMode == NoNestedPartitions && model()->hasChildren( index ) ) { QPair< QVector< PartitionBarsView::Item >, qreal > childVect = computeItemsVector( index ); items += childVect.first; total += childVect.second; } else { qreal size = index.data( PartitionModel::SizeRole ).toLongLong(); total += size; items.append( { size, index } ); } } count = items.count(); // The sizes we have are perfect, but now we have to hardcode a minimum size for small // partitions and compensate for it in the total. qreal adjustedTotal = total; for ( int row = 0; row < count; ++row ) { if ( items[ row ].size < 0.01 * total ) // If this item is smaller than 1% of everything, { // force its width to 1%. adjustedTotal -= items[ row ].size; items[ row ].size = 0.01 * total; adjustedTotal += items[ row ].size; } } return qMakePair( items, adjustedTotal ); }