From fb5516cfdc717e679918d826fb40663fac113d6a Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Sat, 12 Oct 2024 10:02:56 +0200 Subject: [PATCH] Track operations widget with layout (#7537) Put the elements of the `TrackOperationsWidget` into layouts. These are: * The grip that can be used to move tracks * The gear icon that opens the operations menu * The mute button * The solo button The grip that can be used to move tracks around is extracted into its own class called `TrackGrip`. This has several advantages: * It can be put into a layout. * It can render itself at arbitrary sizes by simply repeating its pattern pixmap. * It can be used in a much more object-oriented way because it emits signals when it is grabbed and released. * It is responsible for locally updating its cursor state. The default cursor of the grip now is an open hand which indicates to the users that it can be grabbed. While being grabbed the cursor now is a closed hand. ## Technical details The class `TrackOperationsWidget` now holds an instance of `TrackGrip` and provides a getter to retrieve it. This getter is used by `TrackView` to connect to the two new signals `grabbed` and `released`. The method `TrackOperationsWidget::paintEvent` now only paints the background as it does not need to paint the grip anymore. The `TrackView` now handles the grabbing and release of the grip in `TrackView::onTrackGripGrabbed` and `TrackView::onTrackGripReleased`. Because the events and cursor states are now handled by `TrackGrip` this code could be removed from `TrackView::mousePressEvent`. There was a comment in `TrackView` which indicated that the `TrackOperationsWidget` had to be updated when the track is moved and released because it would hide some elements during the move. The comment and the corresponding code was removed because the operations widget does not hide its elements during moves (this was already the state before the changes made by this commit). Adjust the style sheets of the classic and default themes with regards to the `QPushButton` that's used to show the gear menu in the `TrackOperationsWidget`. The `>` has been removed because the `QPushButton` is not a direct decendent of the `TrackOperationsWidget` anymore. ### Wrapping of `PixmapButton` in `QWidget` The PixmapButtons that are used in `TrackOperationsWidget` current have to be wrapped into a `QWidget`. This is necessary due to some strange effect where the PixmapButtons are resized to a size that's larger than their minimum/fixed size when the method `show` is called in `TrackContainerView::realignTracks`. Specifically, with the default theme the buttons are resized from their minimum size of (16, 14) to (26, 26). This then makes them behave not as expected in layouts. The resizing is not done for QWidgets. Therefore we wrap the PixmapButton in a QWidget which is set to a fixed size that will be able to show the active and inactive pixmap. We can then use the QWidget in layouts without any disturbances. The resizing only seems to affect the track view hierarchy and is triggered by Qt's internal mechanisms. For example the buttons in the mixer view do not seem to be affected. If you want to debug this simply override "PixmapButton::resizeEvent" and trigger a break point in there, e.g. whenever the new size is not (16, 14). ### More layout-friendly PixmapButton Make the `PixmapButton` more friendly for layouts by implementing `minimumSizeHint`. It returns a size that accommodate to show the active and the inactive pixmap. Also make `sizeHint` return the minimum size hint. The previous implementation would have made layouts jump when the pixmap is toggled with pixmaps of different sizes. --- data/themes/classic/style.css | 10 +-- data/themes/default/style.css | 8 +- include/PixmapButton.h | 1 + include/TrackGrip.h | 68 ++++++++++++++ include/TrackOperationsWidget.h | 3 + include/TrackView.h | 3 +- src/gui/CMakeLists.txt | 1 + src/gui/tracks/TrackGrip.cpp | 106 ++++++++++++++++++++++ src/gui/tracks/TrackOperationsWidget.cpp | 107 +++++++++++++++-------- src/gui/tracks/TrackView.cpp | 31 ++++--- src/gui/widgets/PixmapButton.cpp | 13 ++- 11 files changed, 280 insertions(+), 71 deletions(-) create mode 100644 include/TrackGrip.h create mode 100644 src/gui/tracks/TrackGrip.cpp diff --git a/data/themes/classic/style.css b/data/themes/classic/style.css index 95737ed3afe..d098bf26660 100644 --- a/data/themes/classic/style.css +++ b/data/themes/classic/style.css @@ -375,7 +375,7 @@ lmms--gui--TrackContentWidget { /* gear button in tracks */ -lmms--gui--TrackOperationsWidget > QPushButton { +lmms--gui--TrackOperationsWidget QPushButton { max-height: 26px; max-width: 26px; min-height: 26px; @@ -384,7 +384,7 @@ lmms--gui--TrackOperationsWidget > QPushButton { border: none; } -lmms--gui--TrackOperationsWidget > QPushButton::menu-indicator { +lmms--gui--TrackOperationsWidget QPushButton::menu-indicator { image: url("resources:trackop.png"); subcontrol-origin: padding; subcontrol-position: center; @@ -392,12 +392,12 @@ lmms--gui--TrackOperationsWidget > QPushButton::menu-indicator { top: 1px; } -lmms--gui--TrackOperationsWidget > QPushButton::menu-indicator:hover { +lmms--gui--TrackOperationsWidget QPushButton::menu-indicator:hover { image: url("resources:trackop_h.png"); } -lmms--gui--TrackOperationsWidget > QPushButton::menu-indicator:pressed, -lmms--gui--TrackOperationsWidget > QPushButton::menu-indicator:checked { +lmms--gui--TrackOperationsWidget QPushButton::menu-indicator:pressed, +lmms--gui--TrackOperationsWidget QPushButton::menu-indicator:checked { image: url("resources:trackop_c.png"); position: relative; top: 2px; diff --git a/data/themes/default/style.css b/data/themes/default/style.css index e2dd369f952..83933df199a 100644 --- a/data/themes/default/style.css +++ b/data/themes/default/style.css @@ -411,7 +411,7 @@ lmms--gui--TrackContentWidget { /* gear button in tracks */ -lmms--gui--TrackOperationsWidget > QPushButton { +lmms--gui--TrackOperationsWidget QPushButton { max-height: 26px; max-width: 26px; min-height: 26px; @@ -420,7 +420,7 @@ lmms--gui--TrackOperationsWidget > QPushButton { border: none; } -lmms--gui--TrackOperationsWidget > QPushButton::menu-indicator { +lmms--gui--TrackOperationsWidget QPushButton::menu-indicator { image: url("resources:trackop.png"); subcontrol-origin: padding; subcontrol-position: center; @@ -428,8 +428,8 @@ lmms--gui--TrackOperationsWidget > QPushButton::menu-indicator { top: 1px; } -lmms--gui--TrackOperationsWidget > QPushButton::menu-indicator:pressed, -lmms--gui--TrackOperationsWidget > QPushButton::menu-indicator:checked { +lmms--gui--TrackOperationsWidget QPushButton::menu-indicator:pressed, +lmms--gui--TrackOperationsWidget QPushButton::menu-indicator:checked { image: url("resources:trackop.png"); position: relative; top: 2px; diff --git a/include/PixmapButton.h b/include/PixmapButton.h index 734bd11ae5c..a0b4141deba 100644 --- a/include/PixmapButton.h +++ b/include/PixmapButton.h @@ -45,6 +45,7 @@ class LMMS_EXPORT PixmapButton : public AutomatableButton void setInactiveGraphic( const QPixmap & _pm, bool _update = true ); QSize sizeHint() const override; + QSize minimumSizeHint() const override; signals: void doubleClicked(); diff --git a/include/TrackGrip.h b/include/TrackGrip.h new file mode 100644 index 00000000000..aa19222a6ff --- /dev/null +++ b/include/TrackGrip.h @@ -0,0 +1,68 @@ +/* + * TrackGrip.h - Grip that can be used to move tracks + * + * Copyright (c) 2024- Michael Gregorius + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#ifndef LMMS_GUI_TRACK_GRIP_H +#define LMMS_GUI_TRACK_GRIP_H + +#include + + +class QPixmap; + +namespace lmms +{ + +class Track; + +namespace gui +{ + +class TrackGrip : public QWidget +{ + Q_OBJECT +public: + TrackGrip(Track* track, QWidget* parent = 0); + ~TrackGrip() override = default; + +signals: + void grabbed(); + void released(); + +protected: + void mousePressEvent(QMouseEvent*) override; + void mouseReleaseEvent(QMouseEvent*) override; + void paintEvent(QPaintEvent*) override; + +private: + Track* m_track = nullptr; + bool m_isGrabbed = false; + static QPixmap* s_grabbedPixmap; + static QPixmap* s_releasedPixmap; +}; + +} // namespace gui + +} // namespace lmms + +#endif // LMMS_GUI_TRACK_GRIP_H diff --git a/include/TrackOperationsWidget.h b/include/TrackOperationsWidget.h index 4dbb5353c5f..8417298b423 100644 --- a/include/TrackOperationsWidget.h +++ b/include/TrackOperationsWidget.h @@ -33,6 +33,7 @@ namespace lmms::gui { class PixmapButton; +class TrackGrip; class TrackView; class TrackOperationsWidget : public QWidget @@ -42,6 +43,7 @@ class TrackOperationsWidget : public QWidget TrackOperationsWidget( TrackView * parent ); ~TrackOperationsWidget() override = default; + TrackGrip* getTrackGrip() const { return m_trackGrip; } protected: void mousePressEvent( QMouseEvent * me ) override; @@ -65,6 +67,7 @@ private slots: private: TrackView * m_trackView; + TrackGrip* m_trackGrip; QPushButton * m_trackOps; PixmapButton * m_muteBtn; PixmapButton * m_soloBtn; diff --git a/include/TrackView.h b/include/TrackView.h index b2654202b9a..c13ba1e870e 100644 --- a/include/TrackView.h +++ b/include/TrackView.h @@ -171,7 +171,8 @@ public slots: private slots: void createClipView( lmms::Clip * clip ); void muteChanged(); - + void onTrackGripGrabbed(); + void onTrackGripReleased(); } ; diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 4195ec58c1a..fe4a2c462b2 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -94,6 +94,7 @@ SET(LMMS_SRCS gui/tracks/TrackLabelButton.cpp gui/tracks/TrackOperationsWidget.cpp gui/tracks/TrackRenameLineEdit.cpp + gui/tracks/TrackGrip.cpp gui/tracks/TrackView.cpp gui/widgets/AutomatableButton.cpp diff --git a/src/gui/tracks/TrackGrip.cpp b/src/gui/tracks/TrackGrip.cpp new file mode 100644 index 00000000000..6133c6e962d --- /dev/null +++ b/src/gui/tracks/TrackGrip.cpp @@ -0,0 +1,106 @@ +/* + * TrackGrip.cpp - Grip that can be used to move tracks + * + * Copyright (c) 2024- Michael Gregorius + * + * This file is part of LMMS - https://lmms.io + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public + * License along with this program (see COPYING); if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301 USA. + * + */ + +#include "TrackGrip.h" + +#include "embed.h" +#include "Track.h" + +#include +#include +#include + + +namespace lmms::gui +{ + +QPixmap* TrackGrip::s_grabbedPixmap = nullptr; +QPixmap* TrackGrip::s_releasedPixmap = nullptr; + +constexpr int c_margin = 2; + +TrackGrip::TrackGrip(Track* track, QWidget* parent) : + QWidget(parent), + m_track(track) +{ + if (!s_grabbedPixmap) + { + s_grabbedPixmap = new QPixmap(embed::getIconPixmap("track_op_grip_c")); + } + + if (!s_releasedPixmap) + { + s_releasedPixmap = new QPixmap(embed::getIconPixmap("track_op_grip")); + } + + setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Expanding); + + setCursor(Qt::OpenHandCursor); + + setFixedWidth(std::max(s_grabbedPixmap->width(), s_releasedPixmap->width()) + 2 * c_margin); +} + +void TrackGrip::mousePressEvent(QMouseEvent* m) +{ + m->accept(); + + m_isGrabbed = true; + setCursor(Qt::ClosedHandCursor); + + emit grabbed(); + + update(); +} + +void TrackGrip::mouseReleaseEvent(QMouseEvent* m) +{ + m->accept(); + + m_isGrabbed = false; + setCursor(Qt::OpenHandCursor); + + emit released(); + + update(); +} + +void TrackGrip::paintEvent(QPaintEvent*) +{ + QPainter p(this); + + // Check if the color of the track should be used for the background + const auto color = m_track->color(); + const auto muted = m_track->getMutedModel()->value(); + + if (color.has_value() && !muted) + { + p.fillRect(rect(), color.value()); + } + + // Paint the pixmap + auto r = rect().marginsRemoved(QMargins(c_margin, c_margin, c_margin, c_margin)); + p.drawTiledPixmap(r, m_isGrabbed ? *s_grabbedPixmap : *s_releasedPixmap); +} + +} // namespace lmms::gui diff --git a/src/gui/tracks/TrackOperationsWidget.cpp b/src/gui/tracks/TrackOperationsWidget.cpp index 6cb74e2c5b7..6aca6628242 100644 --- a/src/gui/tracks/TrackOperationsWidget.cpp +++ b/src/gui/tracks/TrackOperationsWidget.cpp @@ -24,6 +24,7 @@ #include "TrackOperationsWidget.h" +#include #include #include #include @@ -44,6 +45,7 @@ #include "StringPairDrag.h" #include "Track.h" #include "TrackContainerView.h" +#include "TrackGrip.h" #include "TrackView.h" namespace lmms::gui @@ -68,41 +70,86 @@ TrackOperationsWidget::TrackOperationsWidget( TrackView * parent ) : setObjectName( "automationEnabled" ); + auto layout = new QHBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(0); + layout->setAlignment(Qt::AlignTop); - m_trackOps = new QPushButton( this ); - m_trackOps->move( 12, 1 ); + m_trackGrip = new TrackGrip(m_trackView->getTrack(), this); + layout->addWidget(m_trackGrip); + + // This widget holds the gear icon and the mute and solo + // buttons in a layout. + auto operationsWidget = new QWidget(this); + auto operationsLayout = new QHBoxLayout(operationsWidget); + operationsLayout->setContentsMargins(0, 0, 0, 0); + operationsLayout->setSpacing(0); + + m_trackOps = new QPushButton(operationsWidget); m_trackOps->setFocusPolicy( Qt::NoFocus ); m_trackOps->setMenu( toMenu ); m_trackOps->setToolTip(tr("Actions")); + // This helper lambda wraps a PixmapButton in a QWidget. This is necessary due to some strange effect where the + // PixmapButtons are resized to a size that's larger than their minimum/fixed size when the method "show" is called + // in "TrackContainerView::realignTracks". Specifically, with the default theme the buttons are resized from + // (16, 14) to (26, 26). This then makes them behave not as expected in layouts. + // The resizing is not done for QWidgets. Therefore we wrap the PixmapButton in a QWidget which is set to a + // fixed size that will be able to show the active and inactive pixmap. We can then use the QWidget in layouts + // without any disturbances. + // + // The resizing only seems to affect the track view hierarchy and is triggered by Qt's internal mechanisms. + // For example the buttons in the mixer view do not seem to be affected. + // If you want to debug this simply override "PixmapButton::resizeEvent" and trigger a break point in there. + auto buildPixmapButtonWrappedInWidget = [](QWidget* parent, const QString& toolTip, + std::string_view activeGraphic, std::string_view inactiveGraphic, PixmapButton*& pixmapButton) + { + const auto activePixmap = embed::getIconPixmap(activeGraphic); + const auto inactivePixmap = embed::getIconPixmap(inactiveGraphic); + + auto necessarySize = activePixmap.size().expandedTo(inactivePixmap.size()); + + auto wrapperWidget = new QWidget(parent); + wrapperWidget->setFixedSize(necessarySize); + + auto button = new PixmapButton(wrapperWidget, toolTip); + button->setCheckable(true); + button->setActiveGraphic(activePixmap); + button->setInactiveGraphic(inactivePixmap); + button->setToolTip(toolTip); + + pixmapButton = button; + + return wrapperWidget; + }; - m_muteBtn = new PixmapButton( this, tr( "Mute" ) ); - m_muteBtn->setActiveGraphic( embed::getIconPixmap( "led_off" ) ); - m_muteBtn->setInactiveGraphic( embed::getIconPixmap( "led_green" ) ); - m_muteBtn->setCheckable( true ); + auto muteWidget = buildPixmapButtonWrappedInWidget(operationsWidget, tr("Mute"), "led_off", "led_green", m_muteBtn); + auto soloWidget = buildPixmapButtonWrappedInWidget(operationsWidget, tr("Solo"), "led_red", "led_off", m_soloBtn); - m_soloBtn = new PixmapButton( this, tr( "Solo" ) ); - m_soloBtn->setActiveGraphic( embed::getIconPixmap( "led_red" ) ); - m_soloBtn->setInactiveGraphic( embed::getIconPixmap( "led_off" ) ); - m_soloBtn->setCheckable( true ); + operationsLayout->addWidget(m_trackOps, Qt::AlignCenter); + operationsLayout->addSpacing(5); if( ConfigManager::inst()->value( "ui", "compacttrackbuttons" ).toInt() ) { - m_muteBtn->move( 46, 0 ); - m_soloBtn->move( 46, 16 ); + auto vlayout = new QVBoxLayout(); + vlayout->setContentsMargins(0, 0, 0, 0); + vlayout->setSpacing(0); + vlayout->addStretch(1); + vlayout->addWidget(muteWidget); + vlayout->addWidget(soloWidget); + vlayout->addStretch(1); + operationsLayout->addLayout(vlayout); } else { - m_muteBtn->move( 46, 8 ); - m_soloBtn->move( 62, 8 ); + operationsLayout->addWidget(muteWidget, Qt::AlignCenter); + operationsLayout->addWidget(soloWidget, Qt::AlignCenter); } - m_muteBtn->show(); - m_muteBtn->setToolTip(tr("Mute")); + operationsLayout->addStretch(1); - m_soloBtn->show(); - m_soloBtn->setToolTip(tr("Solo")); + layout->addWidget(operationsWidget, 0, Qt::AlignTop); connect( this, SIGNAL(trackRemovalScheduled(lmms::gui::TrackView*)), m_trackView->trackContainerView(), @@ -154,31 +201,17 @@ void TrackOperationsWidget::mousePressEvent( QMouseEvent * me ) -/*! \brief Repaint the trackOperationsWidget +/*! + * \brief Repaint the trackOperationsWidget * - * If we're not moving, and in the Pattern Editor, then turn - * automation on or off depending on its previous state and show - * ourselves. - * - * Otherwise, hide ourselves. - * - * \todo Flesh this out a bit - is it correct? - * \param pe The paint event to respond to + * Only things that's done for now is to paint the background + * with the brush of the window from the palette. */ -void TrackOperationsWidget::paintEvent( QPaintEvent * pe ) +void TrackOperationsWidget::paintEvent(QPaintEvent*) { QPainter p( this ); p.fillRect(rect(), palette().brush(QPalette::Window)); - - if (m_trackView->getTrack()->color().has_value() && !m_trackView->getTrack()->getMutedModel()->value()) - { - QRect coloredRect( 0, 0, 10, m_trackView->getTrack()->getHeight() ); - - p.fillRect(coloredRect, m_trackView->getTrack()->color().value()); - } - - p.drawPixmap(2, 2, embed::getIconPixmap(m_trackView->isMovingTrack() ? "track_op_grip_c" : "track_op_grip")); } diff --git a/src/gui/tracks/TrackView.cpp b/src/gui/tracks/TrackView.cpp index e2323602154..ecd397975f1 100644 --- a/src/gui/tracks/TrackView.cpp +++ b/src/gui/tracks/TrackView.cpp @@ -41,6 +41,7 @@ #include "PixmapButton.h" #include "StringPairDrag.h" #include "Track.h" +#include "TrackGrip.h" #include "TrackContainerView.h" #include "ClipView.h" @@ -102,6 +103,10 @@ TrackView::TrackView( Track * track, TrackContainerView * tcv ) : connect( &m_track->m_soloModel, SIGNAL(dataChanged()), m_track, SLOT(toggleSolo()), Qt::DirectConnection ); + + auto trackGrip = m_trackOperationsWidget.getTrackGrip(); + connect(trackGrip, &TrackGrip::grabbed, this, &TrackView::onTrackGripGrabbed); + connect(trackGrip, &TrackGrip::released, this, &TrackView::onTrackGripReleased); // create views for already existing clips for (const auto& clip : m_track->m_clips) @@ -284,22 +289,6 @@ void TrackView::mousePressEvent( QMouseEvent * me ) QCursor c( Qt::SizeVerCursor); QApplication::setOverrideCursor( c ); } - else - { - if( me->x()>10 ) // 10 = The width of the grip + 2 pixels to the left and right. - { - QWidget::mousePressEvent( me ); - return; - } - - m_action = Action::Move; - - QCursor c( Qt::SizeVerCursor ); - QApplication::setOverrideCursor( c ); - // update because in move-mode, all elements in - // track-op-widgets are hidden as a visual feedback - m_trackOperationsWidget.update(); - } me->accept(); } @@ -451,6 +440,16 @@ void TrackView::muteChanged() } +void TrackView::onTrackGripGrabbed() +{ + m_action = Action::Move; +} + +void TrackView::onTrackGripReleased() +{ + m_action = Action::None; +} + void TrackView::setIndicatorMute(FadeButton* indicator, bool muted) diff --git a/src/gui/widgets/PixmapButton.cpp b/src/gui/widgets/PixmapButton.cpp index 069acad567c..5f2e01b3d24 100644 --- a/src/gui/widgets/PixmapButton.cpp +++ b/src/gui/widgets/PixmapButton.cpp @@ -124,16 +124,13 @@ void PixmapButton::setInactiveGraphic( const QPixmap & _pm, bool _update ) QSize PixmapButton::sizeHint() const { - if (isActive()) - { - return m_activePixmap.size(); - } - else - { - return m_inactivePixmap.size(); - } + return minimumSizeHint(); } +QSize PixmapButton::minimumSizeHint() const +{ + return m_activePixmap.size().expandedTo(m_inactivePixmap.size()); +} bool PixmapButton::isActive() const {