From c6ed4a274abeab4e811460fd4c5cc06832821f15 Mon Sep 17 00:00:00 2001 From: Michael Gregorius Date: Sun, 29 Oct 2023 11:16:39 +0100 Subject: [PATCH] First version of a new dynamic LADSPA control dialog (#2068) Introduce a new version of the LADSPA control dialog which uses "bar controllers" and arranges them in a grid layout. Long parameter names are elided long if needed. The new dialog is implemented in the class `LadspaMatrixControlDialog`. Note: it is still possible to reactivate the old dialog as it has not been removed yet. You can do so in `plugins/LadspaEffect/LadspaControls.h` by replacing the implementation of `createView()` with the following: ``` gui::EffectControlDialog* createView() override { return new gui::LadspaControlDialog( this ); } ``` Introduce the new base class `FloatModelEditorBase`. It serves as the base widget for widgets that manipulate a float model and provides some base functionalities like context menus, edit dialogs, mouse handling, etc. Currently `BarModelEditor` and `Knob` inherit from `FloatModelEditorBase`. Therefore lots of code was moved from `Knob` to `FloatModelEditorBase`. `BarModelEditor` supports style sheets. See the changes in style.css for the available options and their usage. Extend `LedCheckBox` so that it adds support to render the text with the default font in the default size. The class now supports two modes: * Legacy mode * Non-legacy mode Legacy mode is the default and renders the LedCheckBox as before. The font is set to 7 pixels and all the text is rendered with a shadow. Non-legacy mode uses the current font to render the text. The text is rendered without a shadow and the pixmap is centered vertically to the left side of the text. Non-legacy mode is currently only used in the context of the matrix LADSPA dialog. Toggle options are rendered using it as well as the "Link Channels" button. Add `TempoSyncBarModelEditor` which adds a tempo sync option to a `BarModelEditor`. Please note that the code was mostly copied and adjusted from the `TempoSyncKnob` class. It was not attempted yet to unify some of the code because with the current design it seems to be a road to "inheritance hell". A better solution might be to refactor the code so that it becomes more composable. What follows are the individual commit messages of the 64 commits that have been squashed into a single merge commit. * First version of a new dynamic LADSPA control dialog The new dialog shows the LADSPA controls in a matrix layout. Each row corresponds to a LADSPA parameter. Each parameter can have several channels which can be linked. Each channel has its own row of controls. The class LadspaMatrixControlView was added by copying and modifying LadspaControlView to get rid of the link buttons which are associated with some controls and some labels. * Remove trailing whitespaces * Merge fix * Use the new connect syntax * Remove empty destructors * Use override keyword * Reformat a bit * Use nullptr * Use range-based loops * Add BarModelEditor and improve layouts Add the new class `BarModelEditor` which is intended to become a new way to adjust values of float models. Simplify the layout in `LadspaMatrixControlDialog` by removing some nested layouts. Remove the "Parameters" column. Adjust `LadspaMatrixControlView` to implement the following changes: * Show the name of the control next to toggle buttons (`LedCheckBox`). * Use the new `BarModelEditor` for integer and float types. * SHow the name of the control next to time based parameters that use `TempoSyncKnob`. The names are shown so that the "Parameters" column can be removed. Technical details ------------------ The class `LadspaMatrixControlDialog` now creates a widget that contains the matrix layout with the controls. This widget is then added to a scroll area. The layout is populated in the new method `arrangeControls`. Add some helper methods to `LadspaMatrixControlDialog` which retrieve the `LadspaControls` instance and the number of channels. Add the implementation of `BarModelEditor` to `src/gui/CMakeLists.txt`. TODOs ------ Extract common code out of the `Knob` class so that it can be reused by `BarModelEditor`. * Use factory to create LADSPA control widgets Replace the class `LadspaMatrixControlView` with the factory class `LadspaWidgetFactory`. The former was a widget that wrapped the widget representation of a LADSPA control in yet another widget with a layout. The factory simply returns the configured widget so that it can be incorporated directly in layouts or other widgets. Adjust `LadspaMatrixControlDialog` so that it uses the `LadspaWidgetFactory` instead of the `LadspaMatrixControlView`. * Initial version of FloatModelEditorBase Create an initial version of `FloatModelEditorBase`. It is intended to become the base widget for all widgets that manipulate a float model and provides some base functionalities like context menus, edit dialogs, mouse handling, etc. The initial version is a copy of `Knob`. The enum and its values have been suffixed with "Temp" as they will be removed later anyway. Same applies for the function `convertPixmapToGrayScaleTemp`. * Remove Knob related code from FloatModelEditorBase First removal of Knob related code from FloatModelEditorBase. * Remove setHtmlLabel * Remove enum for knob types * Remove label related code Remove setLabel and the members m_label and m_isHtmlLabel. * Remove several Qt properties * Remove angle related members and methods * Remove unused members and includes * Clean up includes * Make BarModelEditor inherit from FloatModelEditorBase Make `BarModelEditor` inherit from `FloatModelEditorBase` so that it inherits all shared functionality. Currently the class mostly implements size related methods and overrides the paint method. * Move LadspaWidgetFactory to LadspaEffect Move the class `LadspaWidgetFactory` to the project LadspaEffect to make it hopefully compile with mingw32 and mingw64. Interestingly the code compiled and worked under Linux and MacOS. * Code adjustments after scripted checks Add an include guard and an additional `#pragme once`. Add comments to closing namespace scopes. * Export BarModelEditor Export BarModelEditor so that for example the LadspaEffect project/ plugin can use it. * Improve rendering of bar model editor Increase the margin from 2 to 3 so that we have a more prominent border around the filled area. Fix a problem in the rendering code which led to situations where the bar was drawn to the left of the start position for small values. Return a more exact minimum size hint. * Elide long parameter names Elide long parameter names if needed. * Enable horizontal direction of manipulation Extend `FloatModelEditorBase` so that it also allows manipulation of the values when the mouse is moved in horizontal directions. The default is to use the vertical direction. Make use of this new feature in `BarModelEditor` which now reacts to horizontal movements when changing values. * Represent enum parameters using the bar widget The implementation of the current LADSPA dialog in master uses knobs to represent enum parameters. Therefore it should also work with the new bar widget. This gets rid of many labels with the parameter name followed by "(unsupported)". * Remove TODO in LadspaWidgetFactory * Render text of LedCheckBox with default font Extend LedCheckBox so that it adds support to render the text with the default font in the default size. The class now supports two modes: * Legacy mode * Non-legacy mode Legacy mode is the default and renders the LedCheckBox as before. The font is set to 7 pixels and all the text is rendered with a shadow. Non-legacy mode uses the current font to render the text. The text is rendered without a shadow and the pixmap is centered vertically to the left side of the text. The non-legacy mode is currently only used in the context of the matrix LADSPA dialog. Toggle options are rendered using it as well as the "Link Channels" button. * Use "yellow" LEDs for bool parameters Use "yellow" LEDs (which are actually blue) for dynamically added bool parameters so that the dialog is consistent with regards to the LED colors. The "Link Channels" button also uses a "yellow" LED. * Fix Qt5 builds Fix the Qt5 builds which do not know horizontalAdvance as a member of FontMetrics. Also refactor LedCheckBox::onTextUpdated to make it more compact. * Style sheets for BarModelEditor Add style sheets options for BarModelEditor. See the changes in style.css for the available options and their usage. The members that can be manipulated by the style sheet options are initialized accoriding to the palette so that the BarModelEditor should also render something useful if no style is set. Adjust the paintEvent method to use the style sheet brushes and colors. * Layout optimizations Set the vertical scroll bar to always show so that the controls do not move around if the scroll bar is hidden or shown. Set the margin of the grid layout to 0 so that the whole dialog becomes tighter. * Get rid of initial scroll bars Make sure that the widget is shown without a scrollbar whenever possible using the following rules. If the widget fits on the workspace we use the height of the widget as the minimum size of the scroll area. This will ensure that the scrollbar is not shown initially (and never will be). If the widget is larger than the workspace then we want it to mostly cover the workspace. In that case the minimum height is set to 90 % of the workspace. * Switch to a green theme Switch to a green theme to better match the default theme. * Adjust classic theme Adjust the classic theme so that it renders the bars in a legible way. * Fixes for CodeFactor Remove virtual keyword from methods that are marked as override. Remove whitespaces that make CodeFactor unhappy. One of these fixes includes moving the implementation of LadspaMatrixControlDialog::isResizable into the cpp file. * CodeFactor is still unhappy Remove a blank line after the constructor. * Center align time based controls Align time based controls, i.e. knobs, in the center. The implementation defeats the purpose of the widget factory a bit but it makes the design look nicer. * Fix build Fix the build by adjusting the enums. I added the changes while writing the commit message for the merge commit but they did not make it. * Attempt at CodeFactor warning Try to make the last CodeFactor warning disappear. * Add bar controller with tempo sync option Add `TempoSyncBarModelEditor` which adds a tempo sync option to a `BarModelEditor`. Please note that the code was mostly copied and adjusted from the `TempoSyncKnob` class. It was not attempted yet to unify some of the code because with the current design it seems to be a road to "inheritance hell". A better solution might be to refactor the code so that it is more composable. Another option might be to move the tempo sync functionality into a class like `FloatModelEditorBase`. Although this would not be a 100% right place either because `TempoSyncKnobModel` inherits from `FloatModel`. In that case we'd have specialized code in a generic base class. Adjust `LadspaWidgetFactory` so that it now returns an instance of a `TempoSyncBarModelEditor` instead of a `TempoSyncKnob`. Remove the extra layout code from `LadspaMatrixControlDialog.cpp` as most things are bar editors now. Adjust `TempoSyncKnobModel` so we do not have to make `TempoSyncBarModelEditor` a friend of it. * Another attempt to please CodeFactor Have another attempt to fix CodeFactor's complaints about `LadspaMatrixControlDialog.h`. * Coding conventions, blanks and blank lines Remove lots of unnecessary white space. Remove blank lines. Remove leading underscores from parameters. Remove line breaks in `TempoSyncBarModelEditor.cpp` to make the code more readable. Remove repeated method calls by introducing local variables. * Break down complex method Break down the complex method `TempoSyncBarModelEditor::updateDescAndIcon` by delegating to the new methods `updateTextDescription` and `updateIcon`. * Another attempt to please CodeFactor Another attempt to fix `LadspaMatrixControlDialog.h` to please CodeFactor. * Work on TODOs The TODO "Assumes that all processors are equal..." has been turned into a comment when we start iterating over the channels. If the channels are not of the same structure this would likely also have been a problem in the old implementation. The TODO "Use a factory..." has been removed as this is already done. The include of `FloatModelEditorBase.h` in LadspaWidgetFactory has been removed. * Modifier keys for mouse wheel adjustments Integrate the changes of commit 7000afb2eac into `FloatModelEditorBase`. This commit added modifier keys for mouse wheel adjustments to the `Knob` class. The port to `FloatModelEditorBase` results in the bar control now also supporting different scales of adjustments that can be switched with the Shift, Ctrl and Alt keys. * Show current value on mouse over Integrate the changes of commit fcdf4c05684 into `FloatModelEditorBase`. This commit lets users show the current value of a float model when the mouse is moved over the control. * Make Knob inherit from FloatModelEditorBase Make `Knob` inherit from `FloatModelEditorBase`. This is mostly a continuation for the changes introduced with commit c63d86f. The idea is that `FloatModelEditorBase` contains the underlying functionality and logic to deal with float models. `Knob` and other classes then only override the presentation aspects. This way `Knob` and `BarModelEditor` can share the same functionality but can differ in how they present the data. Technical details ------------------ Remove all methods that are already defined in `FloatModelEditorBase` from `Knob`. These are all methods that are defined in the same way in `FloatModelEditorBase`. The method `paintEvent` is not removed because it is overridden by `Knob` as it has its own presentation. Remove forward declaration of `QPixmap` from `FloatModelEditorBase` as it was not used. Remove unused function `convertPixmapToGrayScaleTemp` from `FloatModelEditorBase`. * Cleanup includes in Knob class * Code style changes in FloatModelEditorBase `FloatModelEditorBase` is a new class/file. Therefore we can do some extensive code cleanup on the code that was initially copied from `Knob`. Adjust whitespace for methods and some if-statements. Remove underscores from parameter names. Use only two blank lines between method definitions. * Cleanup include for FloatModelEditorBase * Show effect name as title of dialog Add the effect name as the title in the content of the window. This improves the structure of the dialog as it is now clearer from the content itself to which effect the controls belong to. This duplicates the information from the window title. However, the window title is rather small and the larger font in the content makes it easier to find an effect in a set of opened dialogs. * Revert "Show effect name as title of dialog" This reverts commit 8ce0d366d0c0b7fbc629ebf0227a3ed155f91d5c. * Fix problem with LedCheckBox in grid layout The LedCheckBox does not play nice when being used in a grid layout as it disables the resizing behavior of every column it appears in. The solution is to wrap it into the layout of a parent widget that knows how to behave. * Reduce minimum width of BarModelEditor Reduce the minimum width of `BarModelEditor` from 200 to 50. --------- Co-authored-by: Hyunjin Song --- data/themes/classic/style.css | 5 + data/themes/default/style.css | 5 + include/BarModelEditor.h | 76 +++ include/FloatModelEditorBase.h | 121 +++++ include/Knob.h | 67 +-- include/LadspaControl.h | 2 + include/LedCheckBox.h | 11 +- include/TempoSyncBarModelEditor.h | 90 ++++ include/TempoSyncKnobModel.h | 3 + plugins/LadspaEffect/CMakeLists.txt | 2 +- plugins/LadspaEffect/LadspaControls.h | 4 +- .../LadspaMatrixControlDialog.cpp | 243 +++++++++ .../LadspaEffect/LadspaMatrixControlDialog.h | 91 ++++ plugins/LadspaEffect/LadspaWidgetFactory.cpp | 83 ++++ plugins/LadspaEffect/LadspaWidgetFactory.h | 49 ++ src/gui/CMakeLists.txt | 3 + src/gui/widgets/BarModelEditor.cpp | 117 +++++ src/gui/widgets/FloatModelEditorBase.cpp | 464 ++++++++++++++++++ src/gui/widgets/Knob.cpp | 422 +--------------- src/gui/widgets/LedCheckBox.cpp | 75 ++- src/gui/widgets/TempoSyncBarModelEditor.cpp | 302 ++++++++++++ 21 files changed, 1727 insertions(+), 508 deletions(-) create mode 100644 include/BarModelEditor.h create mode 100644 include/FloatModelEditorBase.h create mode 100644 include/TempoSyncBarModelEditor.h create mode 100644 plugins/LadspaEffect/LadspaMatrixControlDialog.cpp create mode 100644 plugins/LadspaEffect/LadspaMatrixControlDialog.h create mode 100644 plugins/LadspaEffect/LadspaWidgetFactory.cpp create mode 100644 plugins/LadspaEffect/LadspaWidgetFactory.h create mode 100644 src/gui/widgets/BarModelEditor.cpp create mode 100644 src/gui/widgets/FloatModelEditorBase.cpp create mode 100644 src/gui/widgets/TempoSyncBarModelEditor.cpp diff --git a/data/themes/classic/style.css b/data/themes/classic/style.css index 2b853395d..c4fe0e153 100644 --- a/data/themes/classic/style.css +++ b/data/themes/classic/style.css @@ -975,6 +975,11 @@ lmms--gui--CompressorControlDialog lmms--gui--Knob { qproperty-lineWidth: 2; } +lmms--gui--BarModelEditor { + qproperty-backgroundBrush: rgba(28, 73, 51, 255); + qproperty-barBrush: rgba(17, 136, 71, 255); +} + /* palette information */ lmms--gui--LmmsPalette { diff --git a/data/themes/default/style.css b/data/themes/default/style.css index 9cbf5885b..80d56a4bb 100644 --- a/data/themes/default/style.css +++ b/data/themes/default/style.css @@ -1019,6 +1019,11 @@ lmms--gui--CompressorControlDialog lmms--gui--Knob { qproperty-lineWidth: 2; } +lmms--gui--BarModelEditor { + qproperty-backgroundBrush: rgba(28, 73, 51, 255); + qproperty-barBrush: rgba(17, 136, 71, 255); +} + /* palette information */ lmms--gui--LmmsPalette { diff --git a/include/BarModelEditor.h b/include/BarModelEditor.h new file mode 100644 index 000000000..79a320a7d --- /dev/null +++ b/include/BarModelEditor.h @@ -0,0 +1,76 @@ +/* + * BarModelEditor.h - edit model values using a bar display + * + * Copyright (c) 2023-now 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. + * + */ + +#pragma once + +#ifndef LMMS_GUI_BAR_MODEL_EDITOR_H +#define LMMS_GUI_BAR_MODEL_EDITOR_H + +#include "FloatModelEditorBase.h" + + +namespace lmms::gui +{ + +class LMMS_EXPORT BarModelEditor : public FloatModelEditorBase +{ + Q_OBJECT + +public: + Q_PROPERTY(QBrush backgroundBrush READ getBackgroundBrush WRITE setBackgroundBrush) + Q_PROPERTY(QBrush barBrush READ getBarBrush WRITE setBarBrush) + Q_PROPERTY(QColor textColor READ getTextColor WRITE setTextColor) + + BarModelEditor(QString text, FloatModel * floatModel, QWidget * parent = nullptr); + + // Define how the widget will behave in a layout + QSizePolicy sizePolicy() const; + + QSize minimumSizeHint() const override; + + QSize sizeHint() const override; + + QBrush const & getBackgroundBrush() const; + void setBackgroundBrush(QBrush const & backgroundBrush); + + QBrush const & getBarBrush() const; + void setBarBrush(QBrush const & barBrush); + + QColor const & getTextColor() const; + void setTextColor(QColor const & textColor); + +protected: + void paintEvent(QPaintEvent *event) override; + +private: + QString const m_text; + + QBrush m_backgroundBrush; + QBrush m_barBrush; + QColor m_textColor; +}; + +} // namespace lmms::gui + +#endif // LMMS_GUI_BAR_MODEL_EDITOR_H diff --git a/include/FloatModelEditorBase.h b/include/FloatModelEditorBase.h new file mode 100644 index 000000000..72f1450de --- /dev/null +++ b/include/FloatModelEditorBase.h @@ -0,0 +1,121 @@ +/* + * FloatModelEditorBase.h - Base editor for float models + * + * Copyright (c) 2004-2008 Tobias Doerffel + * Copyright (c) 2023 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_FLOAT_MODEL_EDITOR_BASE_H +#define LMMS_GUI_FLOAT_MODEL_EDITOR_BASE_H + +#include +#include + +#include "AutomatableModelView.h" + + +namespace lmms::gui +{ + +class SimpleTextFloat; + +class LMMS_EXPORT FloatModelEditorBase : public QWidget, public FloatModelView +{ + Q_OBJECT + + mapPropertyFromModel(bool, isVolumeKnob, setVolumeKnob, m_volumeKnob); + mapPropertyFromModel(float, volumeRatio, setVolumeRatio, m_volumeRatio); + + void initUi(const QString & name); //!< to be called by ctors + +public: + enum class DirectionOfManipulation + { + Vertical, + Horizontal + }; + + FloatModelEditorBase(DirectionOfManipulation directionOfManipulation = DirectionOfManipulation::Vertical, QWidget * _parent = nullptr, const QString & _name = QString()); //!< default ctor + FloatModelEditorBase(const FloatModelEditorBase& other) = delete; + + // TODO: remove + inline void setHintText(const QString & txt_before, const QString & txt_after) + { + setDescription(txt_before); + setUnit(txt_after); + } + +signals: + void sliderPressed(); + void sliderReleased(); + void sliderMoved(float value); + + +protected: + void contextMenuEvent(QContextMenuEvent * me) override; + void dragEnterEvent(QDragEnterEvent * dee) override; + void dropEvent(QDropEvent * de) override; + void focusOutEvent(QFocusEvent * fe) override; + void mousePressEvent(QMouseEvent * me) override; + void mouseReleaseEvent(QMouseEvent * me) override; + void mouseMoveEvent(QMouseEvent * me) override; + void mouseDoubleClickEvent(QMouseEvent * me) override; + void paintEvent(QPaintEvent * me) override; + void wheelEvent(QWheelEvent * me) override; + + void enterEvent(QEvent *event) override; + void leaveEvent(QEvent *event) override; + + virtual float getValue(const QPoint & p); + +private slots: + virtual void enterValue(); + void friendlyUpdate(); + void toggleScale(); + +private: + virtual QString displayValue() const; + + void doConnections() override; + + void showTextFloat(int msecBeforeDisplay, int msecDisplayTime); + void setPosition(const QPoint & p); + + inline float pageSize() const + { + return (model()->maxValue() - model()->minValue()) / 100.0f; + } + + static SimpleTextFloat * s_textFloat; + + BoolModel m_volumeKnob; + FloatModel m_volumeRatio; + + QPoint m_lastMousePos; //!< mouse position in last mouseMoveEvent + float m_leftOver; + bool m_buttonPressed; + + DirectionOfManipulation m_directionOfManipulation; +}; + +} // namespace lmms::gui + +#endif // LMMS_GUI_FLOAT_MODEL_EDITOR_BASE_H diff --git a/include/Knob.h b/include/Knob.h index d5739bb1c..3c3339a6f 100644 --- a/include/Knob.h +++ b/include/Knob.h @@ -26,12 +26,9 @@ #define LMMS_GUI_KNOB_H #include -#include -#include -#include #include -#include "AutomatableModelView.h" +#include "FloatModelEditorBase.h" class QPixmap; @@ -50,7 +47,7 @@ enum class KnobType void convertPixmapToGrayScale(QPixmap &pixMap); -class LMMS_EXPORT Knob : public QWidget, public FloatModelView +class LMMS_EXPORT Knob : public FloatModelEditorBase { Q_OBJECT Q_ENUMS( KnobType ) @@ -72,9 +69,6 @@ class LMMS_EXPORT Knob : public QWidget, public FloatModelView Q_PROPERTY(QColor arcActiveColor MEMBER m_arcActiveColor) Q_PROPERTY(QColor arcInactiveColor MEMBER m_arcInactiveColor) - mapPropertyFromModel(bool,isVolumeKnob,setVolumeKnob,m_volumeKnob); - mapPropertyFromModel(float,volumeRatio,setVolumeRatio,m_volumeRatio); - Q_PROPERTY(KnobType knobNum READ knobNum WRITE setknobNum) Q_PROPERTY(QColor textColor READ textColor WRITE setTextColor) @@ -87,13 +81,6 @@ public: Knob( QWidget * _parent = nullptr, const QString & _name = QString() ); //!< default ctor Knob( const Knob& other ) = delete; - // TODO: remove - inline void setHintText( const QString & _txt_before, - const QString & _txt_after ) - { - setDescription( _txt_before ); - setUnit( _txt_after ); - } void setLabel( const QString & txt ); void setHtmlLabel( const QString &htmltxt ); @@ -125,46 +112,16 @@ public: void setTextColor( const QColor & c ); -signals: - void sliderPressed(); - void sliderReleased(); - void sliderMoved( float value ); - - protected: - void contextMenuEvent( QContextMenuEvent * _me ) override; - void dragEnterEvent( QDragEnterEvent * _dee ) override; - void dropEvent( QDropEvent * _de ) override; - void focusOutEvent( QFocusEvent * _fe ) override; - void mousePressEvent( QMouseEvent * _me ) override; - void mouseReleaseEvent( QMouseEvent * _me ) override; - void mouseMoveEvent( QMouseEvent * _me ) override; - void mouseDoubleClickEvent( QMouseEvent * _me ) override; void paintEvent( QPaintEvent * _me ) override; - void wheelEvent( QWheelEvent * _me ) override; + void changeEvent(QEvent * ev) override; - void enterEvent(QEvent *event) override; - void leaveEvent(QEvent *event) override; - - virtual float getValue( const QPoint & _p ); - -private slots: - virtual void enterValue(); - void friendlyUpdate(); - void toggleScale(); - private: - virtual QString displayValue() const; - - void doConnections() override; - QLineF calculateLine( const QPointF & _mid, float _radius, float _innerRadius = 1) const; void drawKnob( QPainter * _p ); - void showTextFloat(int msecBeforeDisplay, int msecDisplayTime); - void setPosition( const QPoint & _p ); bool updateAngle(); int angleFromValue( float value, float minValue, float maxValue, float totalAngle ) const @@ -172,25 +129,11 @@ private: return static_cast( ( value - 0.5 * ( minValue + maxValue ) ) / ( maxValue - minValue ) * m_totalAngle ) % 360; } - inline float pageSize() const - { - return ( model()->maxValue() - model()->minValue() ) / 100.0f; - } - - - static SimpleTextFloat * s_textFloat; - QString m_label; bool m_isHtmlLabel; QTextDocument* m_tdRenderer; std::unique_ptr m_knobPixmap; - BoolModel m_volumeKnob; - FloatModel m_volumeRatio; - - QPoint m_lastMousePos; //!< mouse position in last mouseMoveEvent - float m_leftOver; - bool m_buttonPressed; float m_totalAngle; int m_angle; @@ -211,9 +154,7 @@ private: QColor m_textColor; KnobType m_knobNum; - -} ; - +}; } // namespace lmms::gui diff --git a/include/LadspaControl.h b/include/LadspaControl.h index e4f0cd745..8af8f9923 100644 --- a/include/LadspaControl.h +++ b/include/LadspaControl.h @@ -41,6 +41,7 @@ namespace gui { class LadspaControlView; +class LadspaMatrixControlDialog; } // namespace gui @@ -125,6 +126,7 @@ private: friend class gui::LadspaControlView; + friend class gui::LadspaMatrixControlDialog; } ; diff --git a/include/LedCheckBox.h b/include/LedCheckBox.h index e3629e143..aaafffaa1 100644 --- a/include/LedCheckBox.h +++ b/include/LedCheckBox.h @@ -47,10 +47,12 @@ public: LedCheckBox( const QString & _txt, QWidget * _parent, const QString & _name = QString(), - LedColor _color = LedColor::Yellow ); + LedColor _color = LedColor::Yellow, + bool legacyMode = true); LedCheckBox( QWidget * _parent, const QString & _name = QString(), - LedColor _color = LedColor::Yellow ); + LedColor _color = LedColor::Yellow, + bool legacyMode = true); ~LedCheckBox() override; @@ -74,8 +76,13 @@ private: QString m_text; + bool m_legacyMode; + void initUi( LedColor _color ); //!< to be called by ctors + void onTextUpdated(); //!< to be called when you updated @a m_text + void paintLegacy(QPaintEvent * p); + void paintNonLegacy(QPaintEvent * p); } ; diff --git a/include/TempoSyncBarModelEditor.h b/include/TempoSyncBarModelEditor.h new file mode 100644 index 000000000..c1b0bb26f --- /dev/null +++ b/include/TempoSyncBarModelEditor.h @@ -0,0 +1,90 @@ +/* + * TempoSyncBarModelEditor.h - adds bpm to ms conversion for the bar editor class + * + * Copyright (c) 2005-2008 Danny McRae + * Copyright (c) 2009-2014 Tobias Doerffel + * Copyright (c) 2023 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_TEMPO_SYNC_BAR_MODEL_EDITOR_H +#define LMMS_GUI_TEMPO_SYNC_BAR_MODEL_EDITOR_H + +#include +#include + +#include "BarModelEditor.h" +#include "TempoSyncKnobModel.h" + +namespace lmms::gui +{ + +class MeterDialog; + +class LMMS_EXPORT TempoSyncBarModelEditor : public BarModelEditor +{ + Q_OBJECT +public: + TempoSyncBarModelEditor(QString text, FloatModel * floatModel, QWidget * parent = nullptr); + ~TempoSyncBarModelEditor() override; + + const QString & syncDescription(); + void setSyncDescription(const QString & new_description); + + const QPixmap & syncIcon(); + void setSyncIcon(const QPixmap & new_pix); + + TempoSyncKnobModel * model() + { + return castModel(); + } + + void modelChanged() override; + + +signals: + void syncDescriptionChanged(const QString & new_description); + void syncIconChanged(); + + +protected: + void contextMenuEvent(QContextMenuEvent * me) override; + + +protected slots: + void updateDescAndIcon(); + void showCustom(); + + +private: + void updateTextDescription(); + void updateIcon(); + + +private: + QPixmap m_tempoSyncIcon; + QString m_tempoSyncDescription; + + QPointer m_custom; +}; + +} // namespace lmms::gui + +#endif // LMMS_GUI_TEMPO_SYNC_BAR_MODEL_EDITOR_H diff --git a/include/TempoSyncKnobModel.h b/include/TempoSyncKnobModel.h index 5cd2db067..af92a58aa 100644 --- a/include/TempoSyncKnobModel.h +++ b/include/TempoSyncKnobModel.h @@ -82,6 +82,9 @@ public: void setScale( float _new_scale ); + MeterModel & getCustomMeterModel() { return m_custom; } + MeterModel const & getCustomMeterModel() const { return m_custom; } + signals: void syncModeChanged( lmms::TempoSyncKnobModel::SyncMode _new_mode ); void scaleChanged( float _new_scale ); diff --git a/plugins/LadspaEffect/CMakeLists.txt b/plugins/LadspaEffect/CMakeLists.txt index a01eb950f..202a8dd04 100644 --- a/plugins/LadspaEffect/CMakeLists.txt +++ b/plugins/LadspaEffect/CMakeLists.txt @@ -1,6 +1,6 @@ INCLUDE(BuildPlugin) -BUILD_PLUGIN(ladspaeffect LadspaEffect.cpp LadspaControls.cpp LadspaControlDialog.cpp LadspaSubPluginFeatures.cpp LadspaEffect.h LadspaControls.h LadspaControlDialog.h LadspaSubPluginFeatures.h MOCFILES LadspaEffect.h LadspaControls.h LadspaControlDialog.h EMBEDDED_RESOURCES logo.png) +BUILD_PLUGIN(ladspaeffect LadspaEffect.cpp LadspaControls.cpp LadspaControlDialog.cpp LadspaMatrixControlDialog.cpp LadspaSubPluginFeatures.cpp LadspaWidgetFactory.cpp LadspaEffect.h LadspaControls.h LadspaControlDialog.h LadspaMatrixControlDialog.h LadspaSubPluginFeatures.h LadspaWidgetFactory.h MOCFILES LadspaEffect.h LadspaControls.h LadspaControlDialog.h LadspaMatrixControlDialog.h EMBEDDED_RESOURCES logo.png) SET(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/ladspa") diff --git a/plugins/LadspaEffect/LadspaControls.h b/plugins/LadspaEffect/LadspaControls.h index 2bef0c856..c91f3badd 100644 --- a/plugins/LadspaEffect/LadspaControls.h +++ b/plugins/LadspaEffect/LadspaControls.h @@ -27,6 +27,7 @@ #include "EffectControls.h" #include "LadspaControlDialog.h" +#include "LadspaMatrixControlDialog.h" namespace lmms { @@ -59,7 +60,7 @@ public: gui::EffectControlDialog* createView() override { - return new gui::LadspaControlDialog( this ); + return new gui::LadspaMatrixControlDialog( this ); } @@ -79,6 +80,7 @@ private: friend class gui::LadspaControlDialog; + friend class gui::LadspaMatrixControlDialog; friend class LadspaEffect; diff --git a/plugins/LadspaEffect/LadspaMatrixControlDialog.cpp b/plugins/LadspaEffect/LadspaMatrixControlDialog.cpp new file mode 100644 index 000000000..88810cee6 --- /dev/null +++ b/plugins/LadspaEffect/LadspaMatrixControlDialog.cpp @@ -0,0 +1,243 @@ +/* + * LadspaMatrixControlDialog.h - Dialog for displaying and editing control port + * values for LADSPA plugins in a matrix display + * + * Copyright (c) 2015 Michael Gregorius + * + * This file is part of LMMS - http://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 +#include +#include +#include +#include +#include +#include + + +#include "LadspaBase.h" +#include "LadspaControl.h" +#include "LadspaEffect.h" +#include "LadspaMatrixControlDialog.h" +#include "LadspaWidgetFactory.h" +#include "LadspaControlView.h" +#include "LedCheckBox.h" + +#include "GuiApplication.h" +#include "MainWindow.h" + + +namespace lmms::gui +{ + +LadspaMatrixControlDialog::LadspaMatrixControlDialog(LadspaControls * ladspaControls) : + EffectControlDialog(ladspaControls), + m_scrollArea(nullptr), + m_stereoLink(nullptr) +{ + QVBoxLayout * mainLayout = new QVBoxLayout(this); + + m_scrollArea = new QScrollArea(this); + m_scrollArea->setWidgetResizable(true); + m_scrollArea->setFrameShape(QFrame::NoFrame); + // Set to always on so that the elements do not move around when the + // scroll bar is hidden or shown. + m_scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn); + + // Add a scroll area that grows + mainLayout->addWidget(m_scrollArea, 1); + + // Populate the parameter matrix and put it into the scroll area + updateEffectView(ladspaControls); + + // Add button to link all channels if there's more than one channel + if (getChannelCount() > 1) + { + mainLayout->addSpacing(3); + + m_stereoLink = new LedCheckBox(tr("Link Channels"), this, QString(), LedCheckBox::LedColor::Green, false); + m_stereoLink->setModel(&ladspaControls->m_stereoLinkModel); + mainLayout->addWidget(m_stereoLink, 0, Qt::AlignCenter); + } +} + +bool LadspaMatrixControlDialog::isResizable() const +{ + return true; +} + +bool LadspaMatrixControlDialog::needsLinkColumn() const +{ + LadspaControls * ladspaControls = getLadspaControls(); + + ch_cnt_t const channelCount = getChannelCount(); + for (ch_cnt_t i = 0; i < channelCount; ++i) + { + // Create a const reference so that the C++11 based for loop does not detach the Qt container + auto const & currentControls = ladspaControls->m_controls[i]; + for (auto ladspaControl : currentControls) + { + if (ladspaControl->m_link) + { + return true; + } + } + } + + return false; +} + +void LadspaMatrixControlDialog::arrangeControls(QWidget * parent, QGridLayout* gridLayout) +{ + LadspaControls * ladspaControls = getLadspaControls(); + + int const headerRow = 0; + int const linkColumn = 0; + + bool const linkColumnNeeded = needsLinkColumn(); + if (linkColumnNeeded) + { + gridLayout->addWidget(new QLabel("" + tr("Link") + "", parent), headerRow, linkColumn, Qt::AlignHCenter); + + // If there's a link column then it should not stretch + gridLayout->setColumnStretch(linkColumn, 0); + } + + int const channelStartColumn = linkColumnNeeded ? 1 : 0; + + // The header row should not grow vertically + gridLayout->setRowStretch(0, 0); + + // Records the maximum row with parameters so that we can add a vertical spacer after that row + int maxRow = 0; + + // Iterate the channels and add widgets for each control + // Note: the code assumes that all channels have the same structure, i.e. that all channels + // have the same number of parameters which are in the same order. + ch_cnt_t const numberOfChannels = getChannelCount(); + for (ch_cnt_t i = 0; i < numberOfChannels; ++i) + { + int currentChannelColumn = channelStartColumn + i; + gridLayout->setColumnStretch(currentChannelColumn, 1); + + // First add the channel header with the channel number + gridLayout->addWidget(new QLabel("" + tr("Channel %1").arg(QString::number(i + 1)) + "", parent), headerRow, currentChannelColumn, Qt::AlignHCenter); + + int currentRow = 1; + + if (i == 0) + { + // Configure the current parameter row to not stretch. + // Only do this once, i.e. when working with the first channel. + gridLayout->setRowStretch(currentRow, 0); + } + + // Create a const reference so that the C++11 based for loop does not detach the Qt container + auto const & currentControls = ladspaControls->m_controls[i]; + for (auto ladspaControl : currentControls) + { + // Only use the first channel to determine if we need to add link controls + if (i == 0 && ladspaControl->m_link) + { + LedCheckBox * linkCheckBox = new LedCheckBox("", parent, "", LedCheckBox::LedColor::Green); + linkCheckBox->setModel(&ladspaControl->m_linkEnabledModel); + linkCheckBox->setToolTip(tr("Link channels")); + gridLayout->addWidget(linkCheckBox, currentRow, linkColumn, Qt::AlignHCenter); + } + + QWidget * controlWidget = LadspaWidgetFactory::createWidget(ladspaControl, this); + if (controlWidget) + { + gridLayout->addWidget(controlWidget, currentRow, currentChannelColumn); + } + + // Record the maximum row so that we add a vertical spacer after that row + maxRow = std::max(maxRow, currentRow); + + ++currentRow; + } + } + + // Add a spacer item after the maximum row + QSpacerItem * spacer = new QSpacerItem(0, 0, QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); + gridLayout->addItem(spacer, maxRow + 1, 0); +} + +QWidget * LadspaMatrixControlDialog::createMatrixWidget() +{ + QWidget *widget = new QWidget(this); + QGridLayout *gridLayout = new QGridLayout(widget); + gridLayout->setMargin(0); + widget->setLayout(gridLayout); + + arrangeControls(widget, gridLayout); + + return widget; +} + +void LadspaMatrixControlDialog::updateEffectView(LadspaControls * ladspaControls) +{ + m_effectControls = ladspaControls; + + // No need to delete the existing widget as it's deleted + // by the scroll view when we replace it. + QWidget * matrixWidget = createMatrixWidget(); + m_scrollArea->setWidget(matrixWidget); + + // Make sure that the horizontal scroll bar does not show + // From: https://forum.qt.io/topic/13374/solved-qscrollarea-vertical-scroll-only/4 + m_scrollArea->setMinimumWidth(matrixWidget->minimumSizeHint().width() + m_scrollArea->verticalScrollBar()->width()); + + + // Make sure that the widget is shown without a scrollbar whenever possible + // If the widget fits on the workspace we use the height of the widget as the minimum size of the scroll area. + // This will ensure that the scrollbar is not shown initially (and never will be). + // If the widget is larger than the workspace then we want it to mostly cover the workspace. + // + // This is somewhat ugly but I have no idea how to control the initial size of the scroll area otherwise + auto const workspaceSize = getGUI()->mainWindow()->workspace()->viewport()->size(); + // Make sure that we always account a minumum height for the workspace, i.e. that we never compute + // something close to 0 if the LMMS window is very small + int workspaceHeight = qMax(200, static_cast(workspaceSize.height() * 0.9)); + int minOfWidgetAndWorkspace = qMin(matrixWidget->minimumSizeHint().height(), workspaceHeight); + m_scrollArea->setMinimumHeight(minOfWidgetAndWorkspace); + + if (getChannelCount() > 1 && m_stereoLink != nullptr) + { + m_stereoLink->setModel(&ladspaControls->m_stereoLinkModel); + } + + connect(ladspaControls, &LadspaControls::effectModelChanged, + this, &LadspaMatrixControlDialog::updateEffectView, + Qt::DirectConnection); +} + +LadspaControls * LadspaMatrixControlDialog::getLadspaControls() const +{ + return dynamic_cast(m_effectControls); +} + +ch_cnt_t LadspaMatrixControlDialog::getChannelCount() const +{ + return getLadspaControls()->m_processors; +} + +} // namespace lmms::gui diff --git a/plugins/LadspaEffect/LadspaMatrixControlDialog.h b/plugins/LadspaEffect/LadspaMatrixControlDialog.h new file mode 100644 index 000000000..c5949fa15 --- /dev/null +++ b/plugins/LadspaEffect/LadspaMatrixControlDialog.h @@ -0,0 +1,91 @@ +/* + * LadspaMatrixControlDialog.h - Dialog for displaying and editing control port + * values for LADSPA plugins in a matrix display + * + * Copyright (c) 2015 Michael Gregorius + * + * This file is part of LMMS - http://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 LADSPA_MATRIX_CONTROL_DIALOG_H +#define LADSPA_MATRIX_CONTROL_DIALOG_H + +#include "EffectControlDialog.h" + +#include "lmms_basics.h" + + +class QGridLayout; +class QScrollArea; + +namespace lmms +{ + +class LadspaControls; + +namespace gui +{ + +class LedCheckBox; + + +class LadspaMatrixControlDialog : public EffectControlDialog +{ + Q_OBJECT +public: + LadspaMatrixControlDialog(LadspaControls* ctl); + bool isResizable() const override; + +private slots: + void updateEffectView(LadspaControls* ctl); + +private: + /** + * @brief Checks if a link column is needed for the current effect controls. + * @return true if a link column is needed. + */ + bool needsLinkColumn() const; + + /** + * @brief Arranges widgets for the current controls in a grid/matrix layout. + * @param parent The parent of all created widgets + * @param gridLayout The layout into which the controls are organized + */ + void arrangeControls(QWidget * parent, QGridLayout* gridLayout); + + /** + * @brief Creates a widget that holds the widgets of the current controls in a matrix arrangement. + * @param ladspaControls + * @return + */ + QWidget * createMatrixWidget(); + + LadspaControls * getLadspaControls() const; + ch_cnt_t getChannelCount() const; + +private: + QScrollArea* m_scrollArea; + LedCheckBox* m_stereoLink; +}; + +} // namespace gui + +} // namespace lmms + +#endif diff --git a/plugins/LadspaEffect/LadspaWidgetFactory.cpp b/plugins/LadspaEffect/LadspaWidgetFactory.cpp new file mode 100644 index 000000000..0491fd661 --- /dev/null +++ b/plugins/LadspaEffect/LadspaWidgetFactory.cpp @@ -0,0 +1,83 @@ +/* + * LadspaWidgetFactory.cpp - Factory that creates widgets for LADSPA ports + * + * Copyright (c) 2006-2008 Danny McRae + * Copyright (c) 2009 Tobias Doerffel + * Copyright (c) 2015-2023 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 "LadspaWidgetFactory.h" + +#include "LadspaControl.h" +#include "LadspaBase.h" + +#include "BarModelEditor.h" +#include "LedCheckBox.h" +#include "TempoSyncBarModelEditor.h" + +#include +#include + + +namespace lmms::gui +{ + +QWidget * LadspaWidgetFactory::createWidget(LadspaControl * ladspaControl, QWidget * parent) +{ + auto const * port = ladspaControl->port(); + + QString const name = port->name; + + switch (port->data_type) + { + case BufferDataType::Toggled: + { + // The actual check box is put into a widget because LedCheckBox does not play nice with layouts. + // Putting it directly into a grid layout disables the resizing behavior of all columns where it + // appears. Hence we put it into the layout of a widget that knows how to play nice with layouts. + QWidget * widgetWithLayout = new QWidget(parent); + QHBoxLayout * layout = new QHBoxLayout(widgetWithLayout); + layout->setContentsMargins(0, 0, 0, 0); + LedCheckBox * toggle = new LedCheckBox( + name, parent, QString(), LedCheckBox::LedColor::Green, false); + toggle->setModel(ladspaControl->toggledModel()); + layout->addWidget(toggle, 0, Qt::AlignLeft); + + return widgetWithLayout; + } + + case BufferDataType::Integer: + case BufferDataType::Enum: + case BufferDataType::Floating: + return new BarModelEditor(name, ladspaControl->knobModel(), parent); + + case BufferDataType::Time: + return new TempoSyncBarModelEditor(name, ladspaControl->tempoSyncKnobModel(), parent); + + default: + return new QLabel(QObject::tr("%1 (unsupported)").arg(name), parent); + } + + return nullptr; +} + +} // namespace lmms::gui diff --git a/plugins/LadspaEffect/LadspaWidgetFactory.h b/plugins/LadspaEffect/LadspaWidgetFactory.h new file mode 100644 index 000000000..807334d32 --- /dev/null +++ b/plugins/LadspaEffect/LadspaWidgetFactory.h @@ -0,0 +1,49 @@ +/* + * LadspaWidgetFactory.h - Factory that creates widgets for LADSPA ports + * + * Copyright (c) 2023 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_LADSPA_WIDGET_FACTORY_H +#define LMMS_GUI_LADSPA_WIDGET_FACTORY_H + + +class QWidget; + +namespace lmms +{ + +class LadspaControl; + +namespace gui +{ + +class LadspaWidgetFactory +{ +public: + static QWidget * createWidget(LadspaControl * ladspaControl, QWidget * parent); +}; + +} // namespace gui + +} // namespace lmms + +#endif // LMMS_GUI_LADSPA_WIDGET_FACTORY_H diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index afed153f9..1e809e9d7 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -94,11 +94,13 @@ SET(LMMS_SRCS gui/widgets/AutomatableButton.cpp gui/widgets/AutomatableSlider.cpp + gui/widgets/BarModelEditor.cpp gui/widgets/CPULoadWidget.cpp gui/widgets/CaptionMenu.cpp gui/widgets/ComboBox.cpp gui/widgets/CustomTextKnob.cpp gui/widgets/Fader.cpp + gui/widgets/FloatModelEditorBase.cpp gui/widgets/Graph.cpp gui/widgets/GroupBox.cpp gui/widgets/Knob.cpp @@ -115,6 +117,7 @@ SET(LMMS_SRCS gui/widgets/SimpleTextFloat.cpp gui/widgets/TabBar.cpp gui/widgets/TabWidget.cpp + gui/widgets/TempoSyncBarModelEditor.cpp gui/widgets/TempoSyncKnob.cpp gui/widgets/TextFloat.cpp gui/widgets/TimeDisplayWidget.cpp diff --git a/src/gui/widgets/BarModelEditor.cpp b/src/gui/widgets/BarModelEditor.cpp new file mode 100644 index 000000000..4b02c9634 --- /dev/null +++ b/src/gui/widgets/BarModelEditor.cpp @@ -0,0 +1,117 @@ +#include + +#include +#include + + +namespace lmms::gui +{ + +BarModelEditor::BarModelEditor(QString text, FloatModel * floatModel, QWidget * parent) : + FloatModelEditorBase(DirectionOfManipulation::Horizontal, parent), + m_text(text), + m_backgroundBrush(palette().base()), + m_barBrush(palette().button()), + m_textColor(palette().text().color()) +{ + setModel(floatModel); +} + +QSizePolicy BarModelEditor::sizePolicy() const +{ + return QSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); +} + +QSize BarModelEditor::minimumSizeHint() const +{ + auto const fm = fontMetrics(); + return QSize(50, fm.height() + 6); +} + +QSize BarModelEditor::sizeHint() const +{ + return minimumSizeHint(); +} + +QBrush const & BarModelEditor::getBackgroundBrush() const +{ + return m_backgroundBrush; +} + +void BarModelEditor::setBackgroundBrush(QBrush const & backgroundBrush) +{ + m_backgroundBrush = backgroundBrush; +} + +QBrush const & BarModelEditor::getBarBrush() const +{ + return m_barBrush; +} + +void BarModelEditor::setBarBrush(QBrush const & barBrush) +{ + m_barBrush = barBrush; +} + +QColor const & BarModelEditor::getTextColor() const +{ + return m_textColor; +} + +void BarModelEditor::setTextColor(QColor const & textColor) +{ + m_textColor = textColor; +} + +void BarModelEditor::paintEvent(QPaintEvent *event) +{ + QWidget::paintEvent(event); + + auto const * mod = model(); + auto const minValue = mod->minValue(); + auto const maxValue = mod->maxValue(); + auto const range = maxValue - minValue; + + QRect const r = rect(); + + QPainter painter(this); + + // Paint the base rectangle into which the bar and the text go + QBrush const & backgroundBrush = getBackgroundBrush(); + painter.setPen(backgroundBrush.color()); + painter.setBrush(backgroundBrush); + painter.drawRect(r); + + + // Paint the bar + // Compute the percentage as: + // min + x * (max - min) = v <=> x = (v - min) / (max - min) + auto const percentage = range == 0 ? 1. : (mod->value() - minValue) / range; + + int const margin = 3; + QMargins const margins(margin, margin, margin, margin); + QRect const valueRect = r.marginsRemoved(margins); + + QBrush const & barBrush = getBarBrush(); + painter.setPen(barBrush.color()); + painter.setBrush(barBrush); + QPoint const startPoint = valueRect.topLeft(); + QPoint endPoint = valueRect.bottomRight(); + endPoint.setX(startPoint.x() + percentage * (endPoint.x() - startPoint.x())); + + painter.drawRect(QRect(startPoint, endPoint)); + + + // Draw the text into the value rectangle but move it slightly to the right + QRect const textRect = valueRect.marginsRemoved(QMargins(3, 0, 0, 0)); + + // Elide the text if needed + auto const fm = fontMetrics(); + QString const elidedText = fm.elidedText(m_text, Qt::ElideRight, textRect.width()); + + // Now draw the text + painter.setPen(getTextColor()); + painter.drawText(textRect, elidedText); +} + +} // namespace lmms::gui diff --git a/src/gui/widgets/FloatModelEditorBase.cpp b/src/gui/widgets/FloatModelEditorBase.cpp new file mode 100644 index 000000000..7421908e2 --- /dev/null +++ b/src/gui/widgets/FloatModelEditorBase.cpp @@ -0,0 +1,464 @@ +/* + * FloatModelEditorBase.cpp - Base editor for float models + * + * Copyright (c) 2004-2014 Tobias Doerffel + * Copyright (c) 2023 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 "FloatModelEditorBase.h" + +#include +#include +#include + +#ifndef __USE_XOPEN +#define __USE_XOPEN +#endif + +#include "lmms_math.h" +#include "CaptionMenu.h" +#include "ControllerConnection.h" +#include "GuiApplication.h" +#include "LocaleHelper.h" +#include "MainWindow.h" +#include "ProjectJournal.h" +#include "SimpleTextFloat.h" +#include "StringPairDrag.h" + + +namespace lmms::gui +{ + +SimpleTextFloat * FloatModelEditorBase::s_textFloat = nullptr; + +FloatModelEditorBase::FloatModelEditorBase(DirectionOfManipulation directionOfManipulation, QWidget * parent, const QString & name) : + QWidget(parent), + FloatModelView(new FloatModel(0, 0, 0, 1, nullptr, name, true), this), + m_volumeKnob(false), + m_volumeRatio(100.0, 0.0, 1000000.0), + m_buttonPressed(false), + m_directionOfManipulation(directionOfManipulation) +{ + initUi(name); +} + + +void FloatModelEditorBase::initUi(const QString & name) +{ + if (s_textFloat == nullptr) + { + s_textFloat = new SimpleTextFloat; + } + + setWindowTitle(name); + + setFocusPolicy(Qt::ClickFocus); + + doConnections(); +} + + +void FloatModelEditorBase::showTextFloat(int msecBeforeDisplay, int msecDisplayTime) +{ + s_textFloat->setText(displayValue()); + s_textFloat->moveGlobal(this, QPoint(width() + 2, 0)); + s_textFloat->showWithDelay(msecBeforeDisplay, msecDisplayTime); +} + + +float FloatModelEditorBase::getValue(const QPoint & p) +{ + // Find out which direction/coordinate is relevant for this control + int const coordinate = m_directionOfManipulation == DirectionOfManipulation::Vertical ? p.y() : -p.x(); + + // knob value increase is linear to mouse movement + float value = .4f * coordinate; + + // if shift pressed we want slower movement + if (getGUI()->mainWindow()->isShiftPressed()) + { + value /= 4.0f; + value = qBound(-4.0f, value, 4.0f); + } + + return value * pageSize(); +} + + +void FloatModelEditorBase::contextMenuEvent(QContextMenuEvent *) +{ + // for the case, the user clicked right while pressing left mouse- + // button, the context-menu appears while mouse-cursor is still hidden + // and it isn't shown again until user does something which causes + // an QApplication::restoreOverrideCursor()-call... + mouseReleaseEvent(nullptr); + + CaptionMenu contextMenu(model()->displayName(), this); + addDefaultActions(&contextMenu); + contextMenu.addAction(QPixmap(), + model()->isScaleLogarithmic() ? tr("Set linear") : tr("Set logarithmic"), + this, SLOT(toggleScale())); + contextMenu.addSeparator(); + contextMenu.exec(QCursor::pos()); +} + + +void FloatModelEditorBase::toggleScale() +{ + model()->setScaleLogarithmic(! model()->isScaleLogarithmic()); + update(); +} + + +void FloatModelEditorBase::dragEnterEvent(QDragEnterEvent * dee) +{ + StringPairDrag::processDragEnterEvent(dee, "float_value," + "automatable_model"); +} + + +void FloatModelEditorBase::dropEvent(QDropEvent * de) +{ + QString type = StringPairDrag::decodeKey(de); + QString val = StringPairDrag::decodeValue(de); + if (type == "float_value") + { + model()->setValue(LocaleHelper::toFloat(val)); + de->accept(); + } + else if (type == "automatable_model") + { + auto mod = dynamic_cast(Engine::projectJournal()->journallingObject(val.toInt())); + if (mod != nullptr) + { + AutomatableModel::linkModels(model(), mod); + mod->setValue(model()->value()); + } + } +} + + +void FloatModelEditorBase::mousePressEvent(QMouseEvent * me) +{ + if (me->button() == Qt::LeftButton && + ! (me->modifiers() & Qt::ControlModifier) && + ! (me->modifiers() & Qt::ShiftModifier)) + { + AutomatableModel *thisModel = model(); + if (thisModel) + { + thisModel->addJournalCheckPoint(); + thisModel->saveJournallingState(false); + } + + const QPoint & p = me->pos(); + m_lastMousePos = p; + m_leftOver = 0.0f; + + emit sliderPressed(); + + showTextFloat(0, 0); + + s_textFloat->setText(displayValue()); + s_textFloat->moveGlobal(this, + QPoint(width() + 2, 0)); + s_textFloat->show(); + m_buttonPressed = true; + } + else if (me->button() == Qt::LeftButton && + (me->modifiers() & Qt::ShiftModifier)) + { + new StringPairDrag("float_value", + QString::number(model()->value()), + QPixmap(), this); + } + else + { + FloatModelView::mousePressEvent(me); + } +} + + +void FloatModelEditorBase::mouseMoveEvent(QMouseEvent * me) +{ + if (m_buttonPressed && me->pos() != m_lastMousePos) + { + // knob position is changed depending on last mouse position + setPosition(me->pos() - m_lastMousePos); + emit sliderMoved(model()->value()); + // original position for next time is current position + m_lastMousePos = me->pos(); + } + s_textFloat->setText(displayValue()); + s_textFloat->show(); +} + + +void FloatModelEditorBase::mouseReleaseEvent(QMouseEvent* event) +{ + if (event && event->button() == Qt::LeftButton) + { + AutomatableModel *thisModel = model(); + if (thisModel) + { + thisModel->restoreJournallingState(); + } + } + + m_buttonPressed = false; + + emit sliderReleased(); + + QApplication::restoreOverrideCursor(); + + s_textFloat->hide(); +} + + +void FloatModelEditorBase::enterEvent(QEvent *event) +{ + showTextFloat(700, 2000); +} + + +void FloatModelEditorBase::leaveEvent(QEvent *event) +{ + s_textFloat->hide(); +} + + +void FloatModelEditorBase::focusOutEvent(QFocusEvent * fe) +{ + // make sure we don't loose mouse release event + mouseReleaseEvent(nullptr); + QWidget::focusOutEvent(fe); +} + + +void FloatModelEditorBase::mouseDoubleClickEvent(QMouseEvent *) +{ + enterValue(); +} + + +void FloatModelEditorBase::paintEvent(QPaintEvent *) +{ + QPainter p(this); + + QColor const foreground(3, 94, 97); + + auto const * mod = model(); + auto const minValue = mod->minValue(); + auto const maxValue = mod->maxValue(); + auto const range = maxValue - minValue; + + // Compute the percentage + // min + x * (max - min) = v <=> x = (v - min) / (max - min) + auto const percentage = range == 0 ? 1. : (mod->value() - minValue) / range; + + QRect r = rect(); + p.setPen(foreground); + p.setBrush(foreground); + p.drawRect(QRect(r.topLeft(), QPoint(r.width() * percentage, r.height()))); +} + + +void FloatModelEditorBase::wheelEvent(QWheelEvent * we) +{ + we->accept(); + const int deltaY = we->angleDelta().y(); + float direction = deltaY > 0 ? 1 : -1; + + auto * m = model(); + float const step = m->step(); + float const range = m->range(); + + // This is the default number of steps or mouse wheel events that it takes to sweep + // from the lowest value to the highest value. + // It might be modified if the user presses modifier keys. See below. + float numberOfStepsForFullSweep = 100.; + + auto const modKeys = we->modifiers(); + if (modKeys == Qt::ShiftModifier) + { + // The shift is intended to go through the values in very coarse steps as in: + // "Shift into overdrive" + numberOfStepsForFullSweep = 10; + } + else if (modKeys == Qt::ControlModifier) + { + // The control key gives more control, i.e. it enables more fine-grained adjustments + numberOfStepsForFullSweep = 1000; + } + else if (modKeys == Qt::AltModifier) + { + // The alt key enables even finer adjustments + numberOfStepsForFullSweep = 2000; + + // It seems that on some systems pressing Alt with mess with the directions, + // i.e. scrolling the mouse wheel is interpreted as pressing the mouse wheel + // left and right. Account for this quirk. + if (deltaY == 0) + { + int const deltaX = we->angleDelta().x(); + if (deltaX != 0) + { + direction = deltaX > 0 ? 1 : -1; + } + } + } + + // Compute the number of steps but make sure that we always do at least one step + const float stepMult = std::max(range / numberOfStepsForFullSweep / step, 1.f); + const int inc = direction * stepMult; + model()->incValue(inc); + + s_textFloat->setText(displayValue()); + s_textFloat->moveGlobal(this, QPoint(width() + 2, 0)); + s_textFloat->setVisibilityTimeOut(1000); + + emit sliderMoved(model()->value()); +} + + +void FloatModelEditorBase::setPosition(const QPoint & p) +{ + const float value = getValue(p) + m_leftOver; + const auto step = model()->step(); + const float oldValue = model()->value(); + + if (model()->isScaleLogarithmic()) // logarithmic code + { + const float pos = model()->minValue() < 0 + ? oldValue / qMax(qAbs(model()->maxValue()), qAbs(model()->minValue())) + : (oldValue - model()->minValue()) / model()->range(); + const float ratio = 0.1f + qAbs(pos) * 15.f; + float newValue = value * ratio; + if (qAbs(newValue) >= step) + { + float roundedValue = qRound((oldValue - value) / step) * step; + model()->setValue(roundedValue); + m_leftOver = 0.0f; + } + else + { + m_leftOver = value; + } + } + + else // linear code + { + if (qAbs(value) >= step) + { + float roundedValue = qRound((oldValue - value) / step) * step; + model()->setValue(roundedValue); + m_leftOver = 0.0f; + } + else + { + m_leftOver = value; + } + } +} + + +void FloatModelEditorBase::enterValue() +{ + bool ok; + float new_val; + + if (isVolumeKnob() && + ConfigManager::inst()->value("app", "displaydbfs").toInt()) + { + new_val = QInputDialog::getDouble( + this, tr("Set value"), + tr("Please enter a new value between " + "-96.0 dBFS and 6.0 dBFS:"), + ampToDbfs(model()->getRoundedValue() / 100.0), + -96.0, 6.0, model()->getDigitCount(), &ok); + if (new_val <= -96.0) + { + new_val = 0.0f; + } + else + { + new_val = dbfsToAmp(new_val) * 100.0; + } + } + else + { + new_val = QInputDialog::getDouble( + this, tr("Set value"), + tr("Please enter a new value between " + "%1 and %2:"). + arg(model()->minValue()). + arg(model()->maxValue()), + model()->getRoundedValue(), + model()->minValue(), + model()->maxValue(), model()->getDigitCount(), &ok); + } + + if (ok) + { + model()->setValue(new_val); + } +} + + +void FloatModelEditorBase::friendlyUpdate() +{ + if (model() && (model()->controllerConnection() == nullptr || + model()->controllerConnection()->getController()->frequentUpdates() == false || + Controller::runningFrames() % (256*4) == 0)) + { + update(); + } +} + + +QString FloatModelEditorBase::displayValue() const +{ + if (isVolumeKnob() && + ConfigManager::inst()->value("app", "displaydbfs").toInt()) + { + return m_description.trimmed() + QString(" %1 dBFS"). + arg(ampToDbfs(model()->getRoundedValue() / volumeRatio()), + 3, 'f', 2); + } + + return m_description.trimmed() + QString(" %1"). + arg(model()->getRoundedValue()) + m_unit; +} + + +void FloatModelEditorBase::doConnections() +{ + if (model() != nullptr) + { + QObject::connect(model(), SIGNAL(dataChanged()), + this, SLOT(friendlyUpdate())); + + QObject::connect(model(), SIGNAL(propertiesChanged()), + this, SLOT(update())); + } +} + +} // namespace lmms::gui diff --git a/src/gui/widgets/Knob.cpp b/src/gui/widgets/Knob.cpp index 56cf29345..00a9363c8 100644 --- a/src/gui/widgets/Knob.cpp +++ b/src/gui/widgets/Knob.cpp @@ -24,11 +24,6 @@ #include "Knob.h" -#include -#include -#include -#include -#include #include #ifndef __USE_XOPEN @@ -36,36 +31,19 @@ #endif #include "lmms_math.h" -#include "CaptionMenu.h" -#include "ConfigManager.h" -#include "ControllerConnection.h" #include "DeprecationHelper.h" #include "embed.h" #include "gui_templates.h" -#include "GuiApplication.h" -#include "LocaleHelper.h" -#include "MainWindow.h" -#include "ProjectJournal.h" -#include "SimpleTextFloat.h" -#include "StringPairDrag.h" + namespace lmms::gui { -SimpleTextFloat * Knob::s_textFloat = nullptr; - - - - Knob::Knob( KnobType _knob_num, QWidget * _parent, const QString & _name ) : - QWidget( _parent ), - FloatModelView( new FloatModel( 0, 0, 0, 1, nullptr, _name, true ), this ), + FloatModelEditorBase(DirectionOfManipulation::Vertical, _parent, _name), m_label( "" ), m_isHtmlLabel(false), m_tdRenderer(nullptr), - m_volumeKnob( false ), - m_volumeRatio( 100.0, 0.0, 1000000.0 ), - m_buttonPressed( false ), m_angle( -10 ), m_lineWidth( 0 ), m_textColor( 255, 255, 255 ), @@ -84,18 +62,10 @@ Knob::Knob( QWidget * _parent, const QString & _name ) : void Knob::initUi( const QString & _name ) { - if( s_textFloat == nullptr ) - { - s_textFloat = new SimpleTextFloat; - } - - setWindowTitle( _name ); - onKnobNumUpdated(); setTotalAngle( 270.0f ); setInnerRadius( 1.0f ); setOuterRadius( 10.0f ); - setFocusPolicy( Qt::ClickFocus ); // This is a workaround to enable style sheets for knobs which are not styled knobs. // @@ -123,13 +93,9 @@ void Knob::initUi( const QString & _name ) default: break; } - - doConnections(); } - - void Knob::onKnobNumUpdated() { if( m_knobNum != KnobType::Styled ) @@ -484,195 +450,6 @@ void Knob::drawKnob( QPainter * _p ) _p->drawImage( 0, 0, m_cache ); } -void Knob::showTextFloat(int msecBeforeDisplay, int msecDisplayTime) -{ - s_textFloat->setText(displayValue()); - s_textFloat->moveGlobal(this, QPoint(width() + 2, 0)); - s_textFloat->showWithDelay(msecBeforeDisplay, msecDisplayTime); -} - -float Knob::getValue( const QPoint & _p ) -{ - float value; - - // knob value increase is linear to mouse movement - value = .4f * _p.y(); - - // if shift pressed we want slower movement - if( getGUI()->mainWindow()->isShiftPressed() ) - { - value /= 4.0f; - value = qBound( -4.0f, value, 4.0f ); - } - return value * pageSize(); -} - - - - -void Knob::contextMenuEvent( QContextMenuEvent * ) -{ - // for the case, the user clicked right while pressing left mouse- - // button, the context-menu appears while mouse-cursor is still hidden - // and it isn't shown again until user does something which causes - // an QApplication::restoreOverrideCursor()-call... - mouseReleaseEvent( nullptr ); - - CaptionMenu contextMenu( model()->displayName(), this ); - addDefaultActions( &contextMenu ); - contextMenu.addAction( QPixmap(), - model()->isScaleLogarithmic() ? tr( "Set linear" ) : tr( "Set logarithmic" ), - this, SLOT(toggleScale())); - contextMenu.addSeparator(); - contextMenu.exec( QCursor::pos() ); -} - - -void Knob::toggleScale() -{ - model()->setScaleLogarithmic( ! model()->isScaleLogarithmic() ); - update(); -} - - - -void Knob::dragEnterEvent( QDragEnterEvent * _dee ) -{ - StringPairDrag::processDragEnterEvent( _dee, "float_value," - "automatable_model" ); -} - - - - -void Knob::dropEvent( QDropEvent * _de ) -{ - QString type = StringPairDrag::decodeKey( _de ); - QString val = StringPairDrag::decodeValue( _de ); - if( type == "float_value" ) - { - model()->setValue( LocaleHelper::toFloat(val) ); - _de->accept(); - } - else if( type == "automatable_model" ) - { - auto mod = dynamic_cast(Engine::projectJournal()->journallingObject(val.toInt())); - if( mod != nullptr ) - { - AutomatableModel::linkModels( model(), mod ); - mod->setValue( model()->value() ); - } - } -} - - - - -void Knob::mousePressEvent( QMouseEvent * _me ) -{ - if( _me->button() == Qt::LeftButton && - ! ( _me->modifiers() & Qt::ControlModifier ) && - ! ( _me->modifiers() & Qt::ShiftModifier ) ) - { - AutomatableModel *thisModel = model(); - if( thisModel ) - { - thisModel->addJournalCheckPoint(); - thisModel->saveJournallingState( false ); - } - - const QPoint & p = _me->pos(); - m_lastMousePos = p; - m_leftOver = 0.0f; - - emit sliderPressed(); - - showTextFloat(0, 0); - - m_buttonPressed = true; - } - else if( _me->button() == Qt::LeftButton && - (_me->modifiers() & Qt::ShiftModifier) ) - { - new StringPairDrag( "float_value", - QString::number( model()->value() ), - QPixmap(), this ); - } - else - { - FloatModelView::mousePressEvent( _me ); - } -} - - - - -void Knob::mouseMoveEvent( QMouseEvent * _me ) -{ - if( m_buttonPressed && _me->pos() != m_lastMousePos ) - { - // knob position is changed depending on last mouse position - setPosition( _me->pos() - m_lastMousePos ); - emit sliderMoved( model()->value() ); - // original position for next time is current position - m_lastMousePos = _me->pos(); - } - s_textFloat->setText( displayValue() ); - s_textFloat->show(); -} - - - - -void Knob::mouseReleaseEvent( QMouseEvent* event ) -{ - if( event && event->button() == Qt::LeftButton ) - { - AutomatableModel *thisModel = model(); - if( thisModel ) - { - thisModel->restoreJournallingState(); - } - } - - m_buttonPressed = false; - - emit sliderReleased(); - - QApplication::restoreOverrideCursor(); - - s_textFloat->hide(); -} - -void Knob::enterEvent(QEvent *event) -{ - showTextFloat(700, 2000); -} - -void Knob::leaveEvent(QEvent *event) -{ - s_textFloat->hide(); -} - - -void Knob::focusOutEvent( QFocusEvent * _fe ) -{ - // make sure we don't loose mouse release event - mouseReleaseEvent( nullptr ); - QWidget::focusOutEvent( _fe ); -} - - - - -void Knob::mouseDoubleClickEvent( QMouseEvent * ) -{ - enterValue(); -} - - - - void Knob::paintEvent( QPaintEvent * _me ) { QPainter p( this ); @@ -697,201 +474,6 @@ void Knob::paintEvent( QPaintEvent * _me ) } } - - - -void Knob::wheelEvent(QWheelEvent * we) -{ - we->accept(); - const int deltaY = we->angleDelta().y(); - float direction = deltaY > 0 ? 1 : -1; - - auto * m = model(); - float const step = m->step(); - float const range = m->range(); - - // This is the default number of steps or mouse wheel events that it takes to sweep - // from the lowest value to the highest value. - // It might be modified if the user presses modifier keys. See below. - float numberOfStepsForFullSweep = 100.; - - auto const modKeys = we->modifiers(); - if (modKeys == Qt::ShiftModifier) - { - // The shift is intended to go through the values in very coarse steps as in: - // "Shift into overdrive" - numberOfStepsForFullSweep = 10; - } - else if (modKeys == Qt::ControlModifier) - { - // The control key gives more control, i.e. it enables more fine-grained adjustments - numberOfStepsForFullSweep = 1000; - } - else if (modKeys == Qt::AltModifier) - { - // The alt key enables even finer adjustments - numberOfStepsForFullSweep = 2000; - - // It seems that on some systems pressing Alt with mess with the directions, - // i.e. scrolling the mouse wheel is interpreted as pressing the mouse wheel - // left and right. Account for this quirk. - if (deltaY == 0) - { - int const deltaX = we->angleDelta().x(); - if (deltaX != 0) - { - direction = deltaX > 0 ? 1 : -1; - } - } - } - - // Compute the number of steps but make sure that we always do at least one step - const float stepMult = std::max(range / numberOfStepsForFullSweep / step, 1.f); - const int inc = direction * stepMult; - model()->incValue(inc); - - s_textFloat->setText( displayValue() ); - s_textFloat->moveGlobal( this, QPoint( width() + 2, 0 ) ); - s_textFloat->setVisibilityTimeOut( 1000 ); - - emit sliderMoved( model()->value() ); -} - - - - -void Knob::setPosition( const QPoint & _p ) -{ - const float value = getValue( _p ) + m_leftOver; - const auto step = model()->step(); - const float oldValue = model()->value(); - - - - if( model()->isScaleLogarithmic() ) // logarithmic code - { - const float pos = model()->minValue() < 0 - ? oldValue / qMax( qAbs( model()->maxValue() ), qAbs( model()->minValue() ) ) - : ( oldValue - model()->minValue() ) / model()->range(); - const float ratio = 0.1f + qAbs( pos ) * 15.f; - float newValue = value * ratio; - if( qAbs( newValue ) >= step ) - { - float roundedValue = qRound( ( oldValue - value ) / step ) * step; - model()->setValue( roundedValue ); - m_leftOver = 0.0f; - } - else - { - m_leftOver = value; - } - } - - else // linear code - { - if( qAbs( value ) >= step ) - { - float roundedValue = qRound( ( oldValue - value ) / step ) * step; - model()->setValue( roundedValue ); - m_leftOver = 0.0f; - } - else - { - m_leftOver = value; - } - } -} - - - - -void Knob::enterValue() -{ - bool ok; - float new_val; - - if( isVolumeKnob() && - ConfigManager::inst()->value( "app", "displaydbfs" ).toInt() ) - { - new_val = QInputDialog::getDouble( - this, tr( "Set value" ), - tr( "Please enter a new value between " - "-96.0 dBFS and 6.0 dBFS:" ), - ampToDbfs( model()->getRoundedValue() / 100.0 ), - -96.0, 6.0, model()->getDigitCount(), &ok ); - if( new_val <= -96.0 ) - { - new_val = 0.0f; - } - else - { - new_val = dbfsToAmp( new_val ) * 100.0; - } - } - else - { - new_val = QInputDialog::getDouble( - this, tr( "Set value" ), - tr( "Please enter a new value between " - "%1 and %2:" ). - arg( model()->minValue() ). - arg( model()->maxValue() ), - model()->getRoundedValue(), - model()->minValue(), - model()->maxValue(), model()->getDigitCount(), &ok ); - } - - if( ok ) - { - model()->setValue( new_val ); - } -} - - - - -void Knob::friendlyUpdate() -{ - if (model() && (model()->controllerConnection() == nullptr || - model()->controllerConnection()->getController()->frequentUpdates() == false || - Controller::runningFrames() % (256*4) == 0)) - { - update(); - } -} - - - - -QString Knob::displayValue() const -{ - if( isVolumeKnob() && - ConfigManager::inst()->value( "app", "displaydbfs" ).toInt() ) - { - return m_description.trimmed() + QString( " %1 dBFS" ). - arg( ampToDbfs( model()->getRoundedValue() / volumeRatio() ), - 3, 'f', 2 ); - } - return m_description.trimmed() + QString( " %1" ). - arg( model()->getRoundedValue() ) + m_unit; -} - - - - -void Knob::doConnections() -{ - if( model() != nullptr ) - { - QObject::connect( model(), SIGNAL(dataChanged()), - this, SLOT(friendlyUpdate())); - - QObject::connect( model(), SIGNAL(propertiesChanged()), - this, SLOT(update())); - } -} - - void Knob::changeEvent(QEvent * ev) { if (ev->type() == QEvent::EnabledChange) diff --git a/src/gui/widgets/LedCheckBox.cpp b/src/gui/widgets/LedCheckBox.cpp index 0c16bf391..75e73328f 100644 --- a/src/gui/widgets/LedCheckBox.cpp +++ b/src/gui/widgets/LedCheckBox.cpp @@ -44,9 +44,10 @@ static const auto names = std::array LedCheckBox::LedCheckBox( const QString & _text, QWidget * _parent, - const QString & _name, LedColor _color ) : + const QString & _name, LedColor _color, bool legacyMode ) : AutomatableButton( _parent, _name ), - m_text( _text ) + m_text( _text ), + m_legacyMode(legacyMode) { initUi( _color ); } @@ -55,8 +56,8 @@ LedCheckBox::LedCheckBox( const QString & _text, QWidget * _parent, LedCheckBox::LedCheckBox( QWidget * _parent, - const QString & _name, LedColor _color ) : - LedCheckBox( QString(), _parent, _name, _color ) + const QString & _name, LedColor _color, bool legacyMode ) : + LedCheckBox( QString(), _parent, _name, _color, legacyMode ) { } @@ -80,24 +81,16 @@ void LedCheckBox::setText( const QString &s ) -void LedCheckBox::paintEvent( QPaintEvent * ) +void LedCheckBox::paintEvent( QPaintEvent * pe ) { - QPainter p( this ); - p.setFont( pointSize<7>( font() ) ); - - if( model()->value() == true ) - { - p.drawPixmap( 0, 0, *m_ledOnPixmap ); + if (!m_legacyMode) + { + paintNonLegacy(pe); } else { - p.drawPixmap( 0, 0, *m_ledOffPixmap ); + paintLegacy(pe); } - - p.setPen( QColor( 64, 64, 64 ) ); - p.drawText( m_ledOffPixmap->width() + 4, 11, text() ); - p.setPen( QColor( 255, 255, 255 ) ); - p.drawText( m_ledOffPixmap->width() + 3, 10, text() ); } @@ -111,7 +104,11 @@ void LedCheckBox::initUi( LedColor _color ) names[static_cast(_color)].toUtf8().constData() ) ); m_ledOffPixmap = new QPixmap( embed::getIconPixmap( "led_off" ) ); - setFont( pointSize<7>( font() ) ); + if (m_legacyMode) + { + setFont( pointSize<7>( font() ) ); + } + setText( m_text ); } @@ -120,9 +117,45 @@ void LedCheckBox::initUi( LedColor _color ) void LedCheckBox::onTextUpdated() { - setFixedSize(m_ledOffPixmap->width() + 5 + horizontalAdvance(QFontMetrics(font()), - text()), - m_ledOffPixmap->height()); + QFontMetrics const fm = fontMetrics(); + + int const width = m_ledOffPixmap->width() + 5 + horizontalAdvance(fm, text()); + int const height = m_legacyMode ? m_ledOffPixmap->height() : qMax(m_ledOffPixmap->height(), fm.height()); + + setFixedSize(width, height); +} + +void LedCheckBox::paintLegacy(QPaintEvent * pe) +{ + QPainter p( this ); + p.setFont( pointSize<7>( font() ) ); + + if( model()->value() == true ) + { + p.drawPixmap( 0, 0, *m_ledOnPixmap ); + } + else + { + p.drawPixmap( 0, 0, *m_ledOffPixmap ); + } + + p.setPen( QColor( 64, 64, 64 ) ); + p.drawText( m_ledOffPixmap->width() + 4, 11, text() ); + p.setPen( QColor( 255, 255, 255 ) ); + p.drawText( m_ledOffPixmap->width() + 3, 10, text() ); +} + +void LedCheckBox::paintNonLegacy(QPaintEvent * pe) +{ + QPainter p(this); + + QPixmap * drawnPixmap = model()->value() ? m_ledOnPixmap : m_ledOffPixmap; + + p.drawPixmap( 0, rect().height() / 2 - drawnPixmap->height() / 2, *drawnPixmap); + + QRect r = rect(); + r -= QMargins(m_ledOffPixmap->width() + 5, 0, 0, 0); + p.drawText(r, text()); } diff --git a/src/gui/widgets/TempoSyncBarModelEditor.cpp b/src/gui/widgets/TempoSyncBarModelEditor.cpp new file mode 100644 index 000000000..5ff2332e0 --- /dev/null +++ b/src/gui/widgets/TempoSyncBarModelEditor.cpp @@ -0,0 +1,302 @@ +/* + * TempoSyncBarModelEditor.cpp - adds bpm to ms conversion for the bar editor class + * + * Copyright (c) 2005-2007 Danny McRae + * Copyright (c) 2005-2009 Tobias Doerffel + * Copyright (c) 2023 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 + +#include "TempoSyncBarModelEditor.h" +#include "Engine.h" +#include "CaptionMenu.h" +#include "embed.h" +#include "GuiApplication.h" +#include "MainWindow.h" +#include "MeterDialog.h" +#include "Song.h" +#include "SubWindow.h" + + +namespace lmms::gui +{ + +TempoSyncBarModelEditor::TempoSyncBarModelEditor(QString text, FloatModel * floatModel, QWidget * parent) : + BarModelEditor(text, floatModel, parent), + m_tempoSyncIcon(embed::getIconPixmap("tempo_sync")), + m_tempoSyncDescription(tr("Tempo Sync")), + m_custom(nullptr) +{ + modelChanged(); +} + + +TempoSyncBarModelEditor::~TempoSyncBarModelEditor() +{ + if(m_custom) + { + delete m_custom->parentWidget(); + } +} + + +void TempoSyncBarModelEditor::modelChanged() +{ + TempoSyncKnobModel * tempoSyncModel = model(); + + if(tempoSyncModel == nullptr) + { + qWarning("no TempoSyncKnobModel has been set!"); + } + + if(m_custom != nullptr) + { + m_custom->setModel(&tempoSyncModel->getCustomMeterModel()); + } + + connect(tempoSyncModel, &TempoSyncKnobModel::syncModeChanged, this, &TempoSyncBarModelEditor::updateDescAndIcon); + connect(this, SIGNAL(sliderMoved(float)), tempoSyncModel, SLOT(disableSync())); + + updateDescAndIcon(); +} + + +void TempoSyncBarModelEditor::contextMenuEvent(QContextMenuEvent *) +{ + mouseReleaseEvent(nullptr); + + TempoSyncKnobModel * tempoSyncModel = model(); + + CaptionMenu contextMenu(tempoSyncModel->displayName(), this); + addDefaultActions(&contextMenu); + + contextMenu.addSeparator(); + + float limit = 60000.0f / (Engine::getSong()->getTempo() * tempoSyncModel->scale()); + + QMenu * syncMenu = contextMenu.addMenu(m_tempoSyncIcon, m_tempoSyncDescription); + + float const maxValue = tempoSyncModel->maxValue(); + + if(limit / 8.0f <= maxValue) + { + connect(syncMenu, SIGNAL(triggered(QAction*)), tempoSyncModel, SLOT(setTempoSync(QAction*))); + + syncMenu->addAction(embed::getIconPixmap("note_none"), + tr("No Sync"))->setData((int) TempoSyncKnobModel::SyncMode::None); + + if(limit / 0.125f <= maxValue) + { + syncMenu->addAction(embed::getIconPixmap("note_double_whole"), + tr("Eight beats"))->setData((int) TempoSyncKnobModel::SyncMode::DoubleWholeNote); + } + + if(limit / 0.25f <= maxValue) + { + syncMenu->addAction(embed::getIconPixmap("note_whole"), + tr("Whole note"))->setData((int) TempoSyncKnobModel::SyncMode::WholeNote); + } + + if(limit / 0.5f <= maxValue) + { + syncMenu->addAction(embed::getIconPixmap("note_half"), + tr("Half note"))->setData((int) TempoSyncKnobModel::SyncMode::HalfNote); + } + + if(limit <= maxValue) + { + syncMenu->addAction(embed::getIconPixmap("note_quarter"), + tr("Quarter note"))->setData((int) TempoSyncKnobModel::SyncMode::QuarterNote); + } + + if(limit / 2.0f <= maxValue) + { + syncMenu->addAction(embed::getIconPixmap("note_eighth"), + tr("8th note"))->setData((int) TempoSyncKnobModel::SyncMode::EighthNote); + } + + if(limit / 4.0f <= maxValue) + { + syncMenu->addAction(embed::getIconPixmap("note_sixteenth"), + tr("16th note"))->setData((int) TempoSyncKnobModel::SyncMode::SixteenthNote); + } + + syncMenu->addAction(embed::getIconPixmap("note_thirtysecond"), + tr("32nd note"))->setData((int) TempoSyncKnobModel::SyncMode::ThirtysecondNote); + + syncMenu->addAction(embed::getIconPixmap("dont_know"), + tr("Custom..."), this, SLOT(showCustom()))->setData((int) TempoSyncKnobModel::SyncMode::Custom); + + contextMenu.addSeparator(); + } + + contextMenu.exec(QCursor::pos()); + + delete syncMenu; +} + +void TempoSyncBarModelEditor::updateDescAndIcon() +{ + updateTextDescription(); + + if(m_custom != nullptr && model()->syncMode() != TempoSyncKnobModel::SyncMode::Custom) + { + m_custom->parentWidget()->hide(); + } + + updateIcon(); + + emit syncDescriptionChanged(m_tempoSyncDescription); + emit syncIconChanged(); +} + + +const QString & TempoSyncBarModelEditor::syncDescription() +{ + return m_tempoSyncDescription; +} + + +void TempoSyncBarModelEditor::setSyncDescription(const QString & new_description) +{ + m_tempoSyncDescription = new_description; + emit syncDescriptionChanged(new_description); +} + + +const QPixmap & TempoSyncBarModelEditor::syncIcon() +{ + return m_tempoSyncIcon; +} + + +void TempoSyncBarModelEditor::setSyncIcon(const QPixmap & new_icon) +{ + m_tempoSyncIcon = new_icon; + emit syncIconChanged(); +} + + +void TempoSyncBarModelEditor::showCustom() +{ + if(m_custom == nullptr) + { + m_custom = new MeterDialog(getGUI()->mainWindow()->workspace()); + QMdiSubWindow * subWindow = getGUI()->mainWindow()->addWindowedWidget(m_custom); + Qt::WindowFlags flags = subWindow->windowFlags(); + flags &= ~Qt::WindowMaximizeButtonHint; + subWindow->setWindowFlags(flags); + subWindow->setFixedSize(subWindow->size()); + m_custom->setWindowTitle("Meter"); + m_custom->setModel(&model()->getCustomMeterModel()); + } + + m_custom->parentWidget()->show(); + model()->setTempoSync(TempoSyncKnobModel::SyncMode::Custom); +} + + +void TempoSyncBarModelEditor::updateTextDescription() +{ + TempoSyncKnobModel * tempoSyncModel = model(); + + auto const syncMode = tempoSyncModel->syncMode(); + + switch(syncMode) + { + case TempoSyncKnobModel::SyncMode::None: + m_tempoSyncDescription = tr("Tempo Sync"); + break; + case TempoSyncKnobModel::SyncMode::Custom: + m_tempoSyncDescription = tr("Custom ") + + "(" + + QString::number(tempoSyncModel->getCustomMeterModel().numeratorModel().value()) + + "/" + + QString::number(tempoSyncModel->getCustomMeterModel().denominatorModel().value()) + + ")"; + break; + case TempoSyncKnobModel::SyncMode::DoubleWholeNote: + m_tempoSyncDescription = tr("Synced to Eight Beats"); + break; + case TempoSyncKnobModel::SyncMode::WholeNote: + m_tempoSyncDescription = tr("Synced to Whole Note"); + break; + case TempoSyncKnobModel::SyncMode::HalfNote: + m_tempoSyncDescription = tr("Synced to Half Note"); + break; + case TempoSyncKnobModel::SyncMode::QuarterNote: + m_tempoSyncDescription = tr("Synced to Quarter Note"); + break; + case TempoSyncKnobModel::SyncMode::EighthNote: + m_tempoSyncDescription = tr("Synced to 8th Note"); + break; + case TempoSyncKnobModel::SyncMode::SixteenthNote: + m_tempoSyncDescription = tr("Synced to 16th Note"); + break; + case TempoSyncKnobModel::SyncMode::ThirtysecondNote: + m_tempoSyncDescription = tr("Synced to 32nd Note"); + break; + default: ; + } +} + +void TempoSyncBarModelEditor::updateIcon() +{ + switch(model()->syncMode()) + { + case TempoSyncKnobModel::SyncMode::None: + m_tempoSyncIcon = embed::getIconPixmap("tempo_sync"); + break; + case TempoSyncKnobModel::SyncMode::Custom: + m_tempoSyncIcon = embed::getIconPixmap("dont_know"); + break; + case TempoSyncKnobModel::SyncMode::DoubleWholeNote: + m_tempoSyncIcon = embed::getIconPixmap("note_double_whole"); + break; + case TempoSyncKnobModel::SyncMode::WholeNote: + m_tempoSyncIcon = embed::getIconPixmap("note_whole"); + break; + case TempoSyncKnobModel::SyncMode::HalfNote: + m_tempoSyncIcon = embed::getIconPixmap("note_half"); + break; + case TempoSyncKnobModel::SyncMode::QuarterNote: + m_tempoSyncIcon = embed::getIconPixmap("note_quarter"); + break; + case TempoSyncKnobModel::SyncMode::EighthNote: + m_tempoSyncIcon = embed::getIconPixmap("note_eighth"); + break; + case TempoSyncKnobModel::SyncMode::SixteenthNote: + m_tempoSyncIcon = embed::getIconPixmap("note_sixteenth"); + break; + case TempoSyncKnobModel::SyncMode::ThirtysecondNote: + m_tempoSyncIcon = embed::getIconPixmap("note_thirtysecond"); + break; + default: + qWarning("TempoSyncKnob::calculateTempoSyncTime:" + "invalid TempoSyncMode"); + break; + } +} + + +} // namespace lmms::gui