diff --git a/CMakeLists.txt b/CMakeLists.txt index a68b788d6..e20f13527 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -494,7 +494,11 @@ IF(WANT_SF2) find_package(FluidSynth 1.1.0) if(FluidSynth_FOUND) SET(LMMS_HAVE_FLUIDSYNTH TRUE) - SET(STATUS_FLUIDSYNTH "OK") + if(FluidSynth_VERSION_STRING VERSION_GREATER_EQUAL 2) + set(STATUS_FLUIDSYNTH "OK") + else() + set(STATUS_FLUIDSYNTH "OK (FluidSynth version < 2: per-note panning unsupported)") + endif() else() SET(STATUS_FLUIDSYNTH "not found, libfluidsynth-dev (or similar)" "is highly recommended") @@ -605,7 +609,9 @@ else() set(NOOP_COMMAND "${CMAKE_COMMAND}" "-E" "echo") endif() if(STRIP) - set(STRIP_COMMAND "$,${NOOP_COMMAND},${STRIP}>") + # TODO CMake 3.19: Now that CONFIG generator expressions support testing for + # multiple configurations, combine the OR into a single CONFIG expression. + set(STRIP_COMMAND "$,$>,${NOOP_COMMAND},${STRIP}>") else() set(STRIP_COMMAND "${NOOP_COMMAND}") endif() diff --git a/include/ArrayVector.h b/include/ArrayVector.h new file mode 100644 index 000000000..06e09226c --- /dev/null +++ b/include/ArrayVector.h @@ -0,0 +1,388 @@ +/* + * ArrayVector.h + * + * Copyright (c) 2023 Dominic Clark + * + * 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_ARRAY_VECTOR_H +#define LMMS_ARRAY_VECTOR_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace lmms { + +namespace detail { + +template +constexpr bool is_input_iterator_v = false; + +template +constexpr bool is_input_iterator_v::iterator_category>> = + std::is_convertible_v::iterator_category, std::input_iterator_tag>; + +} // namespace detail + +/** + * A container that stores up to a maximum of `N` elements of type `T` directly + * within itself, rather than separately on the heap. Useful when a dynamically + * resizeable container is needed for use in real-time code. Can be thought of + * as a hybrid between `std::array` and `std::vector`. The interface follows + * that of `std::vector` - see standard C++ documentation. + */ +template +class ArrayVector +{ +public: + using size_type = std::size_t; + using difference_type = std::ptrdiff_t; + using value_type = T; + using reference = T&; + using const_reference = const T&; + using pointer = T*; + using const_pointer = const T*; + using iterator = pointer; + using const_iterator = const_pointer; + using reverse_iterator = std::reverse_iterator; + using const_reverse_iterator = std::reverse_iterator; + + ArrayVector() = default; + + ArrayVector(const ArrayVector& other) noexcept(std::is_nothrow_copy_constructible_v) : + m_size{other.m_size} + { + std::uninitialized_copy(other.begin(), other.end(), begin()); + } + + ArrayVector(ArrayVector&& other) noexcept(std::is_nothrow_move_constructible_v) : + m_size{other.m_size} + { + std::uninitialized_move(other.begin(), other.end(), begin()); + other.clear(); + } + + ArrayVector(size_type count, const T& value) noexcept(std::is_nothrow_copy_constructible_v) : + m_size{count} + { + assert(count <= N); + std::uninitialized_fill_n(begin(), count, value); + } + + explicit ArrayVector(size_type count) noexcept(std::is_nothrow_default_constructible_v) : + m_size{count} + { + assert(count <= N); + std::uninitialized_value_construct_n(begin(), count); + } + + template, int> = 0> + ArrayVector(It first, It last) + { + // Can't check the size first as the iterator may not be multipass + const auto end = std::uninitialized_copy(first, last, begin()); + m_size = end - begin(); + assert(m_size <= N); + } + + ArrayVector(std::initializer_list il) noexcept(std::is_nothrow_copy_constructible_v) : + m_size{il.size()} + { + assert(il.size() <= N); + std::uninitialized_copy(il.begin(), il.end(), begin()); + } + + ~ArrayVector() { std::destroy(begin(), end()); } + + ArrayVector& operator=(const ArrayVector& other) + noexcept(std::is_nothrow_copy_assignable_v && std::is_nothrow_copy_constructible_v) + { + if (this != &other) { + const auto toAssign = std::min(other.size(), size()); + const auto assignedFromEnd = other.begin() + toAssign; + const auto assignedToEnd = std::copy(other.begin(), other.begin() + toAssign, begin()); + std::destroy(assignedToEnd, end()); + std::uninitialized_copy(assignedFromEnd, other.end(), end()); + m_size = other.size(); + } + return *this; + } + + ArrayVector& operator=(ArrayVector&& other) + noexcept(std::is_nothrow_move_assignable_v && std::is_nothrow_move_constructible_v) + { + if (this != &other) { + const auto toAssign = std::min(other.size(), size()); + const auto assignedFromEnd = other.begin() + toAssign; + const auto assignedToEnd = std::move(other.begin(), other.begin() + toAssign, begin()); + std::destroy(assignedToEnd, end()); + std::uninitialized_move(assignedFromEnd, other.end(), end()); + m_size = other.size(); + other.clear(); + } + return *this; + } + + ArrayVector& operator=(std::initializer_list il) + noexcept(std::is_nothrow_copy_assignable_v && std::is_nothrow_copy_constructible_v) + { + assert(il.size() <= N); + const auto toAssign = std::min(il.size(), size()); + const auto assignedFromEnd = il.begin() + toAssign; + const auto assignedToEnd = std::copy(il.begin(), assignedFromEnd, begin()); + std::destroy(assignedToEnd, end()); + std::uninitialized_copy(assignedFromEnd, il.end(), end()); + m_size = il.size(); + return *this; + } + + void assign(size_type count, const T& value) + noexcept(std::is_nothrow_copy_assignable_v && std::is_nothrow_copy_constructible_v) + { + assert(count <= N); + const auto temp = value; + const auto toAssign = std::min(count, size()); + const auto toConstruct = count - toAssign; + const auto assignedToEnd = std::fill_n(begin(), toAssign, temp); + std::destroy(assignedToEnd, end()); + std::uninitialized_fill_n(assignedToEnd, toConstruct, temp); + m_size = count; + } + + template, int> = 0> + void assign(It first, It last) + { + // Can't check the size first as the iterator may not be multipass + auto pos = begin(); + for (; first != last && pos != end(); ++pos, ++first) { + *pos = *first; + } + std::destroy(pos, end()); + pos = std::uninitialized_copy(first, last, pos); + m_size = pos - begin(); + assert(m_size <= N); + } + + reference at(size_type index) + { + if (index >= m_size) { throw std::out_of_range{"index out of range"}; } + return data()[index]; + } + + const_reference at(size_type index) const + { + if (index >= m_size) { throw std::out_of_range{"index out of range"}; } + return data()[index]; + } + + reference operator[](size_type index) noexcept + { + assert(index < m_size); + return data()[index]; + } + + const_reference operator[](size_type index) const noexcept + { + assert(index < m_size); + return data()[index]; + } + + reference front() noexcept { return operator[](0); } + const_reference front() const noexcept { return operator[](0); } + + reference back() noexcept { return operator[](m_size - 1); } + const_reference back() const noexcept { return operator[](m_size - 1); } + + pointer data() noexcept { return *std::launder(reinterpret_cast(m_data)); } + const_pointer data() const noexcept { return *std::launder(reinterpret_cast(m_data)); } + + iterator begin() noexcept { return data(); } + const_iterator begin() const noexcept { return data(); } + const_iterator cbegin() const noexcept { return data(); } + + iterator end() noexcept { return data() + m_size; } + const_iterator end() const noexcept { return data() + m_size; } + const_iterator cend() const noexcept { return data() + m_size; } + + reverse_iterator rbegin() noexcept { return std::reverse_iterator{end()}; } + const_reverse_iterator rbegin() const noexcept { return std::reverse_iterator{end()}; } + const_reverse_iterator crbegin() const noexcept { return std::reverse_iterator{cend()}; } + + reverse_iterator rend() noexcept { return std::reverse_iterator{begin()}; } + const_reverse_iterator rend() const noexcept { return std::reverse_iterator{begin()}; } + const_reverse_iterator crend() const noexcept { return std::reverse_iterator{cbegin()}; } + + bool empty() const noexcept { return m_size == 0; } + bool full() const noexcept { return m_size == N; } + size_type size() const noexcept { return m_size; } + size_type max_size() const noexcept { return N; } + size_type capacity() const noexcept { return N; } + + void clear() noexcept + { + std::destroy(begin(), end()); + m_size = 0; + } + + iterator insert(const_iterator pos, const T& value) { return emplace(pos, value); } + iterator insert(const_iterator pos, T&& value) { return emplace(pos, std::move(value)); } + + iterator insert(const_iterator pos, size_type count, const T& value) + { + assert(m_size + count <= N); + assert(cbegin() <= pos && pos <= cend()); + const auto mutPos = begin() + (pos - cbegin()); + const auto newEnd = std::uninitialized_fill_n(end(), count, value); + std::rotate(mutPos, end(), newEnd); + m_size += count; + return mutPos; + } + + template, int> = 0> + iterator insert(const_iterator pos, It first, It last) + { + // Can't check the size first as the iterator may not be multipass + assert(cbegin() <= pos && pos <= cend()); + const auto mutPos = begin() + (pos - cbegin()); + const auto newEnd = std::uninitialized_copy(first, last, end()); + std::rotate(mutPos, end(), newEnd); + m_size = newEnd - begin(); + assert(m_size <= N); + return mutPos; + } + + iterator insert(const_iterator pos, std::initializer_list il) { return insert(pos, il.begin(), il.end()); } + + template + iterator emplace(const_iterator pos, Args&&... args) + { + assert(cbegin() <= pos && pos <= cend()); + const auto mutPos = begin() + (pos - cbegin()); + emplace_back(std::forward(args)...); + std::rotate(mutPos, end() - 1, end()); + return mutPos; + } + + iterator erase(const_iterator pos) { return erase(pos, pos + 1); } + iterator erase(const_iterator first, const_iterator last) + { + assert(cbegin() <= first && first <= last && last <= cend()); + const auto mutFirst = begin() + (first - cbegin()); + const auto mutLast = begin() + (last - cbegin()); + const auto newEnd = std::move(mutLast, end(), mutFirst); + std::destroy(newEnd, end()); + m_size = newEnd - begin(); + return mutFirst; + } + + void push_back(const T& value) { emplace_back(value); } + void push_back(T&& value) { emplace_back(std::move(value)); } + + template + reference emplace_back(Args&&... args) + { + assert(!full()); + // TODO C++20: Use std::construct_at + const auto result = new(static_cast(end())) T(std::forward(args)...); + ++m_size; + return *result; + } + + void pop_back() + { + assert(!empty()); + --m_size; + std::destroy_at(end()); + } + + void resize(size_type size) + { + if (size > N) { throw std::length_error{"size exceeds maximum size"}; } + if (size < m_size) { + std::destroy(begin() + size, end()); + } else { + std::uninitialized_value_construct(end(), begin() + size); + } + m_size = size; + } + + void resize(size_type size, const value_type& value) + { + if (size > N) { throw std::length_error{"size exceeds maximum size"}; } + if (size < m_size) { + std::destroy(begin() + size, end()); + } else { + std::uninitialized_fill(end(), begin() + size, value); + } + m_size = size; + } + + void swap(ArrayVector& other) + noexcept(std::is_nothrow_swappable_v && std::is_nothrow_move_constructible_v) + { + using std::swap; + swap(*this, other); + } + + friend void swap(ArrayVector& a, ArrayVector& b) + noexcept(std::is_nothrow_swappable_v && std::is_nothrow_move_constructible_v) + { + const auto toSwap = std::min(a.size(), b.size()); + const auto aSwapEnd = a.begin() + toSwap; + const auto bSwapEnd = b.begin() + toSwap; + std::swap_ranges(a.begin(), aSwapEnd, b.begin()); + std::uninitialized_move(aSwapEnd, a.end(), bSwapEnd); + std::uninitialized_move(bSwapEnd, b.end(), aSwapEnd); + std::destroy(aSwapEnd, a.end()); + std::destroy(bSwapEnd, b.end()); + std::swap(a.m_size, b.m_size); + } + + // TODO C++20: Replace with operator<=> + friend bool operator<(const ArrayVector& l, const ArrayVector& r) + { + return std::lexicographical_compare(l.begin(), l.end(), r.begin(), r.end()); + } + friend bool operator<=(const ArrayVector& l, const ArrayVector& r) { return !(r < l); } + friend bool operator>(const ArrayVector& l, const ArrayVector& r) { return r < l; } + friend bool operator>=(const ArrayVector& l, const ArrayVector& r) { return !(l < r); } + + friend bool operator==(const ArrayVector& l, const ArrayVector& r) + { + return std::equal(l.begin(), l.end(), r.begin(), r.end()); + } + // TODO C++20: Remove + friend bool operator!=(const ArrayVector& l, const ArrayVector& r) { return !(l == r); } + +private: + alignas(T) std::byte m_data[std::max(N * sizeof(T), std::size_t{1})]; // Intentionally a raw array + size_type m_size = 0; +}; + +} // namespace lmms + +#endif // LMMS_ARRAY_VECTOR_H diff --git a/include/InstrumentTrackView.h b/include/InstrumentTrackView.h index 363f5b3ab..d7d5fb83a 100644 --- a/include/InstrumentTrackView.h +++ b/include/InstrumentTrackView.h @@ -25,6 +25,7 @@ #ifndef LMMS_GUI_INSTRUMENT_TRACK_VIEW_H #define LMMS_GUI_INSTRUMENT_TRACK_VIEW_H +#include "MixerLineLcdSpinBox.h" #include "TrackView.h" #include "InstrumentTrack.h" @@ -72,6 +73,7 @@ public: protected: + void modelChanged() override; void dragEnterEvent( QDragEnterEvent * _dee ) override; void dropEvent( QDropEvent * _de ) override; @@ -97,6 +99,7 @@ private: // widgets in track-settings-widget TrackLabelButton * m_tlb; + MixerLineLcdSpinBox* m_mixerChannelNumber; Knob * m_volumeKnob; Knob * m_panningKnob; FadeButton * m_activityIndicator; diff --git a/include/Knob.h b/include/Knob.h index 85a51e363..d5739bb1c 100644 --- a/include/Knob.h +++ b/include/Knob.h @@ -144,6 +144,9 @@ protected: 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: @@ -160,6 +163,7 @@ private: float _innerRadius = 1) const; void drawKnob( QPainter * _p ); + void showTextFloat(int msecBeforeDisplay, int msecDisplayTime); void setPosition( const QPoint & _p ); bool updateAngle(); diff --git a/include/NotePlayHandle.h b/include/NotePlayHandle.h index 46b14c4cd..61df5a77a 100644 --- a/include/NotePlayHandle.h +++ b/include/NotePlayHandle.h @@ -108,6 +108,9 @@ public: return m_unpitchedFrequency; } + //! Get the current per-note detuning for this note + float currentDetuning() const { return m_baseDetuning->value(); } + /*! Renders one chunk using the attached instrument into the buffer */ void play( sampleFrame* buffer ) override; diff --git a/include/SampleTrackView.h b/include/SampleTrackView.h index b586df15e..3ccb97aea 100644 --- a/include/SampleTrackView.h +++ b/include/SampleTrackView.h @@ -26,6 +26,7 @@ #define LMMS_GUI_SAMPLE_TRACK_VIEW_H +#include "MixerLineLcdSpinBox.h" #include "TrackView.h" namespace lmms @@ -90,6 +91,7 @@ private slots: private: SampleTrackWindow * m_window; + MixerLineLcdSpinBox* m_mixerChannelNumber; Knob * m_volumeKnob; Knob * m_panningKnob; FadeButton * m_activityIndicator; diff --git a/include/SimpleTextFloat.h b/include/SimpleTextFloat.h index f720d0b3e..bde6c84fa 100644 --- a/include/SimpleTextFloat.h +++ b/include/SimpleTextFloat.h @@ -31,6 +31,7 @@ #include "lmms_export.h" class QLabel; +class QTimer; namespace lmms::gui { @@ -44,6 +45,8 @@ public: void setText(const QString & text); + void showWithDelay(int msecBeforeDisplay, int msecDisplayTime); + void setVisibilityTimeOut(int msecs); void moveGlobal(QWidget * w, const QPoint & offset) @@ -51,11 +54,14 @@ public: move(w->mapToGlobal(QPoint(0, 0)) + offset); } + void hide(); + private: QLabel * m_textLabel; + QTimer * m_showTimer; + QTimer * m_hideTimer; }; - } // namespace lmms::gui #endif diff --git a/include/TrackView.h b/include/TrackView.h index 763705599..f697d9ea8 100644 --- a/include/TrackView.h +++ b/include/TrackView.h @@ -48,11 +48,11 @@ class FadeButton; class TrackContainerView; -const int DEFAULT_SETTINGS_WIDGET_WIDTH = 224; +const int DEFAULT_SETTINGS_WIDGET_WIDTH = 256; const int TRACK_OP_WIDTH = 78; // This shaves 150-ish pixels off track buttons, // ruled from config: ui.compacttrackbuttons -const int DEFAULT_SETTINGS_WIDGET_WIDTH_COMPACT = 96; +const int DEFAULT_SETTINGS_WIDGET_WIDTH_COMPACT = 128; const int TRACK_OP_WIDTH_COMPACT = 62; diff --git a/plugins/Sf2Player/Sf2Player.cpp b/plugins/Sf2Player/Sf2Player.cpp index 1f0cb7c59..d46af5e4f 100644 --- a/plugins/Sf2Player/Sf2Player.cpp +++ b/plugins/Sf2Player/Sf2Player.cpp @@ -30,6 +30,7 @@ #include #include +#include "ArrayVector.h" #include "AudioEngine.h" #include "ConfigManager.h" #include "FileDialog.h" @@ -71,17 +72,47 @@ Plugin::Descriptor PLUGIN_EXPORT sf2player_plugin_descriptor = } +/** + * A non-owning reference to a single FluidSynth voice, for tracking whether the + * referenced voice is still the same voice that was passed to the constructor. + */ +class FluidVoice +{ +public: + //! Create a reference to the voice currently pointed at by `voice`. + explicit FluidVoice(fluid_voice_t* voice) : + m_voice{voice}, + m_id{fluid_voice_get_id(voice)} + { } + + //! Get a pointer to the referenced voice. + fluid_voice_t* get() const noexcept { return m_voice; } + + //! Test whether this object still refers to the original voice. + bool isValid() const + { + return fluid_voice_get_id(m_voice) == m_id && fluid_voice_is_playing(m_voice); + } + +private: + fluid_voice_t* m_voice; + unsigned int m_id; +}; struct Sf2PluginData { int midiNote; int lastPanning; float lastVelocity; - fluid_voice_t * fluidVoice; + // The soundfonts I checked used at most two voices per note, so space for + // four should be safe. This may need to be increased if a soundfont with + // more voices per note is found. + ArrayVector fluidVoices; bool isNew; f_cnt_t offset; bool noteOffSent; -} ; + panning_t panning; +}; @@ -681,10 +712,10 @@ void Sf2Instrument::playNote( NotePlayHandle * _n, sampleFrame * ) pluginData->midiNote = midiNote; pluginData->lastPanning = 0; pluginData->lastVelocity = _n->midiVelocity( baseVelocity ); - pluginData->fluidVoice = nullptr; pluginData->isNew = true; pluginData->offset = _n->offset(); pluginData->noteOffSent = false; + pluginData->panning = _n->getPanning(); _n->m_pluginData = pluginData; @@ -703,6 +734,17 @@ void Sf2Instrument::playNote( NotePlayHandle * _n, sampleFrame * ) m_playingNotes.append( _n ); m_playingNotesMutex.unlock(); } + + // Update the pitch of all the voices + if (const auto data = static_cast(_n->m_pluginData)) { + const auto detuning = _n->currentDetuning(); + for (const auto& voice : data->fluidVoices) { + if (voice.isValid()) { + fluid_voice_gen_set(voice.get(), GEN_COARSETUNE, detuning); + fluid_voice_update_param(voice.get(), GEN_COARSETUNE); + } + } + } } @@ -715,35 +757,47 @@ void Sf2Instrument::noteOn( Sf2PluginData * n ) const int poly = fluid_synth_get_polyphony( m_synth ); #ifndef _MSC_VER fluid_voice_t* voices[poly]; - unsigned int id[poly]; #else const auto voices = static_cast(_alloca(poly * sizeof(fluid_voice_t*))); - const auto id = static_cast(_alloca(poly * sizeof(unsigned int))); #endif - fluid_synth_get_voicelist( m_synth, voices, poly, -1 ); - for( int i = 0; i < poly; ++i ) - { - id[i] = 0; - } - for( int i = 0; i < poly && voices[i]; ++i ) - { - id[i] = fluid_voice_get_id( voices[i] ); - } fluid_synth_noteon( m_synth, m_channel, n->midiNote, n->lastVelocity ); - // get new voice and save it - fluid_synth_get_voicelist( m_synth, voices, poly, -1 ); - for( int i = 0; i < poly && voices[i]; ++i ) + // Get any new voices and store them in the plugin data + fluid_synth_get_voicelist(m_synth, voices, poly, -1); + for (int i = 0; i < poly && voices[i] && !n->fluidVoices.full(); ++i) { - const unsigned int newID = fluid_voice_get_id( voices[i] ); - if( id[i] != newID || newID == 0 ) - { - n->fluidVoice = voices[i]; - break; + const auto voice = voices[i]; + // FluidSynth stops voices with the same channel and pitch upon note-on, + // so voices with the current channel and pitch are playing this note. + if (fluid_voice_get_channel(voice) == m_channel + && fluid_voice_get_key(voice) == n->midiNote + && fluid_voice_is_on(voice) + ) { + n->fluidVoices.emplace_back(voices[i]); } } +#if FLUIDSYNTH_VERSION_MAJOR >= 2 + // Smallest balance value that results in full attenuation of one channel. + // Corresponds to internal FluidSynth macro `FLUID_CB_AMP_SIZE`. + constexpr static auto maxBalance = 1441.f; + // Convert panning from linear to exponential for FluidSynth + const auto panning = n->panning; + const auto factor = 1.f - std::abs(panning) / static_cast(PanningRight); + const auto balance = std::copysign( + factor <= 0 ? maxBalance : std::min(-200.f * std::log10(factor), maxBalance), + panning + ); + // Set note panning on all the voices + for (const auto& voice : n->fluidVoices) { + if (voice.isValid()) { + fluid_voice_gen_set(voice.get(), GEN_CUSTOM_BALANCE, balance); + fluid_voice_update_param(voice.get(), GEN_CUSTOM_BALANCE); + } + } +#endif + m_synthMutex.unlock(); m_notesRunningMutex.lock(); @@ -859,6 +913,7 @@ void Sf2Instrument::play( sampleFrame * _working_buffer ) void Sf2Instrument::renderFrames( f_cnt_t frames, sampleFrame * buf ) { m_synthMutex.lock(); + fluid_synth_get_gain(m_synth); // This flushes voice updates as a side effect if( m_internalSampleRate < Engine::audioEngine()->processingSampleRate() && m_srcState != nullptr ) { diff --git a/src/core/DataFile.cpp b/src/core/DataFile.cpp index 6ad2f8526..5c98ec81c 100644 --- a/src/core/DataFile.cpp +++ b/src/core/DataFile.cpp @@ -1671,6 +1671,8 @@ void DataFile::upgrade_extendedNoteRange() { auto root = documentElement(); UpgradeExtendedNoteRange upgradeExtendedNoteRange(root); + + upgradeExtendedNoteRange.upgrade(); } diff --git a/src/core/UpgradeExtendedNoteRange.cpp b/src/core/UpgradeExtendedNoteRange.cpp index e61da3723..6ed98e63e 100644 --- a/src/core/UpgradeExtendedNoteRange.cpp +++ b/src/core/UpgradeExtendedNoteRange.cpp @@ -193,6 +193,7 @@ static void fixTrack(QDomElement & track, std::set & automatedBase for (int i = 0; i < subTracks.size(); ++i) { QDomElement subTrack = subTracks.item(i).toElement(); + assert (static_cast(subTrack.attribute("type").toInt()) != Track::Type::Pattern); fixTrack(subTrack, automatedBaseNoteIds); } } diff --git a/src/gui/SideBarWidget.cpp b/src/gui/SideBarWidget.cpp index 60760ba59..c218bedd3 100644 --- a/src/gui/SideBarWidget.cpp +++ b/src/gui/SideBarWidget.cpp @@ -62,16 +62,16 @@ void SideBarWidget::paintEvent( QPaintEvent * ) QFont f = p.font(); f.setBold( true ); - f.setUnderline( true ); + f.setUnderline(false); f.setPointSize( f.pointSize() + 2 ); p.setFont( f ); p.setPen( palette().highlightedText().color() ); - const int tx = m_icon.width()+4; + const int tx = m_icon.width() + 8; QFontMetrics metrics( f ); - const int ty = metrics.ascent(); + const int ty = (metrics.ascent() + m_icon.height()) / 2; p.drawText( tx, ty, m_title ); p.drawPixmap( 2, 2, m_icon.transformed( QTransform().rotate( -90 ) ) ); diff --git a/src/gui/tracks/InstrumentTrackView.cpp b/src/gui/tracks/InstrumentTrackView.cpp index 669fdaccb..87c0f0449 100644 --- a/src/gui/tracks/InstrumentTrackView.cpp +++ b/src/gui/tracks/InstrumentTrackView.cpp @@ -63,7 +63,6 @@ InstrumentTrackView::InstrumentTrackView( InstrumentTrack * _it, TrackContainerV m_tlb = new TrackLabelButton( this, getTrackSettingsWidget() ); m_tlb->setCheckable( true ); m_tlb->setIcon( embed::getIconPixmap( "instrument_track" ) ); - m_tlb->move( 3, 1 ); m_tlb->show(); connect( m_tlb, SIGNAL(toggled(bool)), @@ -75,24 +74,14 @@ InstrumentTrackView::InstrumentTrackView( InstrumentTrack * _it, TrackContainerV connect(ConfigManager::inst(), SIGNAL(valueChanged(QString,QString,QString)), this, SLOT(handleConfigChange(QString,QString,QString))); - // creation of widgets for track-settings-widget - int widgetWidth; - if( ConfigManager::inst()->value( "ui", - "compacttrackbuttons" ).toInt() ) - { - widgetWidth = DEFAULT_SETTINGS_WIDGET_WIDTH_COMPACT; - } - else - { - widgetWidth = DEFAULT_SETTINGS_WIDGET_WIDTH; - } + m_mixerChannelNumber = new MixerLineLcdSpinBox(2, getTrackSettingsWidget(), tr("Mixer channel"), this); + m_mixerChannelNumber->show(); m_volumeKnob = new Knob( KnobType::Small17, getTrackSettingsWidget(), tr( "Volume" ) ); m_volumeKnob->setVolumeKnob( true ); m_volumeKnob->setModel( &_it->m_volumeModel ); m_volumeKnob->setHintText( tr( "Volume:" ), "%" ); - m_volumeKnob->move( widgetWidth-2*24, 2 ); m_volumeKnob->setLabel( tr( "VOL" ) ); m_volumeKnob->show(); @@ -100,7 +89,6 @@ InstrumentTrackView::InstrumentTrackView( InstrumentTrack * _it, TrackContainerV tr( "Panning" ) ); m_panningKnob->setModel( &_it->m_panningModel ); m_panningKnob->setHintText(tr("Panning:"), "%"); - m_panningKnob->move( widgetWidth-24, 2 ); m_panningKnob->setLabel( tr( "PAN" ) ); m_panningKnob->show(); @@ -151,9 +139,18 @@ InstrumentTrackView::InstrumentTrackView( InstrumentTrack * _it, TrackContainerV QApplication::palette().color( QPalette::Active, QPalette::BrightText).darker(), getTrackSettingsWidget() ); - m_activityIndicator->setGeometry( - widgetWidth-2*24-11, 2, 8, 28 ); + m_activityIndicator->setFixedSize(8, 28); m_activityIndicator->show(); + + auto layout = new QHBoxLayout(getTrackSettingsWidget()); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(0); + layout->addWidget(m_tlb); + layout->addWidget(m_mixerChannelNumber); + layout->addWidget(m_activityIndicator); + layout->addWidget(m_volumeKnob); + layout->addWidget(m_panningKnob); + connect( m_activityIndicator, SIGNAL(pressed()), this, SLOT(activityIndicatorPressed())); connect( m_activityIndicator, SIGNAL(released()), @@ -268,6 +265,13 @@ void InstrumentTrackView::handleConfigChange(QString cls, QString attr, QString } } +void InstrumentTrackView::modelChanged() +{ + TrackView::modelChanged(); + auto st = castModel(); + m_mixerChannelNumber->setModel(&st->m_mixerChannelModel); +} + void InstrumentTrackView::dragEnterEvent( QDragEnterEvent * _dee ) { InstrumentTrackWindow::dragEnterEventGeneric( _dee ); diff --git a/src/gui/tracks/SampleTrackView.cpp b/src/gui/tracks/SampleTrackView.cpp index 6a6a2c5fd..8516eb5c2 100644 --- a/src/gui/tracks/SampleTrackView.cpp +++ b/src/gui/tracks/SampleTrackView.cpp @@ -56,20 +56,17 @@ SampleTrackView::SampleTrackView( SampleTrack * _t, TrackContainerView* tcv ) : connect(m_tlb, SIGNAL(clicked(bool)), this, SLOT(showEffects())); m_tlb->setIcon(embed::getIconPixmap("sample_track")); - m_tlb->move(3, 1); m_tlb->show(); + m_mixerChannelNumber = new MixerLineLcdSpinBox(2, getTrackSettingsWidget(), tr("Mixer channel"), this); + m_mixerChannelNumber->show(); + m_volumeKnob = new Knob( KnobType::Small17, getTrackSettingsWidget(), tr( "Track volume" ) ); m_volumeKnob->setVolumeKnob( true ); m_volumeKnob->setModel( &_t->m_volumeModel ); m_volumeKnob->setHintText( tr( "Channel volume:" ), "%" ); - int settingsWidgetWidth = ConfigManager::inst()-> - value( "ui", "compacttrackbuttons" ).toInt() - ? DEFAULT_SETTINGS_WIDGET_WIDTH_COMPACT - : DEFAULT_SETTINGS_WIDGET_WIDTH; - m_volumeKnob->move( settingsWidgetWidth - 2 * 24, 2 ); m_volumeKnob->setLabel( tr( "VOL" ) ); m_volumeKnob->show(); @@ -77,7 +74,6 @@ SampleTrackView::SampleTrackView( SampleTrack * _t, TrackContainerView* tcv ) : tr( "Panning" ) ); m_panningKnob->setModel( &_t->m_panningModel ); m_panningKnob->setHintText( tr( "Panning:" ), "%" ); - m_panningKnob->move( settingsWidgetWidth - 24, 2 ); m_panningKnob->setLabel( tr( "PAN" ) ); m_panningKnob->show(); @@ -87,8 +83,18 @@ SampleTrackView::SampleTrackView( SampleTrack * _t, TrackContainerView* tcv ) : QApplication::palette().color(QPalette::Active, QPalette::BrightText).darker(), getTrackSettingsWidget() ); - m_activityIndicator->setGeometry(settingsWidgetWidth - 2 * 24 - 11, 2, 8, 28); + m_activityIndicator->setFixedSize(8, 28); m_activityIndicator->show(); + + auto layout = new QHBoxLayout(getTrackSettingsWidget()); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(0); + layout->addWidget(m_tlb); + layout->addWidget(m_mixerChannelNumber); + layout->addWidget(m_activityIndicator); + layout->addWidget(m_volumeKnob); + layout->addWidget(m_panningKnob); + connect(_t, SIGNAL(playingChanged()), this, SLOT(updateIndicator())); setModel( _t ); @@ -170,6 +176,7 @@ void SampleTrackView::modelChanged() { auto st = castModel(); m_volumeKnob->setModel(&st->m_volumeModel); + m_mixerChannelNumber->setModel(&st->m_mixerChannelModel); TrackView::modelChanged(); } diff --git a/src/gui/widgets/Knob.cpp b/src/gui/widgets/Knob.cpp index c2f90fb2b..56cf29345 100644 --- a/src/gui/widgets/Knob.cpp +++ b/src/gui/widgets/Knob.cpp @@ -22,6 +22,8 @@ * */ +#include "Knob.h" + #include #include #include @@ -34,7 +36,6 @@ #endif #include "lmms_math.h" -#include "Knob.h" #include "CaptionMenu.h" #include "ConfigManager.h" #include "ControllerConnection.h" @@ -48,7 +49,6 @@ #include "SimpleTextFloat.h" #include "StringPairDrag.h" - namespace lmms::gui { @@ -484,6 +484,13 @@ 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; @@ -580,10 +587,8 @@ void Knob::mousePressEvent( QMouseEvent * _me ) emit sliderPressed(); - s_textFloat->setText( displayValue() ); - s_textFloat->moveGlobal( this, - QPoint( width() + 2, 0 ) ); - s_textFloat->show(); + showTextFloat(0, 0); + m_buttonPressed = true; } else if( _me->button() == Qt::LeftButton && @@ -613,6 +618,7 @@ void Knob::mouseMoveEvent( QMouseEvent * _me ) m_lastMousePos = _me->pos(); } s_textFloat->setText( displayValue() ); + s_textFloat->show(); } @@ -638,7 +644,15 @@ void Knob::mouseReleaseEvent( QMouseEvent* event ) s_textFloat->hide(); } +void Knob::enterEvent(QEvent *event) +{ + showTextFloat(700, 2000); +} +void Knob::leaveEvent(QEvent *event) +{ + s_textFloat->hide(); +} void Knob::focusOutEvent( QFocusEvent * _fe ) diff --git a/src/gui/widgets/SimpleTextFloat.cpp b/src/gui/widgets/SimpleTextFloat.cpp index d1f490b5e..df438423e 100644 --- a/src/gui/widgets/SimpleTextFloat.cpp +++ b/src/gui/widgets/SimpleTextFloat.cpp @@ -45,6 +45,14 @@ SimpleTextFloat::SimpleTextFloat() : m_textLabel = new QLabel(this); layout->addWidget(m_textLabel); + + m_showTimer = new QTimer(); + m_showTimer->setSingleShot(true); + QObject::connect(m_showTimer, &QTimer::timeout, this, &SimpleTextFloat::show); + + m_hideTimer = new QTimer(); + m_hideTimer->setSingleShot(true); + QObject::connect(m_hideTimer, &QTimer::timeout, this, &SimpleTextFloat::hide); } void SimpleTextFloat::setText(const QString & text) @@ -52,6 +60,29 @@ void SimpleTextFloat::setText(const QString & text) m_textLabel->setText(text); } +void SimpleTextFloat::showWithDelay(int msecBeforeDisplay, int msecDisplayTime) +{ + if (msecBeforeDisplay != 0) + { + m_showTimer->start(msecBeforeDisplay); + } + else + { + show(); + } + + if (msecDisplayTime != 0) + { + m_hideTimer->start(msecBeforeDisplay + msecDisplayTime); + } +} + +void SimpleTextFloat::hide() +{ + m_showTimer->stop(); + m_hideTimer->stop(); + QWidget::hide(); +} void SimpleTextFloat::setVisibilityTimeOut(int msecs) { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 6ff9c41e9..514bf7815 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -19,6 +19,7 @@ ADD_EXECUTABLE(tests QTestSuite.cpp $ + src/core/ArrayVectorTest.cpp src/core/AutomatableModelTest.cpp src/core/ProjectVersionTest.cpp src/core/RelativePathsTest.cpp diff --git a/tests/main.cpp b/tests/main.cpp index 6d375e6c6..c1a5b5a10 100644 --- a/tests/main.cpp +++ b/tests/main.cpp @@ -16,7 +16,7 @@ int main(int argc, char* argv[]) int failed = 0; for (QTestSuite*& suite : QTestSuite::suites()) { - failed += QTest::qExec(suite, argc, argv); + if (QTest::qExec(suite, argc, argv) != 0) { ++failed; } } qDebug() << "<<" << failed << "out of"< +#include + +#include "QTestSuite.h" + +using lmms::ArrayVector; + +struct ShouldNotConstruct +{ + ShouldNotConstruct() { QFAIL("should not construct"); } +}; + +struct ShouldNotDestruct +{ + ~ShouldNotDestruct() { QFAIL("should not destruct"); } +}; + +enum class Construction { Default, Copy, Move, CopyAssign, MoveAssign }; + +struct Constructible +{ + Constructible() : construction{Construction::Default} {} + Constructible(const Constructible&) : construction{Construction::Copy} {} + Constructible(Constructible&&) : construction{Construction::Move} {} + Constructible& operator=(const Constructible&) { construction = Construction::CopyAssign; return *this; } + Constructible& operator=(Constructible&&) { construction = Construction::MoveAssign; return *this; } + Construction construction; +}; + +struct DestructorCheck +{ + ~DestructorCheck() { *destructed = true; } + bool* destructed; +}; + +class ArrayVectorTest : QTestSuite +{ + Q_OBJECT + +private slots: + void defaultConstructorTest() + { + // Ensure no elements are constructed + const auto v = ArrayVector(); + // Ensure the container is empty + QVERIFY(v.empty()); + } + + void copyConstructorTest() + { + { + // Ensure all elements are copy constructed + const auto v = ArrayVector{{}}; + const auto copy = v; + for (const auto& element : copy) { + QCOMPARE(element.construction, Construction::Copy); + } + } + { + // Ensure corresponding elements are used + const auto v = ArrayVector{1, 2, 3}; + const auto copy = v; + const auto expected = std::array{1, 2, 3}; + QVERIFY(std::equal(copy.begin(), copy.end(), expected.begin(), expected.end())); + } + } + + void moveConstructorTest() + { + { + // Ensure all elements are move constructed + auto v = ArrayVector{{}}; + const auto moved = std::move(v); + for (const auto& element : moved) { + QCOMPARE(element.construction, Construction::Move); + } + } + { + // Ensure corresponding elements are used + auto v = ArrayVector{1, 2, 3}; + const auto moved = std::move(v); + const auto expected = std::array{1, 2, 3}; + QVERIFY(std::equal(moved.begin(), moved.end(), expected.begin(), expected.end())); + // Move construction should leave the source empty + QVERIFY(v.empty()); + } + } + + void fillValueConstructorTest() + { + // Ensure all elements are copy constructed + const auto v = ArrayVector(1, {}); + for (const auto& element : v) { + QCOMPARE(element.construction, Construction::Copy); + } + // Ensure the container has the correct size + QCOMPARE(v.size(), std::size_t{1}); + } + + void fillDefaultConstructorTest() + { + // Ensure all elements are copy constructed + const auto v = ArrayVector(1); + for (const auto& element : v) { + QCOMPARE(element.construction, Construction::Default); + } + // Ensure the container has the correct size + QCOMPARE(v.size(), std::size_t{1}); + } + + void rangeConstructorTest() + { + { + // Ensure the elements are copy constructed from normal iterators + const auto data = std::array{Constructible{}}; + const auto v = ArrayVector(data.begin(), data.end()); + for (const auto& element : v) { + QCOMPARE(element.construction, Construction::Copy); + } + } + { + // Ensure the elements are move constructed from move iterators + auto data = std::array{Constructible{}}; + const auto v = ArrayVector( + std::move_iterator{data.begin()}, std::move_iterator{data.end()}); + for (const auto& element : v) { + QCOMPARE(element.construction, Construction::Move); + } + } + { + // Ensure corresponding elements are used + const auto data = std::array{1, 2, 3}; + const auto v = ArrayVector(data.begin(), data.end()); + QVERIFY(std::equal(v.begin(), v.end(), data.begin(), data.end())); + } + } + + void initializerListConstructorTest() + { + // Ensure the container is constructed with the correct data + const auto v = ArrayVector{1, 2, 3}; + const auto expected = std::array{1, 2, 3}; + QVERIFY(std::equal(v.begin(), v.end(), expected.begin(), expected.end())); + } + + void destructorTest() + { + { + // Should not call destructors for space without elements + const auto v = ArrayVector{}; + } + { + // Should call destructors for all elements + auto destructed = false; + { + const auto v = ArrayVector{{&destructed}}; + } + QVERIFY(destructed); + } + } + + void copyAssignmentTest() + { + { + // Self-assignment should not change the contents + auto v = ArrayVector{1, 2, 3}; + const auto oldValue = v; + v = v; + QCOMPARE(v, oldValue); + } + { + // Assignment to a larger container should copy assign + const auto src = ArrayVector(3); + auto dst = ArrayVector(5); + dst = src; + QCOMPARE(dst.size(), std::size_t{3}); + for (const auto& element : dst) { + QCOMPARE(element.construction, Construction::CopyAssign); + } + } + { + // Assignment to a smaller container should copy construct + const auto src = ArrayVector(3); + auto dst = ArrayVector{}; + dst = src; + QCOMPARE(dst.size(), std::size_t{3}); + for (const auto& element : dst) { + QCOMPARE(element.construction, Construction::Copy); + } + } + { + // Ensure corresponding elements are used + const auto src = ArrayVector{1, 2, 3}; + auto dst = ArrayVector{}; + dst = src; + QCOMPARE(dst, (ArrayVector{1, 2, 3})); + } + } + + void moveAssignmentTest() + { + { + // Self-assignment should not change the contents + auto v = ArrayVector{1, 2, 3}; + const auto oldValue = v; + v = std::move(v); + QCOMPARE(v, oldValue); + } + { + // Assignment to a larger container should move assign + auto src = ArrayVector(3); + auto dst = ArrayVector(5); + dst = std::move(src); + QCOMPARE(dst.size(), std::size_t{3}); + for (const auto& element : dst) { + QCOMPARE(element.construction, Construction::MoveAssign); + } + } + { + // Assignment to a smaller container should move construct + auto src = ArrayVector(3); + auto dst = ArrayVector{}; + dst = std::move(src); + QCOMPARE(dst.size(), std::size_t{3}); + for (const auto& element : dst) { + QCOMPARE(element.construction, Construction::Move); + } + } + { + // Ensure corresponding elements are used + auto src = ArrayVector{1, 2, 3}; + auto dst = ArrayVector{}; + dst = std::move(src); + QCOMPARE(dst, (ArrayVector{1, 2, 3})); + } + } + + void initializerListAssignmentTest() + { + { + // Assignment to a larger container should copy assign + auto v = ArrayVector(2); + v = {Constructible{}}; + QCOMPARE(v.size(), std::size_t{1}); + for (const auto& element : v) { + QCOMPARE(element.construction, Construction::CopyAssign); + } + } + { + // Assignment to a smaller container should copy construct + auto v = ArrayVector{}; + v = {Constructible{}}; + QCOMPARE(v.size(), std::size_t{1}); + for (const auto& element : v) { + QCOMPARE(element.construction, Construction::Copy); + } + } + { + // Ensure corresponding elements are used + auto v = ArrayVector{}; + v = {1, 2, 3}; + QCOMPARE(v, (ArrayVector{1, 2, 3})); + } + } + + void fillValueAssignTest() + { + { + // Assignment to a larger container should copy assign + auto v = ArrayVector(5); + v.assign(3, {}); + QCOMPARE(v.size(), std::size_t{3}); + for (const auto& element : v) { + QCOMPARE(element.construction, Construction::CopyAssign); + } + } + { + // Assignment to a smaller container should copy construct + auto v = ArrayVector{}; + v.assign(3, {}); + QCOMPARE(v.size(), std::size_t{3}); + for (const auto& element : v) { + QCOMPARE(element.construction, Construction::Copy); + } + } + { + // Ensure correct value is filled + auto v = ArrayVector{}; + v.assign(3, 1); + QCOMPARE(v, (ArrayVector{1, 1, 1})); + } + } + + void rangeAssignTest() + { + { + // Assignment to a larger container should copy assign + const auto data = std::array{Constructible{}}; + auto v = ArrayVector(2); + v.assign(data.begin(), data.end()); + QCOMPARE(v.size(), std::size_t{1}); + for (const auto& element : v) { + QCOMPARE(element.construction, Construction::CopyAssign); + } + } + { + // Assignment to a smaller container should copy construct + const auto data = std::array{Constructible{}}; + auto v = ArrayVector{}; + v.assign(data.begin(), data.end()); + QCOMPARE(v.size(), std::size_t{1}); + for (const auto& element : v) { + QCOMPARE(element.construction, Construction::Copy); + } + } + { + // Ensure correct value is filled + const auto data = std::array{1, 2, 3}; + auto v = ArrayVector{}; + v.assign(data.begin(), data.end()); + QCOMPARE(v, (ArrayVector{1, 2, 3})); + } + } + + void atTest() + { + { + // Non-const version + auto v = ArrayVector{1, 2, 3}; + QCOMPARE(v.at(1), 2); + QVERIFY_EXCEPTION_THROWN(v.at(3), std::out_of_range); + } + { + // Const version + const auto v = ArrayVector{1, 2, 3}; + QCOMPARE(v.at(1), 2); + QVERIFY_EXCEPTION_THROWN(v.at(3), std::out_of_range); + } + } + + void subscriptTest() + { + { + // Non-const version + auto v = ArrayVector{1, 2, 3}; + QCOMPARE(v[1], 2); + } + { + // Const version + const auto v = ArrayVector{1, 2, 3}; + QCOMPARE(v[1], 2); + } + } + + void frontTest() + { + { + // Non-const version + auto v = ArrayVector{1, 2, 3}; + QCOMPARE(v.front(), 1); + } + { + // Const version + const auto v = ArrayVector{1, 2, 3}; + QCOMPARE(v.front(), 1); + } + } + + void backTest() + { + { + // Non-const version + auto v = ArrayVector{1, 2, 3}; + QCOMPARE(v.back(), 3); + } + { + // Const version + const auto v = ArrayVector{1, 2, 3}; + QCOMPARE(v.back(), 3); + } + } + + void dataTest() + { + { + // Non-const version + auto v = ArrayVector{1, 2, 3}; + QCOMPARE(v.data(), &v.front()); + } + { + // Const version + const auto v = ArrayVector{1, 2, 3}; + QCOMPARE(v.data(), &v.front()); + } + } + + void beginEndTest() + { + const auto expected = std::array{1, 2, 3}; + { + // Non-const version + auto v = ArrayVector{1, 2, 3}; + QVERIFY(std::equal(v.begin(), v.end(), expected.begin(), expected.end())); + QVERIFY(std::equal(v.cbegin(), v.cend(), expected.begin(), expected.end())); + } + { + // Const version + const auto v = ArrayVector{1, 2, 3}; + QVERIFY(std::equal(v.begin(), v.end(), expected.begin(), expected.end())); + } + } + + void rbeginRendTest() + { + const auto expected = std::array{3, 2, 1}; + { + // Non-const version + auto v = ArrayVector{1, 2, 3}; + QVERIFY(std::equal(v.rbegin(), v.rend(), expected.begin(), expected.end())); + QVERIFY(std::equal(v.crbegin(), v.crend(), expected.begin(), expected.end())); + } + { + // Const version + const auto v = ArrayVector{1, 2, 3}; + QVERIFY(std::equal(v.rbegin(), v.rend(), expected.begin(), expected.end())); + } + } + + void emptyFullSizeMaxCapacityTest() + { + auto v = ArrayVector{}; + QVERIFY(v.empty()); + QVERIFY(!v.full()); + QCOMPARE(v.size(), std::size_t{0}); + QCOMPARE(v.max_size(), std::size_t{2}); + QCOMPARE(v.capacity(), std::size_t{2}); + + v.push_back(1); + QVERIFY(!v.empty()); + QVERIFY(!v.full()); + QCOMPARE(v.size(), std::size_t{1}); + QCOMPARE(v.max_size(), std::size_t{2}); + QCOMPARE(v.capacity(), std::size_t{2}); + + v.push_back(2); + QVERIFY(!v.empty()); + QVERIFY(v.full()); + QCOMPARE(v.size(), std::size_t{2}); + QCOMPARE(v.max_size(), std::size_t{2}); + QCOMPARE(v.capacity(), std::size_t{2}); + + auto empty = ArrayVector{}; + QVERIFY(empty.empty()); + QVERIFY(empty.full()); + QCOMPARE(empty.size(), std::size_t{0}); + QCOMPARE(empty.max_size(), std::size_t{0}); + QCOMPARE(empty.capacity(), std::size_t{0}); + } + + void insertValueTest() + { + { + // Copy + const auto data = Constructible{}; + auto v = ArrayVector{}; + v.insert(v.cbegin(), data); + QCOMPARE(v.size(), std::size_t{1}); + QCOMPARE(v[0].construction, Construction::Copy); + } + { + // Move + auto v = ArrayVector{}; + v.insert(v.cbegin(), Constructible{}); + QCOMPARE(v.size(), std::size_t{1}); + QCOMPARE(v[0].construction, Construction::Move); + } + { + // Ensure the correct value is used (copy) + const auto data = 1; + auto v = ArrayVector{2, 3}; + v.insert(v.cbegin(), data); + QCOMPARE(v, (ArrayVector{1, 2, 3})); + } + { + // Ensure the correct value is used (move) + auto v = ArrayVector{2, 3}; + v.insert(v.cbegin(), 1); + QCOMPARE(v, (ArrayVector{1, 2, 3})); + } + } + + void insertFillValueTest() + { + { + // Insertion should copy construct + auto v = ArrayVector{}; + v.insert(v.cbegin(), 3, {}); + QCOMPARE(v.size(), std::size_t{3}); + for (const auto& element : v) { + QCOMPARE(element.construction, Construction::Copy); + } + } + { + // Ensure correct value is filled + auto v = ArrayVector{1, 3}; + v.insert(v.cbegin() + 1, 3, 2); + QCOMPARE(v, (ArrayVector{1, 2, 2, 2, 3})); + } + } + + void insertRangeTest() + { + { + // Insertion should copy construct + const auto data = std::array{Constructible{}}; + auto v = ArrayVector{}; + v.insert(v.cbegin(), data.begin(), data.end()); + QCOMPARE(v.size(), std::size_t{1}); + for (const auto& element : v) { + QCOMPARE(element.construction, Construction::Copy); + } + } + { + // Ensure correct value is filled + const auto data = std::array{2, 3}; + auto v = ArrayVector{1, 4}; + v.insert(v.cbegin() + 1, data.begin(), data.end()); + QCOMPARE(v, (ArrayVector{1, 2, 3, 4})); + } + } + + void insertInitializerListTest() + { + { + // Insertion should copy construct + auto v = ArrayVector{}; + v.insert(v.cbegin(), {Constructible{}}); + QCOMPARE(v.size(), std::size_t{1}); + for (const auto& element : v) { + QCOMPARE(element.construction, Construction::Copy); + } + } + { + // Ensure corresponding elements are used + auto v = ArrayVector{1, 4}; + v.insert(v.cbegin() + 1, {2, 3}); + QCOMPARE(v, (ArrayVector{1, 2, 3, 4})); + } + } + + void emplaceTest() + { + { + // Ensure the value is constructed in-place + auto v = ArrayVector{}; + v.emplace(v.cbegin()); + QCOMPARE(v.size(), std::size_t{1}); + QCOMPARE(v[0].construction, Construction::Default); + } + { + // Ensure the correct value is used (move) + auto v = ArrayVector{2, 3}; + v.emplace(v.cbegin(), 1); + QCOMPARE(v, (ArrayVector{1, 2, 3})); + } + } + + void eraseTest() + { + { + // Ensure destructors are run + auto destructed = false; + auto v = ArrayVector{{&destructed}}; + v.erase(v.cbegin()); + QVERIFY(destructed); + } + { + // Ensure the result is correct + auto v = ArrayVector{10, 1, 2, 3}; + v.erase(v.cbegin()); + QCOMPARE(v, (ArrayVector{1, 2, 3})); + } + } + + void eraseRangeTest() + { + { + // Ensure destructors are run + auto destructed = false; + auto v = ArrayVector{{&destructed}}; + v.erase(v.cbegin(), v.cend()); + QVERIFY(destructed); + } + { + // Ensure the result is correct + auto v = ArrayVector{1, 20, 21, 2, 3}; + v.erase(v.cbegin() + 1, v.cbegin() + 3); + QCOMPARE(v, (ArrayVector{1, 2, 3})); + } + } + + void pushBackTest() + { + { + // Copy + const auto data = Constructible{}; + auto v = ArrayVector{}; + v.push_back(data); + QCOMPARE(v.size(), std::size_t{1}); + QCOMPARE(v[0].construction, Construction::Copy); + } + { + // Move + auto v = ArrayVector{}; + v.push_back({}); + QCOMPARE(v.size(), std::size_t{1}); + QCOMPARE(v[0].construction, Construction::Move); + } + { + // Ensure the correct value is used (copy) + const auto data = 3; + auto v = ArrayVector{1, 2}; + v.push_back(data); + QCOMPARE(v, (ArrayVector{1, 2, 3})); + } + { + // Ensure the correct value is used (move) + auto v = ArrayVector{1, 2}; + v.push_back(3); + QCOMPARE(v, (ArrayVector{1, 2, 3})); + } + } + + void emplaceBackTest() + { + { + // Ensure the value is constructed in-place + auto v = ArrayVector{}; + v.emplace_back(); + QCOMPARE(v.size(), std::size_t{1}); + QCOMPARE(v[0].construction, Construction::Default); + } + { + // Ensure the correct value is used (move) + auto v = ArrayVector{1, 2}; + v.emplace_back(3); + QCOMPARE(v, (ArrayVector{1, 2, 3})); + } + } + + void popBackTest() + { + { + // Ensure destructors are run + auto destructed = false; + auto v = ArrayVector{{&destructed}}; + v.pop_back(); + QVERIFY(destructed); + } + { + // Ensure the result is correct + auto v = ArrayVector{1, 2, 3}; + v.pop_back(); + QCOMPARE(v, (ArrayVector{1, 2})); + } + } + + void resizeDefaultTest() + { + { + // Smaller + auto destructed = false; + auto v = ArrayVector{{&destructed}}; + QCOMPARE(v.size(), std::size_t{1}); + v.resize(0); + QCOMPARE(v.size(), std::size_t{0}); + QVERIFY(destructed); + } + { + // Bigger + auto v = ArrayVector{}; + QCOMPARE(v.size(), std::size_t{0}); + v.resize(1); + QCOMPARE(v.size(), std::size_t{1}); + QCOMPARE(v[0].construction, Construction::Default); + } + { + // Too big + auto v = ArrayVector{}; + QVERIFY_EXCEPTION_THROWN(v.resize(2), std::length_error); + } + } + + void resizeValueTest() + { + { + // Smaller + auto dummy = false; + auto destructed = false; + auto v = ArrayVector{{&destructed}}; + QCOMPARE(v.size(), std::size_t{1}); + v.resize(0, {&dummy}); + QCOMPARE(v.size(), std::size_t{0}); + QVERIFY(destructed); + } + { + // Bigger + auto v = ArrayVector{}; + QCOMPARE(v.size(), std::size_t{0}); + v.resize(1, {}); + QCOMPARE(v.size(), std::size_t{1}); + QCOMPARE(v[0].construction, Construction::Copy); + } + { + // Too big + auto v = ArrayVector{}; + QVERIFY_EXCEPTION_THROWN(v.resize(2), std::length_error); + } + { + // Ensure the correct value is used + auto v = ArrayVector{}; + v.resize(1, 1); + QCOMPARE(v, (ArrayVector{1})); + } + } + + void clearTest() + { + { + // Ensure destructors are run + auto destructed = false; + auto v = ArrayVector{{&destructed}}; + v.clear(); + QVERIFY(destructed); + } + { + // Ensure the result is correct + auto v = ArrayVector{1, 2, 3}; + v.clear(); + QCOMPARE(v, (ArrayVector{})); + } + } + + void memberSwapTest() + { + auto a = ArrayVector{1, 2, 3, 4}; + auto b = ArrayVector{2, 4, 6}; + + const auto aOriginal = a; + const auto bOriginal = b; + + a.swap(b); + + QCOMPARE(a, bOriginal); + QCOMPARE(b, aOriginal); + } + + void freeSwapTest() + { + auto a = ArrayVector{1, 2, 3, 4}; + auto b = ArrayVector{2, 4, 6}; + + const auto aOriginal = a; + const auto bOriginal = b; + + swap(a, b); + + QCOMPARE(a, bOriginal); + QCOMPARE(b, aOriginal); + } + + void comparisonTest() + { + const auto v = ArrayVector{1, 2, 3}; + const auto l = ArrayVector{1, 2, 2}; + const auto e = ArrayVector{1, 2, 3}; + const auto g = ArrayVector{1, 3, 3}; + + QVERIFY(l < v); + QVERIFY(!(e < v)); + QVERIFY(!(g < v)); + + QVERIFY(l <= v); + QVERIFY(e <= v); + QVERIFY(!(g <= v)); + + QVERIFY(!(l > v)); + QVERIFY(!(e > v)); + QVERIFY(g > v); + + QVERIFY(!(l >= v)); + QVERIFY(e >= v); + QVERIFY(g >= v); + + QVERIFY(!(l == v)); + QVERIFY(e == v); + QVERIFY(!(g == v)); + + QVERIFY(l != v); + QVERIFY(!(e != v)); + QVERIFY(g != v); + } +} ArrayVectorTests; + +#include "ArrayVectorTest.moc"