diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml index a1447e7f7..85464e133 100644 --- a/.github/workflows/build-android.yml +++ b/.github/workflows/build-android.yml @@ -87,7 +87,7 @@ jobs: # Upload Engine - name: Upload Engine - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: Engine-${{ matrix.arch }}-${{ matrix.branding }} (${{ matrix.os }}) path: | diff --git a/.github/workflows/build-ios.yml b/.github/workflows/build-ios.yml index 2c025ebce..d13d94657 100644 --- a/.github/workflows/build-ios.yml +++ b/.github/workflows/build-ios.yml @@ -69,7 +69,7 @@ jobs: # Upload Engine - name: Upload Engine - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: Engine-${{ matrix.arch }}-${{ matrix.branding }} (${{ matrix.os }}) path: | diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 4b71875c2..7c6608068 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -28,7 +28,7 @@ jobs: touch repository.tar.zst tar -I "zstd -T0 --ultra -10" --exclude=repository.tar.zst --exclude=.git -cf repository.tar.zst . - name: Upload - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: retention-days: 1 name: Repository @@ -238,14 +238,14 @@ jobs: # Upload Studio - name: Upload Studio - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: Packages (${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.branding }}) path: ./dist/${{ matrix.os }}/*.deb # Upload Engine - name: Upload Engine - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: Engine (${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.branding }}) path: | diff --git a/.github/workflows/build-osx.yml b/.github/workflows/build-osx.yml index 8c97a7d7d..3555e9dc6 100644 --- a/.github/workflows/build-osx.yml +++ b/.github/workflows/build-osx.yml @@ -85,7 +85,7 @@ jobs: # Upload - name: Upload - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: Binaries (${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.branding }}) path: | @@ -94,7 +94,7 @@ jobs: # Upload lto.o - name: Upload lto.o - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: retention-days: 1 name: lto.o (${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.branding }}) @@ -102,7 +102,7 @@ jobs: # Upload Engine - name: Upload Engine - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: Engine (${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.branding }}) path: | @@ -184,7 +184,7 @@ jobs: # Upload - name: Upload - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: Packages (${{ matrix.os }}-${{ matrix.branding }}) path: ./dist/osx-10.15/*.pkg @@ -272,7 +272,7 @@ jobs: # Upload - name: Upload - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: Packages Store (${{ matrix.os }}-${{ matrix.branding }}) path: ./dist/osx-10.15/*.pkg diff --git a/.github/workflows/build-win.yml b/.github/workflows/build-win.yml index 30535aa54..f6e7c6eb9 100644 --- a/.github/workflows/build-win.yml +++ b/.github/workflows/build-win.yml @@ -107,7 +107,7 @@ jobs: # Upload Studio - name: Upload Studio - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: Binaries (${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.branding }}) path: | @@ -117,7 +117,7 @@ jobs: # Upload Engine - name: Upload Engine - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: Engine (${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.branding }}) path: | @@ -219,7 +219,7 @@ jobs: # Upload MSI - name: Upload MSI - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: Packages MSI (${{ matrix.os }}-${{ matrix.branding }}) path: | @@ -227,7 +227,7 @@ jobs: # Upload Packages - name: Upload App - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: Packages App (${{ matrix.os }}-${{ matrix.branding }}) path: | diff --git a/src/Engine/Devices/OpenBCI/OpenBCIDevices.cpp b/src/Engine/Devices/OpenBCI/OpenBCIDevices.cpp index 23d20e638..accdeb14c 100644 --- a/src/Engine/Devices/OpenBCI/OpenBCIDevices.cpp +++ b/src/Engine/Devices/OpenBCI/OpenBCIDevices.cpp @@ -139,22 +139,22 @@ void OpenBCIDaisyDevice::CreateElectrodes() mElectrodes.Clear(); mElectrodes.Reserve(NUMELECTRODESDAISY); - mElectrodes.Add(GetEEGElectrodes()->GetElectrodeByID("P3")); - mElectrodes.Add(GetEEGElectrodes()->GetElectrodeByID("P4")); - mElectrodes.Add(GetEEGElectrodes()->GetElectrodeByID("Oz")); - mElectrodes.Add(GetEEGElectrodes()->GetElectrodeByID("Fz")); - mElectrodes.Add(GetEEGElectrodes()->GetElectrodeByID("T5")); - mElectrodes.Add(GetEEGElectrodes()->GetElectrodeByID("T6")); - mElectrodes.Add(GetEEGElectrodes()->GetElectrodeByID("F3")); - mElectrodes.Add(GetEEGElectrodes()->GetElectrodeByID("F4")); - mElectrodes.Add(GetEEGElectrodes()->GetElectrodeByID("T3")); - mElectrodes.Add(GetEEGElectrodes()->GetElectrodeByID("T4")); - mElectrodes.Add(GetEEGElectrodes()->GetElectrodeByID("C3")); - mElectrodes.Add(GetEEGElectrodes()->GetElectrodeByID("C4")); - mElectrodes.Add(GetEEGElectrodes()->GetElectrodeByID("Cz")); - mElectrodes.Add(GetEEGElectrodes()->GetElectrodeByID("O1")); - mElectrodes.Add(GetEEGElectrodes()->GetElectrodeByID("O2")); - mElectrodes.Add(GetEEGElectrodes()->GetElectrodeByID("Pz")); + mElectrodes.Add(GetEEGElectrodes()->GetElectrodeByID("1")); + mElectrodes.Add(GetEEGElectrodes()->GetElectrodeByID("2")); + mElectrodes.Add(GetEEGElectrodes()->GetElectrodeByID("3")); + mElectrodes.Add(GetEEGElectrodes()->GetElectrodeByID("4")); + mElectrodes.Add(GetEEGElectrodes()->GetElectrodeByID("5")); + mElectrodes.Add(GetEEGElectrodes()->GetElectrodeByID("6")); + mElectrodes.Add(GetEEGElectrodes()->GetElectrodeByID("7")); + mElectrodes.Add(GetEEGElectrodes()->GetElectrodeByID("8")); + mElectrodes.Add(GetEEGElectrodes()->GetElectrodeByID("9")); + mElectrodes.Add(GetEEGElectrodes()->GetElectrodeByID("10")); + mElectrodes.Add(GetEEGElectrodes()->GetElectrodeByID("11")); + mElectrodes.Add(GetEEGElectrodes()->GetElectrodeByID("12")); + mElectrodes.Add(GetEEGElectrodes()->GetElectrodeByID("13")); + mElectrodes.Add(GetEEGElectrodes()->GetElectrodeByID("14")); + mElectrodes.Add(GetEEGElectrodes()->GetElectrodeByID("15")); + mElectrodes.Add(GetEEGElectrodes()->GetElectrodeByID("16")); } #endif diff --git a/src/Engine/Devices/OpenBCI/OpenBCIDevices.h b/src/Engine/Devices/OpenBCI/OpenBCIDevices.h index 07d7ef090..93d8a9da7 100644 --- a/src/Engine/Devices/OpenBCI/OpenBCIDevices.h +++ b/src/Engine/Devices/OpenBCI/OpenBCIDevices.h @@ -87,6 +87,7 @@ class ENGINE_API OpenBCIDeviceBase : public BciDevice double GetExpectedJitter() const override { return 0.1; } bool IsWireless() const override { return true; } bool HasTestMode() const override { return false; } + double GetTimeoutLimit() const override { return 60; } void CreateSensors() override; @@ -133,8 +134,6 @@ class ENGINE_API OpenBCIDevice : public OpenBCIDeviceBase static const char* GetRuleName() { return "DEVICE_OpenBCI"; } double GetSampleRate() const override { return SAMPLERATECYTON; } - double GetTimeoutLimit() const override { return 60; } // Long timeout limit because channel config takes so long - void CreateElectrodes() override; inline double GetImpedance(uint32 idx) override { return mImpedances[idx].imp; } diff --git a/src/Engine/Graph/ChannelSelectorNode.cpp b/src/Engine/Graph/ChannelSelectorNode.cpp index 7bac7ede6..cc9970f65 100644 --- a/src/Engine/Graph/ChannelSelectorNode.cpp +++ b/src/Engine/Graph/ChannelSelectorNode.cpp @@ -297,7 +297,6 @@ bool ChannelSelectorNode::IsChannelSelected(const ChannelBase* channel) const return false; } - // create the output ports (with empty multi channels) void ChannelSelectorNode::ReInitOutputPorts() { diff --git a/src/Engine/Graph/ViewNode.cpp b/src/Engine/Graph/ViewNode.cpp index 798a7e98a..f5a3ea22a 100644 --- a/src/Engine/Graph/ViewNode.cpp +++ b/src/Engine/Graph/ViewNode.cpp @@ -30,7 +30,6 @@ #include "../DSP/Spectrum.h" #include "../EngineManager.h" - using namespace Core; // constructor @@ -60,10 +59,11 @@ void ViewNode::Init() // SETUP PORTS // setup the input ports - InitInputPorts(3); + InitInputPorts(4); GetInputPort(INPUTPORT_DOUBLE).Setup("In", "x1", AttributeChannels::TYPE_ID, INPUTPORT_DOUBLE); GetInputPort(INPUTPORT_SPECTRUM).Setup("In (Spectrum)", "x2", AttributeChannels::TYPE_ID, INPUTPORT_SPECTRUM); GetInputPort(INPUTPORT_ENABLE).Setup("Enable", "enable", AttributeChannels::TYPE_ID, INPUTPORT_ENABLE); + GetInputPort(INPUTPORT_LENGTH).Setup("Length in mins", "length", AttributeChannels::TYPE_ID, INPUTPORT_LENGTH); // ATTRIBUTES @@ -100,6 +100,9 @@ void ViewNode::Init() AttributeSettings* attributeCombinedView = RegisterAttribute("Combined View", "combineView", "Display all input channels in a single chart.", ATTRIBUTE_INTERFACETYPE_CHECKBOX); attributeCombinedView->SetDefaultValue( AttributeBool::Create(defaultCombinedView) ); + AttributeSettings* fixedLength = RegisterAttribute( "Fixed Length in mins", "fixedLength", "Fix length in Seconds.", Core::ATTRIBUTE_INTERFACETYPE_CHECKBOX); + fixedLength->SetDefaultValue( AttributeBool::Create(false) ); + // sync buffer size to initial value SyncBufferSize(true); } @@ -145,6 +148,32 @@ void ViewNode::Update(const Time& elapsed, const Time& delta) } } + auto fixedLengthCache = mFixedLengthDuration; + InputPort& lengthPort = GetInputPort(INPUTPORT_LENGTH); + if (lengthPort.HasConnection() == false) + { + GetAttributeSettings(ATTRIB_LENGTH)->SetIsEnabled(true); + mFixedLengthDuration = -1.; + } + else + { + if (lengthPort.GetChannels()->GetNumChannels() > 0 && lengthPort.GetChannels()->GetChannel(0)->IsEmpty() == false) + { + GetAttributeSettings(ATTRIB_LENGTH)->SetIsEnabled(false); + const double sampleValue = lengthPort.GetChannels()->GetChannel(0)->AsType()->GetLastSample(); + if (sampleValue > 0.) + mFixedLengthDuration = sampleValue; + } + else + { + mFixedLengthDuration = -1.; + } + } + + // If the duration is changed, update buffer size + if (abs(fixedLengthCache - mFixedLengthDuration) > 0.0001) + SyncBufferSize(false); + // update scaling range (i.e. the auto modes need to recalculate the range) CalculateScalingRange(); @@ -195,6 +224,12 @@ void ViewNode::OnAttributesChanged() // show/hide color picker GetAttributeSettings(ATTRIB_COLORPICK)->SetVisible( CustomColor() ); + InputPort &lengthPort = GetInputPort(INPUTPORT_LENGTH); + if (FixedLength()) + lengthPort.SetVisible(true); + else + lengthPort.SetVisible(false); + EMIT_EVENT( OnAttributeUpdated(mParentGraph, this, GetAttributeValue(ATTRIB_COLORPICK)) ); } @@ -299,7 +334,7 @@ void ViewNode::CalculateScalingRange() for (uint32 i = 0; i < numChannels; ++i) { Channel* channel = GetDoubleChannel(i); - const uint32 numSamples = mViewDuration * channel->GetSampleRate(); + const uint32 numSamples = GetViewDuration() * channel->GetSampleRate(); Epoch epoch(channel, numSamples); epoch.SetPositionByOffset(0); @@ -315,7 +350,7 @@ void ViewNode::CalculateScalingRange() for (uint32 i = 0; i < numChannels; ++i) { Channel* channel = GetDoubleChannel(i); - const uint32 numSamples = mViewDuration * channel->GetSampleRate(); + const uint32 numSamples = GetViewDuration() * channel->GetSampleRate(); Epoch epoch(channel, numSamples); epoch.SetPositionByOffset(0); @@ -341,14 +376,16 @@ void ViewNode::SyncBufferSize(bool discard) InputPort& doublePort = GetInputPort(INPUTPORT_DOUBLE); InputPort& specPort = GetInputPort(INPUTPORT_SPECTRUM); InputPort& enablePort = GetInputPort(INPUTPORT_ENABLE); + InputPort& lengthPort = GetInputPort(INPUTPORT_LENGTH); MultiChannel* channels; + auto duration = GetViewDuration(); if (doublePort.HasConnection() && (channels = doublePort.GetChannels())) - channels->SetBufferSizeInSeconds(mViewDuration, discard); + channels->SetBufferSizeInSeconds(duration, discard); if (specPort.HasConnection() && (channels = specPort.GetChannels())) - channels->SetBufferSizeInSeconds(mViewDuration, discard); + channels->SetBufferSizeInSeconds(duration, discard); if (enablePort.HasConnection() && (channels = enablePort.GetChannels())) - channels->SetBufferSizeInSeconds(mViewDuration, discard); + channels->SetBufferSizeInSeconds(duration, discard); } diff --git a/src/Engine/Graph/ViewNode.h b/src/Engine/Graph/ViewNode.h index e8cca916c..9fbfa7eda 100644 --- a/src/Engine/Graph/ViewNode.h +++ b/src/Engine/Graph/ViewNode.h @@ -45,6 +45,7 @@ class ENGINE_API ViewNode : public SPNode INPUTPORT_DOUBLE = 0, INPUTPORT_SPECTRUM, INPUTPORT_ENABLE, + INPUTPORT_LENGTH, }; enum @@ -55,6 +56,7 @@ class ENGINE_API ViewNode : public SPNode ATTRIB_USECOLOR, ATTRIB_COLORPICK, ATTRIB_COMBINE, + ATTRIB_LENGTH, }; enum EScalingMode @@ -94,6 +96,9 @@ class ENGINE_API ViewNode : public SPNode double GetRangeMin(uint32 index) const { if (index < mRangesMin.Size()) return mRangesMin[index]; return 0.0; } double GetRangeMax(uint32 index) const { if (index < mRangesMax.Size()) return mRangesMax[index]; return 0.0; } + bool FixedLength() const { return GetBoolAttribute(ATTRIB_LENGTH); } // checks if length is fixed + double GetFixedLength() const { return mFixedLengthDuration; } // returns the length value + // get the enable flag bool GetEnableValue() const { return mEnableValue; } @@ -108,7 +113,7 @@ class ENGINE_API ViewNode : public SPNode Channel* GetSpectrumChannel(uint32 index); inline void SetViewDuration(double seconds) - { + { mViewDuration = seconds; SyncBufferSize(false); } @@ -118,6 +123,12 @@ class ENGINE_API ViewNode : public SPNode private: void CalculateScalingRange(); void SyncBufferSize(bool discard = false); + inline double GetViewDuration() + { + if (mFixedLengthDuration >= 0.) + return mFixedLengthDuration * 60.; // since this is in mins + return mViewDuration; + } double mViewDuration; double mMaxViewDuration; @@ -126,6 +137,7 @@ class ENGINE_API ViewNode : public SPNode Core::Array mRangesMax; bool mEnableValue; + double mFixedLengthDuration; }; diff --git a/src/Engine/Version.h b/src/Engine/Version.h index 8f9c6866a..5422dd6c2 100644 --- a/src/Engine/Version.h +++ b/src/Engine/Version.h @@ -26,6 +26,6 @@ #define NEUROMORE_ENGINE_VERSION_MAJOR 1 #define NEUROMORE_ENGINE_VERSION_MINOR 7 -#define NEUROMORE_ENGINE_VERSION_PATCH 3 +#define NEUROMORE_ENGINE_VERSION_PATCH 5 #endif diff --git a/src/QtBase/AttributeWidgets/PropertyManager.h b/src/QtBase/AttributeWidgets/PropertyManager.h index 915696f09..4f01f08a9 100644 --- a/src/QtBase/AttributeWidgets/PropertyManager.h +++ b/src/QtBase/AttributeWidgets/PropertyManager.h @@ -59,6 +59,8 @@ class QTBASE_API PropertyManager : public QObject // manual property adding Property* AddProperty(const char* groupName, const char* propertyName, AttributeWidget* attributeWidget, Core::Attribute* attributeValue, Core::AttributeSettings* settings, bool autoDelete=true, bool emitSignal=false); + Core::Array GetProperties() const { return mProperties; } + signals: void ValueChanged(Property* property); void PropertyAdded(const char* groupName, Property* property); diff --git a/src/QtBase/Backend/FilesCreateResponse.cpp b/src/QtBase/Backend/FilesCreateResponse.cpp index d6befed7a..16ef5723c 100644 --- a/src/QtBase/Backend/FilesCreateResponse.cpp +++ b/src/QtBase/Backend/FilesCreateResponse.cpp @@ -17,7 +17,21 @@ using namespace Core; // constructor FilesCreateResponse::FilesCreateResponse(QNetworkReply* reply) : Response(reply) { - // check for errors - if (mHasError == true) - return; + // data + Json::Item dataItem = mJson.Find("data"); + if (dataItem.IsNull() == true) + { + mHasError = true; + return; + } + + // get the data chunk id + Json::Item fileIdItem = dataItem.Find("fileId"); + if (fileIdItem.IsNull() == true) + { + mHasError = true; + return; + } + + mFileId = fileIdItem.GetString(); } diff --git a/src/QtBase/Backend/FilesCreateResponse.h b/src/QtBase/Backend/FilesCreateResponse.h index 5bc1a6e02..10cabb0ba 100644 --- a/src/QtBase/Backend/FilesCreateResponse.h +++ b/src/QtBase/Backend/FilesCreateResponse.h @@ -17,6 +17,8 @@ class QTBASE_API FilesCreateResponse : public Response public: // constructor FilesCreateResponse(QNetworkReply* reply); + + const char* mFileId; }; diff --git a/src/QtBase/PluginSystem/Plugin.h b/src/QtBase/PluginSystem/Plugin.h index 6436fc790..bb5c6dba4 100644 --- a/src/QtBase/PluginSystem/Plugin.h +++ b/src/QtBase/PluginSystem/Plugin.h @@ -91,7 +91,7 @@ class QTBASE_API Plugin : public QObject, public Core::AttributeSet void CreateBaseInterface(const char* objectName); inline DockWidget* GetDockWidget() { return mDock; } - + // attributes & settings void SaveSettings(Core::Json& json, Core::Json::Item& pluginItem) { AttributeSet::Write( json, pluginItem, true ); } bool LoadSettings(const Core::Json& json, const Core::Json::Item& item) { return AttributeSet::Read(json, item, true); } diff --git a/src/QtBase/Resources/Layouts/ExperiencePlayer.layout b/src/QtBase/Resources/Layouts/ExperiencePlayer.layout index 13382f884..c294f9733 100644 --- a/src/QtBase/Resources/Layouts/ExperiencePlayer.layout +++ b/src/QtBase/Resources/Layouts/ExperiencePlayer.layout @@ -1,78 +1,104 @@ -{ - "versionHigh": 1, - "versionLow": 0, - "compileDate": "Jul 11 2023", - "mainWindow": { - "posX": 0, - "posY": 0, - "width": 1920, - "height": 1080, - "isMaximized": true, - "geometry": "AdnQywADAAD/////////+AAACgAAAAVwAAAA3AAAAIEAAAbkAAAD5wAAAAACAAAACgAAAAAAAAAAFwAACf8AAAVv", - "state": "AAAA/wAAAAD9AAAAAQAAAAIAAAoAAAAFQ/wBAAAAAvwAAAAAAAABXQAAASkA/////AIAAAAI+wAAACoAUABMAFUARwBJAE4AMQA3ADQAOQA5ADIAOQA2ADIAMQAxADcANwA1ADUBAAAAFQAAAmcAAAAAAAAAAPsAAAAoAFAATABVAEcASQBOADEAMgA4ADUAOQA5ADkANwA2ADIAMgAwADQAOAAAAAAUAAABRwAAAAAAAAAA/AAAABYAAAOIAAABXAD////6AAAAAAEAAAAC+wAAAEgAMABjAGIAOABjADAAMwAyAC0AMgBlADQANgAtADQANgBjAGIALQA4ADkAYgA0AC0ANwAzADEAYwA2ADMAMQBhADAANgA5AGQBAAAAAP////8AAAEpAP////sAAABIADcAOQAzAGMANwBlADQAMgAtADIAOQBiAGUALQA0ADkAMwAzAC0AYQA2AGEAOQAtAGIAYwA3AGIAMgAyAGUAYgA0AGQAMwAzAQAAAAAAAAFyAAAAAAAAAAD7AAAASAAxADIANgBjADgAZgBjADMALQA4ADkAMwA1AC0ANAA5ADAAYwAtAGIAZQA3ADkALQA2ADMAZAA2ADMAZQAxADAAMwAyADgAMgAAAAMQAAABLQAAAAAAAAAA+wAAACoAUABMAFUARwBJAE4AMQA1ADUAMwAxADMAMQAyADUANwAxADYAMAA2ADgBAAADogAAAbcAAACqAP////sAAAAoAFAATABVAEcASQBOADEAOAAwADIAMwAyADQAOQA3ADYAMgA4ADkAOQAAAAK4AAABTwAAAAAAAAAA+wAAACgAUABMAFUARwBJAE4ANwAzADkAMwAzADAAOAAzADQAMQA3ADEAMQA4AAAAAYYAAAGDAAAAAAAAAAD7AAAAKABQAEwAVQBHAEkATgAxADkANgAwADEAOQAzADgAOQAxADYANQAzADIAAAACnwAAAWQAAAAAAAAAAPwAAAFhAAAInwAAAQkA/////AEAAAAD/AAAAWEAAAdYAAAAgQD////8AgAAAAb7AAAAKABQAEwAVQBHAEkATgAyADMAOQAzADAANQA3ADQAMAAxADcANQA4ADgAAAAAFgAAAKQAAAAAAAAAAPsAAABIADMAMQA5ADkAMAA5ADUAYgAtADgAMAA0ADYALQA0AGEAZQA4AC0AOAA2AGMAMgAtAGQAMwA2AGQAMAA5AGIAOQBiADMAMQBlAAAAABYAAAJ1AAAAAAAAAAD7AAAASABkADYAMQBlADUAYgAwAGEALQBmAGYAMAA1AC0ANAA1ADQAMwAtAGEANQBiADEALQA1ADAAMQBkADYAYQAwADIAMQAwAGYANQEAAAAWAAAB0AAAAFAA////+wAAAEgAOABmADUAOQBhADMAZgBlAC0AZQA2ADYAZAAtADQAYgA4ADYALQA5AGYANQBlAC0AMgBkAGYAYgA2ADUAOQA1AGIAZQBjADABAAAB6gAAA28AAABkAP////sAAAAoAFAATABVAEcASQBOADQAMQAwADMAMQA0ADEANwAyADEAMAA2ADUANwEAAAFQAAADIQAAAAAAAAAA/AAAAUYAAAMrAAAAAAD////6AAAAAAEAAAAF+wAAACYAUABMAFUARwBJAE4AMQA5ADEANAA3ADcANgAzADEANAAwADgAOAEAAAAA/////wAAAAAAAAAA+wAAACoAUABMAFUARwBJAE4AMgAyADQANwA3ADIAMwA1ADgAMgAxADAANAA4ADEAAAAAAP////8AAAAAAAAAAPsAAAAmAFAATABVAEcASQBOADEAMAA0ADIAMgA2ADEANwA0ADgANQA5ADAAAAAAAP////8AAAAAAAAAAPsAAAAqAFAATABVAEcASQBOADIANAA3ADYAMwAxADAAOAAxADkAMwAyADIANAA4AAAAAAD/////AAAAAAAAAAD7AAAAKgBQAEwAVQBHAEkATgAzADAAMwAwADUAMwAyADUAOAAyADMAMgA2ADcANwAAAAGdAAAF4wAAAAAAAAAA/AAACL0AAAFDAAAAhAD////8AgAAAAr7AAAANABHAHIAYQBwAGgAUABsAHUAZwBpAG4AOgA6AFAAYQBsAGUAdAB0AGUAVwBpAGQAZwBlAHQBAAAByQAAAH4AAAAAAAAAAPsAAAA6AEcAcgBhAHAAaABQAGwAdQBnAGkAbgA6ADoAQQB0AHQAcgBpAGIAdQB0AGUAcwBXAGkAZABnAGUAdAEAAAJLAAAApQAAAAAAAAAA/AAAABYAAAVDAAAAXAD////8AQAAAAL7AAAAKgBQAEwAVQBHAEkATgAyADEANgAxADYAMgAwADIANAA3ADEAMwA0ADQANQAAAAYjAAAAvwAAAAAAAAAA+wAAAEgAOABhAGUAYgBlADMANQBiAC0ANgA2AGUAMQAtADQAYwBkADAALQA4ADMANQBhAC0AZgBkADMANgAzADcAZQA0ADMAMgA1AGEBAAAIvQAAAUMAAACEAP////sAAAA2AEcAcgBhAHAAaABQAGwAdQBnAGkAbgA6ADoAUgBlAHMAbwB1AHIAYwBlAFcAaQBkAGcAZQB0AQAAA5cAAADaAAAAAAAAAAD7AAAAKgBQAEwAVQBHAEkATgAyADEANgAzADEAMwAwADUAMQAyADIAOQA0ADQAMQEAAAI5AAAAdgAAAAAAAAAA+wAAAD4AQwBsAGEAcwBzAGkAZgBpAGUAcgBQAGwAdQBnAGkAbgA6ADoAUABhAGwAZQB0AHQAZQBXAGkAZABnAGUAdAEAAAJUAAAAggAAAAAAAAAA+wAAAEQAQwBsAGEAcwBzAGkAZgBpAGUAcgBQAGwAdQBnAGkAbgA6ADoAQQB0AHQAcgBpAGIAdQB0AGUAcwBXAGkAZABnAGUAdAEAAALaAAAAxAAAAAAAAAAA+wAAAEAAQwBsAGEAcwBzAGkAZgBpAGUAcgBQAGwAdQBnAGkAbgA6ADoAUgBlAHMAbwB1AHIAYwBlAFcAaQBkAGcAZQB0AQAAA6IAAADPAAAAAAAAAAD7AAAAOABDAGwAYQBzAHMAaQBmAGkAZQByAFAAbAB1AGcAaQBuADoAOgBBAHQAdAByAGkAYgB1AHQAZQBzAQAAAZgAAAFJAAAAAAAAAAD7AAAANgBDAGwAYQBzAHMAaQBmAGkAZQByAFAAbAB1AGcAaQBuADoAOgBSAGUAcwBvAHUAcgBjAGUAcwEAAALlAAAA9gAAAAAAAAAA+wAAACgAUABMAFUARwBJAE4AMQA3ADYAMAAxADkANgA5ADMAMQA3ADcAMQA5AgAABJwAAAA9AAAB9AAAAkIAAAoAAAAAAAAAAAQAAAAEAAAACAAAAAj8AAAAAA==" - }, - "plugins": [ - { - "objectName": "PLUGIN155313125716068", - "typeUuid": "c38f75d4-7eca-11e4-b4a9-0800200c9a66", - "attributes": [ - { - "internalName": "settingsWindowVisibility", - "value": false - } - ] - }, - { - "objectName": "0cb8c032-2e46-46cb-89b4-731c631a069d", - "typeUuid": "c38f75e1-7eca-11e4-b4a9-0800200c9a66", - "attributes": [ - { - "internalName": "settingsWindowVisibility", - "value": false - } - ] - }, - { - "objectName": "8aebe35b-66e1-4cd0-835a-fd3637e4325a", - "typeUuid": "a9f94048-a277-11e5-bf7f-feff819cdc9f", - "attributes": [ - { - "internalName": "settingsWindowVisibility", - "value": false - } - ] - }, - { - "objectName": "d61e5b0a-ff05-4543-a5b1-501d6a0210f5", - "typeUuid": "1aea9056-26e3-11e5-b345-feff819cdc9f", - "attributes": [ - { - "internalName": "settingsWindowVisibility", - "value": false - } - ] - }, - { - "objectName": "8f59a3fe-e66d-4b86-9f5e-2dfb6595bec0", - "typeUuid": "30ca8a46-321b-11e5-a151-feff819cdc9f", - "attributes": [ - { - "internalName": "settingsWindowVisibility", - "value": false - }, - { - "internalName": "timeRange", - "value": 10.0 - }, - { - "internalName": "style", - "value": 4 - }, - { - "internalName": "showLatencyMarker", - "value": false - } - ] - } - ] +{ + "versionHigh": 1, + "versionLow": 0, + "compileDate": "Feb 7 2025", + "mainWindow": { + "posX": -1, + "posY": -9, + "width": 1920, + "height": 991, + "isMaximized": true, + "geometry": "AdnQywADAAD/////////9wAAB4AAAAP8AAAAyAAAAPgAAAZWAAAC8AAAAAACAAAAB4AAAAAAAAAAHQAAB38AAAP7", + "state": "AAAA/wAAAAD9AAAAAQAAAAIAAAeAAAADx/wBAAAAAvwAAAAAAAABzgAAAb8A/////AIAAAAJ+wAAACoAUABMAFUARwBJAE4AMQA3ADQAOQA5ADIAOQA2ADIAMQAxADcANwA1ADUBAAAAFQAAAmcAAAAAAAAAAPsAAAAoAFAATABVAEcASQBOADEAMgA4ADUAOQA5ADkANwA2ADIAMgAwADQAOAAAAAAUAAABRwAAAAAAAAAA/AAAABgAAAGEAAABLgD////6AAAAAAEAAAAC+wAAAEgAMABjAGIAOABjADAAMwAyAC0AMgBlADQANgAtADQANgBjAGIALQA4ADkAYgA0AC0ANwAzADEAYwA2ADMAMQBhADAANgA5AGQBAAAAAP////8AAAG/AP////sAAABIADcAOQAzAGMANwBlADQAMgAtADIAOQBiAGUALQA0ADkAMwAzAC0AYQA2AGEAOQAtAGIAYwA3AGIAMgAyAGUAYgA0AGQAMwAzAQAAAAAAAAFyAAAAAAAAAAD7AAAASABkADYAMQBlADUAYgAwAGEALQBmAGYAMAA1AC0ANAA1ADQAMwAtAGEANQBiADEALQA1ADAAMQBkADYAYQAwADIAMQAwAGYANQEAAAGhAAABTAAAAFAA////+wAAAEgAMQAyADYAYwA4AGYAYwAzAC0AOAA5ADMANQAtADQAOQAwAGMALQBiAGUANwA5AC0ANgAzAGQANgAzAGUAMQAwADMAMgA4ADIAAAADEAAAAS0AAAAAAAAAAPsAAAAqAFAATABVAEcASQBOADEANQA1ADMAMQAzADEAMgA1ADcAMQA2ADAANgA4AQAAAvIAAADtAAAArgD////7AAAAKABQAEwAVQBHAEkATgAxADgAMAAyADMAMgA0ADkANwA2ADIAOAA5ADkAAAACuAAAAU8AAAAAAAAAAPsAAAAoAFAATABVAEcASQBOADcAMwA5ADMAMwAwADgAMwA0ADEANwAxADEAOAAAAAGGAAABgwAAAAAAAAAA+wAAACgAUABMAFUARwBJAE4AMQA5ADYAMAAxADkAMwA4ADkAMQA2ADUAMwAyAAAAAp8AAAFkAAAAAAAAAAD8AAAB0wAABa0AAALdAP////wBAAAAA/wAAAHTAAAEKAAAAlQA/////AIAAAAG+wAAACgAUABMAFUARwBJAE4AMgAzADkAMwAwADUANwA0ADAAMQA3ADUAOAA4AAAAABYAAACkAAAAAAAAAAD7AAAASAAzADEAOQA5ADAAOQA1AGIALQA4ADAANAA2AC0ANABhAGUAOAAtADgANgBjADIALQBkADMANgBkADAAOQBiADkAYgAzADEAZQAAAAAWAAACdQAAAAAAAAAA+wAAAEgAOABmADUAOQBhADMAZgBlAC0AZQA2ADYAZAAtADQAYgA4ADYALQA5AGYANQBlAC0AMgBkAGYAYgA2ADUAOQA1AGIAZQBjADABAAAAGAAAAecAAABkAP////sAAABIADQAYwBjADcAOQA0AGYAZQAtADQAYwAxADEALQA0AGUAYgAxAC0AYgA5AGQAZgAtAGEAYgBiADMANwA3ADUAZgA5ADQAMAA3AQAAAgQAAAHbAAAAzAD////7AAAAKABQAEwAVQBHAEkATgA0ADEAMAAzADEANAAxADcAMgAxADAANgA1ADcBAAABUAAAAyEAAAAAAAAAAPwAAAFGAAADKwAAAAAA////+gAAAAABAAAABfsAAAAmAFAATABVAEcASQBOADEAOQAxADQANwA3ADYAMwAxADQAMAA4ADgBAAAAAP////8AAAAAAAAAAPsAAAAqAFAATABVAEcASQBOADIAMgA0ADcANwAyADMANQA4ADIAMQAwADQAOAAxAAAAAAD/////AAAAAAAAAAD7AAAAJgBQAEwAVQBHAEkATgAxADAANAAyADIANgAxADcANAA4ADUAOQAwAAAAAAD/////AAAAAAAAAAD7AAAAKgBQAEwAVQBHAEkATgAyADQANwA2ADMAMQAwADgAMQA5ADMAMgAyADQAOAAAAAAA/////wAAAAAAAAAA+wAAACoAUABMAFUARwBJAE4AMwAwADMAMAA1ADMAMgA1ADgAMgAzADIANgA3ADcAAAABnQAABeMAAAAAAAAAAPwAAAYAAAABgAAAAIQA/////AIAAAAK+wAAADQARwByAGEAcABoAFAAbAB1AGcAaQBuADoAOgBQAGEAbABlAHQAdABlAFcAaQBkAGcAZQB0AQAAAckAAAB+AAAAAAAAAAD7AAAAOgBHAHIAYQBwAGgAUABsAHUAZwBpAG4AOgA6AEEAdAB0AHIAaQBiAHUAdABlAHMAVwBpAGQAZwBlAHQBAAACSwAAAKUAAAAAAAAAAPwAAAAYAAADxwAAAFwA/////AEAAAAC+wAAACoAUABMAFUARwBJAE4AMgAxADYAMQA2ADIAMAAyADQANwAxADMANAA0ADUAAAAGIwAAAL8AAAAAAAAAAPsAAABIADgAYQBlAGIAZQAzADUAYgAtADYANgBlADEALQA0AGMAZAAwAC0AOAAzADUAYQAtAGYAZAAzADYAMwA3AGUANAAzADIANQBhAQAABgAAAAGAAAAAhAD////7AAAANgBHAHIAYQBwAGgAUABsAHUAZwBpAG4AOgA6AFIAZQBzAG8AdQByAGMAZQBXAGkAZABnAGUAdAEAAAOXAAAA2gAAAAAAAAAA+wAAACoAUABMAFUARwBJAE4AMgAxADYAMwAxADMAMAA1ADEAMgAyADkANAA0ADEBAAACOQAAAHYAAAAAAAAAAPsAAAA+AEMAbABhAHMAcwBpAGYAaQBlAHIAUABsAHUAZwBpAG4AOgA6AFAAYQBsAGUAdAB0AGUAVwBpAGQAZwBlAHQBAAACVAAAAIIAAAAAAAAAAPsAAABEAEMAbABhAHMAcwBpAGYAaQBlAHIAUABsAHUAZwBpAG4AOgA6AEEAdAB0AHIAaQBiAHUAdABlAHMAVwBpAGQAZwBlAHQBAAAC2gAAAMQAAAAAAAAAAPsAAABAAEMAbABhAHMAcwBpAGYAaQBlAHIAUABsAHUAZwBpAG4AOgA6AFIAZQBzAG8AdQByAGMAZQBXAGkAZABnAGUAdAEAAAOiAAAAzwAAAAAAAAAA+wAAADgAQwBsAGEAcwBzAGkAZgBpAGUAcgBQAGwAdQBnAGkAbgA6ADoAQQB0AHQAcgBpAGIAdQB0AGUAcwEAAAGYAAABSQAAAAAAAAAA+wAAADYAQwBsAGEAcwBzAGkAZgBpAGUAcgBQAGwAdQBnAGkAbgA6ADoAUgBlAHMAbwB1AHIAYwBlAHMBAAAC5QAAAPYAAAAAAAAAAPsAAAAoAFAATABVAEcASQBOADEANwA2ADAAMQA5ADYAOQAzADEANwA3ADEAOQIAAAScAAAAPQAAAfQAAAJCAAAHgAAAAAAAAAAEAAAABAAAAAgAAAAI/AAAAAA=" + }, + "plugins": [ + { + "objectName": "PLUGIN155313125716068", + "typeUuid": "c38f75d4-7eca-11e4-b4a9-0800200c9a66", + "attributes": [ + { + "internalName": "settingsWindowVisibility", + "value": false + } + ] + }, + { + "objectName": "0cb8c032-2e46-46cb-89b4-731c631a069d", + "typeUuid": "c38f75e1-7eca-11e4-b4a9-0800200c9a66", + "attributes": [ + { + "internalName": "settingsWindowVisibility", + "value": false + } + ] + }, + { + "objectName": "8aebe35b-66e1-4cd0-835a-fd3637e4325a", + "typeUuid": "a9f94048-a277-11e5-bf7f-feff819cdc9f", + "attributes": [ + { + "internalName": "settingsWindowVisibility", + "value": false + } + ] + }, + { + "objectName": "d61e5b0a-ff05-4543-a5b1-501d6a0210f5", + "typeUuid": "1aea9056-26e3-11e5-b345-feff819cdc9f", + "attributes": [ + { + "internalName": "settingsWindowVisibility", + "value": false + } + ] + }, + { + "objectName": "8f59a3fe-e66d-4b86-9f5e-2dfb6595bec0", + "typeUuid": "30ca8a46-321b-11e5-a151-feff819cdc9f", + "attributes": [ + { + "internalName": "settingsWindowVisibility", + "value": false + }, + { + "internalName": "timeRange", + "value": 10.0 + }, + { + "internalName": "style", + "value": 4 + }, + { + "internalName": "showLatencyMarker", + "value": false + } + ] + }, + { + "objectName": "4cc794fe-4c11-4eb1-b9df-abb3775f9407", + "typeUuid": "c38fc3f4-7eca-11e4-b4a9-0800200c9a66", + "attributes": [ + { + "internalName": "settingsWindowVisibility", + "value": false + }, + { + "internalName": "multiView", + "value": true + }, + { + "internalName": "show0HzBin", + "value": false + }, + { + "internalName": "averageInterval", + "value": 1.0 + }, + { + "internalName": "horizontalView", + "value": true + } + ] + } + ] } \ No newline at end of file diff --git a/src/QtBase/Version.h b/src/QtBase/Version.h index 97bcc4252..4fc79d2dd 100644 --- a/src/QtBase/Version.h +++ b/src/QtBase/Version.h @@ -9,6 +9,6 @@ #define NEUROMORE_QTBASE_VERSION_MAJOR 1 #define NEUROMORE_QTBASE_VERSION_MINOR 7 -#define NEUROMORE_QTBASE_VERSION_PATCH 3 +#define NEUROMORE_QTBASE_VERSION_PATCH 5 #endif \ No newline at end of file diff --git a/src/Studio/AppManager.cpp b/src/Studio/AppManager.cpp index 8ba553635..f47ee90d2 100644 --- a/src/Studio/AppManager.cpp +++ b/src/Studio/AppManager.cpp @@ -24,6 +24,8 @@ // include precompiled header #include #include +#include +#include // include the required headers #include "AppManager.h" @@ -80,7 +82,8 @@ AppManager::AppManager(int argc, char* argv[]) // create the log file callback String logFilename = GetLogFilename(); - CORE_LOGMANAGER.CreateLogFile( logFilename.AsChar() ); + if (QDir().mkpath(QFileInfo(logFilename.AsChar()).absolutePath())) + CORE_LOGMANAGER.CreateLogFile( logFilename.AsChar() ); // log header LogInfo(); diff --git a/src/Studio/AppManager.h b/src/Studio/AppManager.h index c50c864dd..a7a0a1e77 100644 --- a/src/Studio/AppManager.h +++ b/src/Studio/AppManager.h @@ -42,6 +42,7 @@ #include #include #include +#include // forward declarations QT_FORWARD_DECLARE_CLASS(QSplashScreen) @@ -108,7 +109,8 @@ class AppManager : public QObject void LogInfo(); - Core::String GetLogFilename() const { return QtBaseManager::GetAppDataFolder() + GetAppShortName() + "_Log.txt"; } + Core::String GetLogFilename() const { return QtBaseManager::GetAppDataFolder() + "Logs/" + GetAppShortName() + + FromQtString(QDateTime::currentDateTimeUtc().toString("_yyyy-MM-dd_hh-mm-ss")) + ".txt"; } // splash screen void SetSplashScreenMessage(const char* text) { mSplashScreen->showMessage(text, Qt::AlignBottom | Qt::AlignHCenter, Qt::white); } diff --git a/src/Studio/Plugins/BackendFileSystem/BackendFileSystemWidget.cpp b/src/Studio/Plugins/BackendFileSystem/BackendFileSystemWidget.cpp index 32decd8b2..059035e7c 100644 --- a/src/Studio/Plugins/BackendFileSystem/BackendFileSystemWidget.cpp +++ b/src/Studio/Plugins/BackendFileSystem/BackendFileSystemWidget.cpp @@ -41,6 +41,8 @@ #include #include #include +#include +#include using namespace Core; @@ -303,7 +305,7 @@ void BackendFileSystemWidget::UpdateItems(QTreeWidgetItem* item) // reload file hiarchy from -void BackendFileSystemWidget::Refresh() +void BackendFileSystemWidget::Refresh(const QString& localfolder, const QString& cloudfolder, const bool xprun) { if (GetAuthenticationCenter()->IsInterfaceAllowed() == false) return; @@ -315,7 +317,7 @@ void BackendFileSystemWidget::Refresh() // 2. process request and connect to the reply QNetworkReply* reply = GetBackendInterface()->GetNetworkAccessManager()->ProcessRequest( request, Request::UIMODE_SILENT ); - connect(reply, &QNetworkReply::finished, this, [reply, this]() + connect(reply, &QNetworkReply::finished, this, [reply, this, localfolder, cloudfolder, xprun]() { QNetworkReply* networkReply = qobject_cast( sender() ); @@ -330,6 +332,10 @@ void BackendFileSystemWidget::Refresh() ReInit(); UpdateInterface(); + + // continue folder upload after folder creation (if any is pending) + if (!localfolder.isEmpty() && !cloudfolder.isEmpty()) + UploadFolder(localfolder, cloudfolder, xprun); }); } @@ -811,6 +817,21 @@ void BackendFileSystemWidget::OnContextMenu(const QPoint& point) QAction* removeAction = menu.addAction("&Delete Folder", this, SLOT(OnRemoveItem()) ); removeAction->setIcon( GetQtBaseManager()->FindIcon("Images/Icons/Minus.png") ); } + + if (singleSelectedItem->GetCreud().Read() == true) + { + // save JSON to disk + QAction* saveToDiskAction = menu.addAction("Download", this, SLOT(OnSaveToDisk())); + saveToDiskAction->setIcon(GetQtBaseManager()->FindIcon("Images/Icons/SaveAs.png")); + } + + if (singleSelectedItem->GetCreud().Create() == true) + { + // upload JSON to cloud + QAction* uploadAction = menu.addAction("Upload", this, SLOT(OnUploadFromDisk())); + uploadAction->setIcon(GetQtBaseManager()->FindIcon("Images/Icons/Cloud.png")); + } + } else { @@ -846,13 +867,16 @@ void BackendFileSystemWidget::OnContextMenu(const QPoint& point) copyJsonToClipboardAction->setIcon( GetQtBaseManager()->FindIcon("Images/Icons/Copy.png") ); // save JSON to disk - QAction* saveToDiskAction = menu.addAction("Save To HDD", this, SLOT(OnSaveToDisk()) ); + QAction* saveToDiskAction = menu.addAction("Download", this, SLOT(OnSaveToDisk()) ); saveToDiskAction->setIcon( GetQtBaseManager()->FindIcon("Images/Icons/SaveAs.png") ); + if (singleSelectedItem->GetCreud().Update() == true) + { // load JSON from disk and save it to the cloud - QAction* loadFromDiskSaveToCloudAction = menu.addAction("Upload local file to cloud", this, SLOT(OnLoadFromDiskAndSaveToCloud()) ); + QAction* loadFromDiskSaveToCloudAction = menu.addAction("Upload", this, SLOT(OnUploadFromDisk()) ); loadFromDiskSaveToCloudAction->setIcon( GetQtBaseManager()->FindIcon("Images/Icons/Cloud.png") ); - + } + menu.addSeparator(); // revisions @@ -1136,137 +1160,548 @@ void BackendFileSystemWidget::OnCopyJsonToClipboard() // save JSON to a disk on the local hard drive void BackendFileSystemWidget::OnSaveToDisk() { - // get the selected item and return directly in case no single selected item is selected - SelectionItem* selectedItem = GetSingleSelectionItem(); - if (selectedItem == NULL || selectedItem->IsFolder() == true) - { - QMessageBox::critical( this, "ERROR", "Cannot save graph JSON to local hard disk. No or multiple files selected." ); - return; - } + const QList selectedItems = + mTreeWidget->selectedItems(); - - // get the filename where to save it - QString defaultFileName = mLastSelectedFileDialogFolder + "\\" + selectedItem->GetName(); - QString selectedFilter; - QFileDialog::Options options; - QString filename = QFileDialog::getSaveFileName( this, // parent - "Save", // caption - defaultFileName, // suggested file name and directory - "JSON (*.json)", - &selectedFilter, - options ); - if (filename.isEmpty() == true) - return; + // only single selection supported + if (selectedItems.count() != 1) + return; - mLastSelectedFileDialogFolder = QFileInfo(filename).absolutePath(); + QTreeWidgetItem* rootItem = selectedItems[0]; + SelectionItem rootModel = CreateSelectionItem(rootItem); - // save EXPERIENCE - Experience* experience = GetEngine()->GetActiveExperience(); - if (experience != NULL && experience->GetUuidString().IsEqual(selectedItem->GetUuidString()) == true) - { - // save the experience - Json jsonParser; - Json::Item rootItem = jsonParser.GetRootItem(); - experience->Save(jsonParser, rootItem); // Note: add parameters to export whole design incl. attached classifier/statemachine here - bool result = jsonParser.WriteToFile(FromQtString(filename).AsChar(), true); - if (result == false) - { - QMessageBox::critical( this, "ERROR", "Cannot save design JSON to local hard disk." ); - return; - } - } - // save GRAPH - else - { - // find the graph based on the uuid - Graph* graph = GetGraphManager()->FindGraphByUuid( selectedItem->GetUuid() ); - if (graph == NULL) - { - QMessageBox::critical( this, "ERROR", "Cannot save graph JSON to local hard disk. Different file selected than the shown graph in the graph window." ); - return; - } + // download single file + if (!rootModel.IsFolder()) + { + // show file selection dialog + const QString defaultName = mLastSelectedFileDialogFolder + '/' + rootModel.GetName() + rootModel.GetExtension().AsChar(); + const QString ext((rootModel.GetTypeString() + " (*" + rootModel.GetExtension() + ")").AsChar()); + const QString filename = QFileDialog::getSaveFileName(this, + "Save", defaultName, ext); + + if (filename.isEmpty()) + return; + + // remember selected path + mLastSelectedFileDialogFolder = QFileInfo(filename).absolutePath(); + + // download file + FilesGetRequest request(GetUser()->GetToken(), rootModel.GetUuid()); + QNetworkReply* reply = GetBackendInterface()->GetNetworkAccessManager()->ProcessRequest(request); + connect(reply, &QNetworkReply::finished, this, [reply, this, filename, rootModel]() + { + Json json; + QNetworkReply* networkReply = qobject_cast(sender()); + FilesGetResponse response(networkReply); + if (response.HasError()) + { + QMessageBox::warning(this, "Error", "Download failed", QMessageBox::Ok); + return; + } + if (!json.Parse(response.GetJsonContent())) + { + QMessageBox::warning(this, "Error", "JSON Parse failed", QMessageBox::Ok); + return; + } - // save the currently shown graph and save it as a string - bool result = GraphExporter::Save( FromQtString(filename).AsChar(), graph ); - if (result == false) - { - QMessageBox::critical( this, "ERROR", "Cannot save graph JSON to local hard disk. Saving the graph failed." ); - return; - } - } + // add or update uuid + Core::String jsonstr; + auto jsonitm = json.GetRootItem().Find("uuid"); + if (!jsonitm.IsNull()) jsonitm.SetString(rootModel.GetUuid()); + else json.GetRootItem().AddString("uuid", rootModel.GetUuid()); + if (!json.WriteToFile(filename.toLatin1().data(), true)) + { + QMessageBox::warning(this, "Error", "File Write failed", QMessageBox::Ok); + return; + } + }); + } + // download folder + else + { + if (!rootItem->childCount()) + return; + // show folder selection dialog + const QString defaultName = mLastSelectedFileDialogFolder + '/' + rootModel.GetName(); + const QString folder = QFileDialog::getExistingDirectory( + this, "Select Folder", defaultName); - + if (folder.isEmpty()) + return; + + // remember selected path + mLastSelectedFileDialogFolder = QFileInfo(folder).absolutePath(); + + // shared root path of all elements + const Core::String& rootPath = rootModel.GetPathString(); + const uint32 rootPathLength = rootPath.GetLength(); + + SelectionItem model; + QDir dir; + std::vector stack; + Core::String path; + + // start with root folder + stack.push_back(rootItem); + + // tree traversal + while (!stack.empty()) + { + // pop next + auto* itm = stack.back(); + stack.pop_back(); + + // parse model from qt instance + model = CreateSelectionItem(itm); + + // folder + if (model.IsFolder()) + { + // add subitems + int num = itm->childCount(); + for (int i = 0; i < num; i++) + stack.push_back(itm->child(i)); + } + + // file + else + { + path = model.GetPathString(); + path.Remove(0, rootPathLength); + path = FromQtString(folder) + '/' + path; + + if (dir.mkpath(path.AsChar())) + { + const Core::String filename = path + model.GetNameString() + model.GetExtension(); + FilesGetRequest request(GetUser()->GetToken(), model.GetUuid()); + QNetworkReply* reply = GetBackendInterface()->GetNetworkAccessManager()->ProcessRequest(request); + connect(reply, &QNetworkReply::finished, this, [reply, this, filename, model]() + { + Json json; + QNetworkReply* networkReply = qobject_cast(sender()); + FilesGetResponse response(networkReply); + if (response.HasError()) + { + QMessageBox::warning(this, "Error", "Download failed", QMessageBox::Ok); + return; + } + if (!json.Parse(response.GetJsonContent())) + { + QMessageBox::warning(this, "Error", "JSON Parse failed", QMessageBox::Ok); + return; + } + + // add or update uuid + Core::String jsonstr; + auto jsonitm = json.GetRootItem().Find("uuid"); + if (!jsonitm.IsNull()) jsonitm.SetString(model.GetUuid()); + else json.GetRootItem().AddString("uuid", model.GetUuid()); + + if (!json.WriteToFile(filename.AsChar(), true)) + { + QMessageBox::warning(this, "Error", "File Write failed", QMessageBox::Ok); + return; + } + }); + } + } + } + } } // load a JSON graph from disk and save it back to the backend -void BackendFileSystemWidget::OnLoadFromDiskAndSaveToCloud() +void BackendFileSystemWidget::OnUploadFromDisk() { - // get the filename where to save it - QFileDialog::Options options; - QString selectedFilter; - QString qtFilename = QFileDialog::getOpenFileName( this, // parent - "Open", // caption - "", // directory - "JSON (*.json)", - &selectedFilter, - options ); - - if (qtFilename.isEmpty() == true) - return; + const QList selectedItems = + mTreeWidget->selectedItems(); - String filename = FromQtString(qtFilename); + // only single selection supported + if (selectedItems.count() != 1) + return; - SelectionItem selectedItem = GetSelectedItem(); + QTreeWidgetItem* rootItem = selectedItems[0]; + SelectionItem rootModel = CreateSelectionItem(rootItem); - // is experience - if (selectedItem.GetTypeString().IsEqualNoCase(ITEM_TYPE_EXPERIENCE) == true) - { - Json jsonParser; - if (jsonParser.ParseFile(filename.AsChar()) == false) - { - LogError( "BackendFileSystemWidget::OnLoadFromDiskAndSaveToCloud(): Cannot open file '%s'.", filename.AsChar()); - return; - } + // upload single file + if (!rootModel.IsFolder()) + { + // show file selection dialog + const QString defaultName = mLastSelectedFileDialogFolder + '/' + rootModel.GetName() + rootModel.GetExtension().AsChar(); + const QString ext((rootModel.GetTypeString() + " (*" + rootModel.GetExtension() + ")").AsChar()); + const QString filename = QFileDialog::getOpenFileName(this, + "Upload", defaultName, ext); + + if (filename.isEmpty()) + return; + + // remember selected path + mLastSelectedFileDialogFolder = QFileInfo(filename).absolutePath(); + + // load file + QFile f(filename); + if (!f.open(QFile::ReadOnly | QFile::Text)) { + QMessageBox::warning(this, "Error", "Failed to open file", QMessageBox::Ok); + return; + } + QTextStream in(&f); + QByteArray arr(f.readAll()); + + // verify json + Json json; + if (!json.Parse(arr.constData())) { + QMessageBox::warning(this, "Error", "Failed to parse JSON", QMessageBox::Ok); + return; + } - Experience* experience = new Experience(); - if (experience->Load(jsonParser, jsonParser.GetRootItem()) == false) - { - LogError( "BackendFileSystemWidget::OnLoadFromDiskAndSaveToCloud(): Cannot parse Json file '%s'.", filename.AsChar()); - return; - } - - GetBackendInterface()->GetFileSystem()->SaveExperience( GetUser()->GetToken(), selectedItem.GetUuid(), experience ); + // replace a possible uuid with one from backend + Core::String jsonstr; + auto jsonitm = json.GetRootItem().Find("uuid"); + if (!jsonitm.IsNull()) + jsonitm.SetString(rootModel.GetUuid()); + json.WriteToString(jsonstr, true); + + // upload + FilesUpdateRequest request(GetUser()->GetToken(), rootModel.GetUuid(), jsonstr.AsChar()); + QNetworkReply* reply = GetBackendInterface()->GetNetworkAccessManager()->ProcessRequest(request); + connect(reply, &QNetworkReply::finished, this, [reply, this]() + { + QNetworkReply* networkReply = qobject_cast(sender()); + FilesUpdateResponse response(networkReply); + if (response.HasError()) + { + QMessageBox::warning(this, "Error", "Upload failed", QMessageBox::Ok); + return; + } + this->Refresh(); + }); + } - delete experience; - } - // is classifier/statemachine - else - { - Graph* graph = NULL; - - if (selectedItem.GetTypeString().IsEqualNoCase(ITEM_TYPE_CLASSIFIER) == true) - graph = new Classifier(); - else if (selectedItem.GetTypeString().IsEqualNoCase(ITEM_TYPE_STATEMACHINE) == true) - graph = new StateMachine(); - else - return; + // upload folder + else + { + // show folder selection dialog + const QString defaultName = mLastSelectedFileDialogFolder + '/' + rootModel.GetName(); + const QString folder = QFileDialog::getExistingDirectory( + this, "Select Folder", defaultName); - if (GraphImporter::LoadFromFile(filename.AsChar(), graph) == false) - { - LogError( "BackendFileSystemWidget::OnLoadFromDiskAndSaveToCloud(): Cannot parse Json file '%s'.", filename.AsChar()); - return; - } - - GetBackendInterface()->GetFileSystem()->SaveGraph( GetUser()->GetToken(), selectedItem.GetUuid(), graph ); + if (folder.isEmpty()) + return; - delete graph; - } + // remember selected path + mLastSelectedFileDialogFolder = QFileInfo(folder).absolutePath(); + + // shared root path of all elements + const Core::String& rootPath = rootModel.GetPathString(); + const uint32 rootPathLength = rootPath.GetLength(); + + // prepare upload + mPendingUploads = 0; + mLocalUploadRoot = folder; + mCloudUploadRoot = rootPath.AsChar(); + mLookup.clear(); + + // start upload + this->UploadFolder(folder, rootPath.AsChar(), false); + } } +void BackendFileSystemWidget::ReplaceUuid(Core::Json& json, const char* internalName) +{ + auto jsonattribs = json.GetRootItem().Find("attributes"); + if (jsonattribs.IsArray()) + { + uint32 numattribs = jsonattribs.Size(); + for (uint32 i = 0; i < numattribs; i++) + { + auto jsoninternalname = jsonattribs[i].Find("internalName"); + auto jsonvalue = jsonattribs[i].Find("value"); + if (jsoninternalname.IsString() && jsonvalue.IsString()) + { + auto* sname = jsoninternalname.GetString(); + auto* olduuid = jsonvalue.GetString(); + if (std::string(internalName) == sname) + { + std::string newuuid = mLookup[olduuid]; + //qDebug() << "old uuid: " << olduuid << " new uuid: " << newuuid.c_str(); + if (!newuuid.empty()) + jsonvalue.SetString(newuuid.c_str()); + } + } + } + } +} + +QTreeWidgetItem* BackendFileSystemWidget::FindItemByPath(const QString& path, const QString& type) +{ + QStringList l = path.split("/"); + if (l.length() == 0) + return 0; + const int toplvls = mTreeWidget->topLevelItemCount(); + for (int i = 0; i < toplvls; i++) + { + QTreeWidgetItem* item = mTreeWidget->topLevelItem(i); + QString p = item->data(0, USERDATA_PATH).toString(); + if (p.length() && p[p.length()-1] == '/') + p.chop(1); + if (l[0] != p) + continue; + if (l.length() == 1 && type == "folder") + return item; + for (int j = 1; j < l.length(); j++) + { + bool found = false; + for (int k = 0; k < item->childCount(); k++) + { + auto* child = item->child(k); + QString childtype = child->data(0, USERDATA_TYPE).toString(); + QString wantedtype = j < l.length()-1 ? "folder" : type; + if (child->text(0) == l[j] && childtype == wantedtype) + { + item = item->child(k); + found = true; + break; + } + } + if (!found) + return 0; + } + return item; + } + return 0; +} + +void BackendFileSystemWidget::UploadFolder(const QString& plocal, const QString& pcloud, const bool xprun) +{ + QString pathlocal = plocal; + QString pathcloud = pcloud; + pathlocal.replace("//", "/"); + pathcloud.replace("//", "/"); + if (pathlocal.length() && pathlocal[pathlocal.length()-1] == '/') pathlocal.chop(1); + if (pathcloud.length() && pathcloud[pathcloud.length()-1] == '/') pathcloud.chop(1); + //qDebug() << "local: " << pathlocal << " cloud: " << pathcloud << " xprun: " << xprun; + + // iterate folders + QDirIterator dirit(pathlocal, QStringList(), QDir::Dirs); + while (dirit.hasNext()) + { + QString p = dirit.next(); + QFileInfo finf(p); + QString basename = finf.baseName(); + QString cloudfolder; + + // filter for "." and ".." + if (basename.isEmpty()) + continue; + + // build path on cloud + cloudfolder = pathcloud + '/' + basename; + cloudfolder.replace("//", "/"); + + // exists + if (auto* item = FindItemByPath(cloudfolder, "folder")) + UploadFolder(p, cloudfolder, xprun); + + // must be created + else if (auto* item = FindItemByPath(pathcloud, "folder")) + { + //qDebug() << "creating: " << pathcloud << " (" << "folder" << ")"; + SelectionItem model = CreateSelectionItem(item); + if (!model.GetCreud().Create()) + continue; + mPendingUploads++; + FoldersCreateRequest request(GetUser()->GetToken(), basename.toLatin1().constData(), model.GetUuid()); + QNetworkReply* reply = GetBackendInterface()->GetNetworkAccessManager()->ProcessRequest( request ); + connect(reply, &QNetworkReply::finished, this, [reply, this, p, cloudfolder, xprun]() + { + QNetworkReply* networkReply = qobject_cast( sender() ); + FoldersCreateResponse response(networkReply); + mPendingUploads--; + if (!response.HasError()) + this->Refresh(p, cloudfolder, xprun); + if (mPendingUploads == 0 && !xprun) { + UploadFolder(mLocalUploadRoot, mCloudUploadRoot, true); + } + }); + } + else + assert(false); + } + + // filter for types + QStringList filter; + if (xprun) { + filter << "*.xp.json"; // experience + } + else { + filter << "*.cs.json"; // classifier + filter << "*.sm.json"; // statemachine + } + + // iterate files + QDirIterator filesit(pathlocal, filter, QDir::Files); + while (filesit.hasNext()) + { + QString f = filesit.next(); + + // load file + QFile file(f); + if (!file.open(QFile::ReadOnly | QFile::Text)) { + QMessageBox::warning(this, "Error", "Failed to open file", QMessageBox::Ok); + continue; + } + QTextStream in(&file); + QByteArray arr(file.readAll()); + + // verify json + Json json; + if (!json.Parse(arr.constData())) { + QMessageBox::warning(this, "Error", "Failed to parse JSON", QMessageBox::Ok); + continue; + } + + // get file extension and map to type + QFileInfo finf(f); + QString suff = finf.completeSuffix(); + QString basename = finf.baseName(); + QString type = + suff == "cs.json" ? "CLASSIFIER" : + suff == "sm.json" ? "STATEMACHINE" : + suff == "xp.json" ? "EXPERIENCE" : "folder"; + + // remove file extension with . + f.chop(suff.length()+1); + + // build path on cloud + f.remove(pathlocal); + f = pathcloud + '/' + f; + f.replace("//", "/"); + + // update existing file + if (auto* item = FindItemByPath(f, type)) + { + //qDebug() << "updating: " << f << " (" << type << ")"; + SelectionItem fileModel = CreateSelectionItem(item); + if (!fileModel.GetCreud().Update()) + continue; + + // try to load old uuid, store in map and replace with new + auto jsonuuid = json.GetRootItem().Find("uuid"); + if (jsonuuid.IsString()) + { + mLookup[jsonuuid.GetString()] = fileModel.GetUuid(); + jsonuuid.SetString(fileModel.GetUuid()); + } + + // replace linked uuids for classifier and statemachine if known + if (type == "EXPERIENCE") + { + ReplaceUuid(json, "classifierUuid"); + ReplaceUuid(json, "stateMachineUuid"); + } + + // serialize + Core::String jsonstr; + json.WriteToString(jsonstr, true); + + // start update request + mPendingUploads++; + FilesUpdateRequest request(GetUser()->GetToken(), fileModel.GetUuid(), jsonstr.AsChar()); + QNetworkReply* reply = GetBackendInterface()->GetNetworkAccessManager()->ProcessRequest(request); + connect(reply, &QNetworkReply::finished, this, [reply, this, xprun]() + { + QNetworkReply* networkReply = qobject_cast(sender()); + FilesUpdateResponse response(networkReply); + mPendingUploads--; + if (mPendingUploads == 0) { + if (xprun) this->Refresh(); + else UploadFolder(mLocalUploadRoot, mCloudUploadRoot, true); + } + if (response.HasError()) + { + QMessageBox::warning(this, "Error", "Upload failed", QMessageBox::Ok); + return; + } + }); + } + + // create new file + else if (auto* item = FindItemByPath(pathcloud, "folder")) + { + //qDebug() << "creating: " << f << " (" << type << ")"; + SelectionItem folderModel = CreateSelectionItem(item); + if (!folderModel.GetCreud().Create()) + continue; + + // create empty file first + FilesCreateRequest request( + GetUser()->GetToken(), + basename.toLatin1().constData(), + folderModel.GetUuid(), + type.toLatin1().constData(), + "{}"); + + mPendingUploads++; + QNetworkReply* reply = GetBackendInterface()->GetNetworkAccessManager()->ProcessRequest(request); + connect(reply, &QNetworkReply::finished, this, [reply, this, json, xprun, type]() + { + QNetworkReply* networkReply = qobject_cast(sender()); + FilesCreateResponse response(networkReply); + mPendingUploads--; + + if (response.HasError()) + return; + + // try to load old uuid, store in map and replace with new + auto jsonitm = json.GetRootItem().Find("uuid"); + if (!jsonitm.IsNull()) + { + mLookup[jsonitm.GetString()] = response.mFileId; + jsonitm.SetString(response.mFileId); + } + + Json json2(json); + + // replace linked uuids for classifier and statemachine if known + if (type == "EXPERIENCE") + { + ReplaceUuid(json2, "classifierUuid"); + ReplaceUuid(json2, "stateMachineUuid"); + } + + // serialize + Core::String jsonstr; + json2.WriteToString(jsonstr, true); + + // update json content on backend + mPendingUploads++; + FilesUpdateRequest request(GetUser()->GetToken(), response.mFileId, jsonstr.AsChar()); + QNetworkReply* reply = GetBackendInterface()->GetNetworkAccessManager()->ProcessRequest(request); + connect(reply, &QNetworkReply::finished, this, [reply, this, xprun]() + { + QNetworkReply* networkReply = qobject_cast(sender()); + FilesUpdateResponse response(networkReply); + mPendingUploads--; + if (mPendingUploads == 0) { + if (xprun) this->Refresh(); + else UploadFolder(mLocalUploadRoot, mCloudUploadRoot, true); + } + if (response.HasError()) + { + QMessageBox::warning(this, "Error", "Upload failed", QMessageBox::Ok); + return; + } + }); + }); + } + else + assert(false); + } + //qDebug() << "Pending: " << std::to_string(mPendingUploads).c_str(); +} // called when the minus button got clicked void BackendFileSystemWidget::OnRemoveItem() diff --git a/src/Studio/Plugins/BackendFileSystem/BackendFileSystemWidget.h b/src/Studio/Plugins/BackendFileSystem/BackendFileSystemWidget.h index d4669ba9e..b47c8543f 100644 --- a/src/Studio/Plugins/BackendFileSystem/BackendFileSystemWidget.h +++ b/src/Studio/Plugins/BackendFileSystem/BackendFileSystemWidget.h @@ -85,6 +85,8 @@ class BackendFileSystemWidget : public QWidget inline bool operator==(const SelectionItem& item) const { return ( (mName==item.mName) && (mPath==item.mPath) && (mType==item.mType) && (mUuid == item.mUuid) && (mCreud == item.mCreud) && (mRevision == item.mRevision)); } inline bool IsValid() const { return ( (GetNameString().IsEmpty() == false && GetUuidString().IsEmpty() == false) ); } + inline Core::String GetExtension() const { return GetTypeString() == "CLASSIFIER" ? ".cs.json" : GetTypeString() == "STATEMACHINE" ? ".sm.json" : GetTypeString() == "EXPERIENCE" ? ".xp.json" : ".json"; } + const Creud& GetCreud() const { return mCreud; } bool IsFolder() const { return GetTypeString().IsEqual(FOLDER_TYPE) == true; } @@ -153,7 +155,7 @@ class BackendFileSystemWidget : public QWidget bool ExpandByPath(const QStringList& itemPath); public slots: - void Refresh(); + void Refresh(const QString& localfolder = "", const QString& cloudfolder = "", const bool xprun = false); void OnCreateFile(); void OnCreateFolder(); void OnSearchFieldTextEdited(const QString & text); @@ -165,7 +167,7 @@ class BackendFileSystemWidget : public QWidget void OnCopyFileToPersonalFolder(); void OnContextMenuRetrieveItemRevision(); - void OnLoadFromDiskAndSaveToCloud(); + void OnUploadFromDisk(); void OnCopyJsonToClipboard(); void OnSaveToDisk(); @@ -235,6 +237,10 @@ class BackendFileSystemWidget : public QWidget CollapseState* FindCollapsedState(const char* uuid); bool IsItemCollapsed(const char* uuid); + void ReplaceUuid(Core::Json& json, const char* internalName); + QTreeWidgetItem* FindItemByPath(const QString& path, const QString& type); + void UploadFolder(const QString& pathlocal, const QString& pathcloud, const bool xprun); + Core::Array mSelectedItems; Core::Array mFolderCollapseStates; @@ -254,6 +260,10 @@ class BackendFileSystemWidget : public QWidget ImageButton* mRefreshButton; SearchBoxWidget* mSearchBox; + size_t mPendingUploads; + std::map mLookup; + QString mLocalUploadRoot; + QString mCloudUploadRoot; }; diff --git a/src/Studio/Plugins/Spectrogram2D/Spectrogram2DPlugin.cpp b/src/Studio/Plugins/Spectrogram2D/Spectrogram2DPlugin.cpp index 550dd457c..1bfd54ac9 100644 --- a/src/Studio/Plugins/Spectrogram2D/Spectrogram2DPlugin.cpp +++ b/src/Studio/Plugins/Spectrogram2D/Spectrogram2DPlugin.cpp @@ -60,9 +60,10 @@ void Spectrogram2DPlugin::RegisterAttributes() // register base class attributes Plugin::RegisterAttributes(); + bool multiViewChecked = true; // multi view AttributeSettings* attributeSettings = RegisterAttribute("Multi View", "multiView", "", ATTRIBUTE_INTERFACETYPE_CHECKBOX); - attributeSettings->SetDefaultValue(AttributeBool::Create(true)); + attributeSettings->SetDefaultValue(AttributeBool::Create(multiViewChecked)); // show zero Hz bin attributeSettings = RegisterAttribute("Show 0Hz Bin", "show0HzBin", "", Core::ATTRIBUTE_INTERFACETYPE_CHECKBOX); @@ -74,6 +75,10 @@ void Spectrogram2DPlugin::RegisterAttributes() attributeSettings->SetMinValue( AttributeFloat::Create(0.0f) ); attributeSettings->SetMaxValue( AttributeFloat::Create(10.0f) ); + // show zero Hz bin + attributeSettings = RegisterAttribute("Horizontal View", "horizontalView", "", Core::ATTRIBUTE_INTERFACETYPE_CHECKBOX); + attributeSettings->SetDefaultValue(AttributeBool::Create(true)); + // create default attribute values CreateDefaultAttributeValues(); } @@ -88,9 +93,24 @@ void Spectrogram2DPlugin::OnAttributeChanged(Property* property) if (propertyInternalName.IsEqual("averageInterval") == true) SetAverageInterval( property->AsFloat() ); + if (propertyInternalName.IsEqual("horizontalView") == true) { + bool checked = GetBoolAttributeByName("horizontalView"); + mSpectrumWidget->SetHorizontalView(GetBoolAttributeByName("horizontalView")); + } + // multi view - if (propertyInternalName.IsEqual("multiView") == true) - SetMultiView( GetMultiView() ); + if (propertyInternalName.IsEqual("multiView") == true) { + auto multiView = GetMultiView(); + enableHorizontalViewCheckbox(multiView); + SetMultiView( multiView ); + } +} + +void Spectrogram2DPlugin::enableHorizontalViewCheckbox(bool enable) +{ + if (mHorizontalViewCheckBox) { + mHorizontalViewCheckBox->setEnabled(enable); + } } @@ -124,10 +144,28 @@ bool Spectrogram2DPlugin::Init() // create the attribute set grid widget AttributeSetGridWidget* attributeSetGridWidget = new AttributeSetGridWidget( GetDockWidget() ); attributeSetGridWidget->ReInit(this); - connect( attributeSetGridWidget->GetPropertyManager(), SIGNAL(ValueChanged(Property*)), this, SLOT(OnAttributeChanged(Property*)) ); + + PropertyManager* propertyManager = attributeSetGridWidget->GetPropertyManager(); + + connect( propertyManager, SIGNAL(ValueChanged(Property*)), this, SLOT(OnAttributeChanged(Property*)) ); SetSettingsWidget( attributeSetGridWidget ); + auto properties = propertyManager->GetProperties(); + + for (uint32 i = 0; i < properties.Size(); ++i) { + auto& property = properties[i]; + const auto& name = property->GetAttributeSettings()->GetInternalNameString(); + if (name.IsEqual("horizontalView") == true) { + + mHorizontalViewCheckBox = property->GetAttributeWidget(); + mHorizontalViewCheckBox->setEnabled(GetMultiView()); + break; + } + } + + + /////////////////////////////////////////////////////////////////////////// // Add render widget at the end /////////////////////////////////////////////////////////////////////////// @@ -149,6 +187,8 @@ bool Spectrogram2DPlugin::Init() void Spectrogram2DPlugin::RealtimeUpdate() { + + udpateSelectedChannels(); // must always update spectrum analyzers const uint32 numAnalyzers = mSpectrumAnalyzers.Size(); for (uint32 i = 0; i < numAnalyzers; ++i) @@ -225,6 +265,9 @@ void Spectrogram2DPlugin::ReInit() SetMultiView( GetMultiView() ); + SetHorizontalView(GetHorizontalView()); + + // hide the used checkbox mChannelSelectionWidget->SetShowUsedVisible(false); } @@ -285,3 +328,20 @@ void Spectrogram2DPlugin::SetAverageInterval(double length) output->Reset(); } } + +void Spectrogram2DPlugin::udpateSelectedChannels() +{ + if (auto classifier = GetEngine()->GetActiveClassifier(); classifier) + { + if (auto selector = classifier->FindMainChannelSelector(); selector) + { + auto channels = mChannelSelectionWidget->GetAvailableChannels(); + for (uint32 i = 0; i < channels.Size(); ++i) + { + auto channel = channels[i]; + mChannelSelectionWidget->SetChecked(channel, selector->IsChannelSelected(channel)); + } + } + } + +} \ No newline at end of file diff --git a/src/Studio/Plugins/Spectrogram2D/Spectrogram2DPlugin.h b/src/Studio/Plugins/Spectrogram2D/Spectrogram2DPlugin.h index bc6df1bf6..56e928554 100644 --- a/src/Studio/Plugins/Spectrogram2D/Spectrogram2DPlugin.h +++ b/src/Studio/Plugins/Spectrogram2D/Spectrogram2DPlugin.h @@ -65,10 +65,16 @@ class Spectrogram2DPlugin : public Plugin void SetMultiView(bool multiView) { if (mSpectrumWidget != NULL) mSpectrumWidget->SetMultiView(multiView); } inline double GetAverageInterval() { return GetFloatAttributeByName("averageInterval"); } void SetAverageInterval(double length); + void SetHorizontalView(bool horizontalView) { if (mSpectrumWidget != NULL) mSpectrumWidget->SetHorizontalView(horizontalView); } + inline bool GetHorizontalView() const { return GetBoolAttributeByName("horizontalView"); } private slots: void OnChannelSelectionChanged(); void OnAttributeChanged(Property* property); + + private: + void enableHorizontalViewCheckbox(bool enable); + void udpateSelectedChannels(); private: // selected channels @@ -77,6 +83,7 @@ class Spectrogram2DPlugin : public Plugin Core::Array mAverageSpectra; // holds the current average (the display values) ChannelMultiSelectionWidget* mChannelSelectionWidget; + QWidget* mHorizontalViewCheckBox = nullptr; Spectrogram2DWidget* mSpectrumWidget; Core::Array mSpectrumElements; diff --git a/src/Studio/Plugins/Spectrogram2D/Spectrogram2DWidget.cpp b/src/Studio/Plugins/Spectrogram2D/Spectrogram2DWidget.cpp index 49751542e..237590f4c 100644 --- a/src/Studio/Plugins/Spectrogram2D/Spectrogram2DWidget.cpp +++ b/src/Studio/Plugins/Spectrogram2D/Spectrogram2DWidget.cpp @@ -55,7 +55,6 @@ Spectrogram2DWidget::Spectrogram2DWidget(Spectrogram2DPlugin* plugin, QWidget* p setMinimumHeight(150); } - // destructor Spectrogram2DWidget::~Spectrogram2DWidget() { @@ -66,9 +65,7 @@ Spectrogram2DWidget::~Spectrogram2DWidget() // render frame void Spectrogram2DWidget::paintGL() { - uint32 numSpectrums = 0; - - numSpectrums = mSpectrums.Size(); + uint32 numSpectrums = mSpectrums.Size(); mLeftTextWidth = 50; // TODO HACK /* Classifier* classifier = GetClassifier(); if (classifier != NULL) @@ -114,8 +111,19 @@ void Spectrogram2DWidget::paintGL() } else { - uint32 numAreasInColumn = Math::Sqrt(numSpectrums) + 0.5; - RenderSplitViews( numSpectrums, numAreasInColumn ); + uint32 numAreasOnLine; + if (!mHorizontalView){ + numAreasOnLine = Math::Sqrt(numSpectrums) + 0.5; + } + else + { + numAreasOnLine = 2; + if (numSpectrums % 2 == 1) + { + --numSpectrums; + } + } + RenderSplitViews( numSpectrums, numAreasOnLine, 2.0, mHorizontalView ); } // post rendering @@ -126,6 +134,7 @@ void Spectrogram2DWidget::paintGL() // render callback void Spectrogram2DWidget::RenderCallback::Render(uint32 index, bool isHighlighted, double x, double y, double width, double height) { + // base class render OpenGLWidgetCallback::Render( index, isHighlighted, x, y, width, height ); @@ -227,26 +236,37 @@ void Spectrogram2DWidget::RenderCallback::Render(uint32 index, bool isHighlighte double rangeMax = mCurMaxRange; + + const double minFrequency = spectrumElement->mSpectrum->CalcFrequency(minBinIndex); + const double maxFrequency = spectrumElement->mSpectrum->CalcFrequency(numBins-1); + + mParentWidget->mGridInfo.mNumXSplits = Math::NextPowerOfTwo( mParent->devicePixelRatio() * (width / 100) ); mParentWidget->mGridInfo.mNumXSubSplits = 0; - mParentWidget->mGridInfo.mNumYSplits = Math::NextPowerOfTwo( mParent->devicePixelRatio() * (height / 75) ); - mParentWidget->mGridInfo.mNumYSubSplits = 4; + mParentWidget->mGridInfo.mNumYSplits = mParentWidget->GetMultiView() && mParentWidget->GetHorizontalView() // for horizontal view we want to densly place splits + ? ((maxFrequency - minFrequency) / (mParent->devicePixelRatio() * 5)) : + Math::NextPowerOfTwo( mParent->devicePixelRatio() * (height / 75) ); + mParentWidget->mGridInfo.mNumYSubSplits = 5; OpenGLWidget2DHelpers::RenderGrid( this, &mParentWidget->mGridInfo, FromQtColor(gridColor), FromQtColor(subGridColor), areaStartX, areaEndX, areaStartY, areaEndY ); if (numBins == 0) return; + // in the case of horizontal views, we show 2 charts per row, thus mParentWidget->mSpectrums.Size() / 2 - 1 is the first index of right column charts + bool drawFromRight = (index % 2 == 0); // render spectrum if (mParentWidget->GetMultiView() == true) - OpenGLWidget2DHelpers::RenderSpectrumChart( this, spectrumElement->mSpectrum, spectrumElement->mColor, minBinIndex, numBins -1, rangeMin, rangeMax, areaStartX, areaEndX, areaStartY, areaEndY); + { + OpenGLWidget2DHelpers::RenderSpectrumChart( this, spectrumElement->mSpectrum, spectrumElement->mColor, minFrequency, maxFrequency, rangeMin, rangeMax, areaStartX, areaEndX, areaStartY, areaEndY, mParentWidget->GetHorizontalView(), drawFromRight); + } else { // iterate through all spectra and render each for (uint32 i=0; imSpectrums[i]; - OpenGLWidget2DHelpers::RenderSpectrum(this, currentElement.mSpectrum, currentElement.mColor, minBinIndex, numBins - 1, rangeMin, rangeMax, areaStartX, areaEndX, areaStartY, areaEndY); + OpenGLWidget2DHelpers::RenderSpectrum(this, currentElement.mSpectrum, currentElement.mColor, minFrequency, maxFrequency, rangeMin, rangeMax, areaStartX, areaEndX, areaStartY, areaEndY); } } @@ -255,18 +275,23 @@ void Spectrogram2DWidget::RenderCallback::Render(uint32 index, bool isHighlighte //const int textHeight = GetTextHeight(); const int textRectWidth = areaStartX - 5; - // render Y axis labels - OpenGLWidget2DHelpers::RenderEquallySpacedYLabels( this, mTempString, textColor, rangeMin, rangeMax, mParentWidget->mGridInfo.mNumYSplits, 0.0, 0.0, textRectWidth, areaHeight ); - - // render X axis labels - const double minFrequency = spectrumElement->mSpectrum->CalcFrequency(minBinIndex); - const double maxFrequency = spectrumElement->mSpectrum->CalcFrequency(numBins-1); - OpenGLWidget2DHelpers::RenderEquallySpacedXLabels( this, mTempString, "Hz", textColor, minFrequency, maxFrequency, mParentWidget->mGridInfo.mNumXSplits, areaStartX, height-areaHeight, width-areaStartX, areaHeight ); + auto horizontalView = mParentWidget->GetHorizontalView(); + if (mParentWidget->GetMultiView() == false || mParentWidget->GetHorizontalView() == false) { + // render Y axis labels + OpenGLWidget2DHelpers::RenderEquallySpacedYLabels( this, mTempString, textColor, rangeMin, rangeMax, mParentWidget->mGridInfo.mNumYSplits, 0.0, 0.0, textRectWidth, areaHeight ); + // render X axis labels + OpenGLWidget2DHelpers::RenderEquallySpacedXLabels( this, mTempString, "Hz", textColor, minFrequency, maxFrequency, mParentWidget->mGridInfo.mNumXSplits, areaStartX, height-areaHeight, width-areaStartX, areaHeight ); + } else { + // render Y axis labels + OpenGLWidget2DHelpers::RenderEquallySpacedYLabels( this, mTempString, textColor, minFrequency, maxFrequency, mParentWidget->mGridInfo.mNumYSplits, 0.0, 0.0, textRectWidth, areaHeight, horizontalView, drawFromRight, mParentWidget->mGridInfo.mNumYSubSplits); + // render X axis labels + OpenGLWidget2DHelpers::RenderEquallySpacedXLabels( this, mTempString, "Hz", textColor, rangeMin, rangeMax, mParentWidget->mGridInfo.mNumXSplits, areaStartX, height-areaHeight, width-areaStartX, areaHeight, horizontalView, drawFromRight ); + } // render feedback name - if (spectrumElement != NULL && mParentWidget->mMultiView == true) + if (spectrumElement != NULL && mParentWidget->GetMultiView() == true) { mTempString.Format("%s", spectrumElement->mName.AsChar()); RenderText( mTempString.AsChar(), GetOpenGLWidget()->GetDefaultFontSize(), spectrumElement->mColor, areaStartX+textMargin, 0, OpenGLWidget::ALIGN_TOP| OpenGLWidget::ALIGN_LEFT ); } -} +} \ No newline at end of file diff --git a/src/Studio/Plugins/Spectrogram2D/Spectrogram2DWidget.h b/src/Studio/Plugins/Spectrogram2D/Spectrogram2DWidget.h index 2e23dfb15..ce626ee72 100644 --- a/src/Studio/Plugins/Spectrogram2D/Spectrogram2DWidget.h +++ b/src/Studio/Plugins/Spectrogram2D/Spectrogram2DWidget.h @@ -55,9 +55,12 @@ class Spectrogram2DWidget : public OpenGLWidget void UpdateSpectrum(uint32 index, Spectrum* spectrum) { mSpectrums[index].mSpectrum = spectrum; } void ReInit(const Core::Array& spectrums) { mSpectrums = spectrums; } + uint32 GetNumberOfSpectrums() { return mSpectrums.Size(); } bool GetMultiView() const { return mMultiView; } void SetMultiView(bool multiView) { mMultiView = multiView; } + void SetHorizontalView(bool horizontalView) { mHorizontalView = horizontalView; } + bool GetHorizontalView() const { return mHorizontalView; } private: class RenderCallback : public OpenGLWidgetCallback @@ -81,6 +84,7 @@ class Spectrogram2DWidget : public OpenGLWidget Core::Array mSpectrums; double mLeftTextWidth; bool mMultiView; + bool mHorizontalView = true; QColor mGridColor; QColor mSubGridColor; diff --git a/src/Studio/Plugins/View/ViewPlugin.cpp b/src/Studio/Plugins/View/ViewPlugin.cpp index 7914274ef..25635ee29 100644 --- a/src/Studio/Plugins/View/ViewPlugin.cpp +++ b/src/Studio/Plugins/View/ViewPlugin.cpp @@ -196,3 +196,18 @@ void ViewPlugin::SetViewDuration(double seconds) classifier->GetViewNode(i)->SetViewDuration(seconds); } +double ViewPlugin::GetFixedLength() +{ + Classifier* classifier = GetEngine()->GetActiveClassifier(); + CORE_ASSERT(classifier); + + const uint32 numViewNodes = classifier->GetNumViewNodes(); + for (uint32 i=0; iGetViewNode(i)->GetFixedLength(); + if (fixedLength > 0.) + return fixedLength; + } + return -1.; +} + diff --git a/src/Studio/Plugins/View/ViewPlugin.h b/src/Studio/Plugins/View/ViewPlugin.h index 711063f47..340627518 100644 --- a/src/Studio/Plugins/View/ViewPlugin.h +++ b/src/Studio/Plugins/View/ViewPlugin.h @@ -77,6 +77,7 @@ class ViewPlugin : public Plugin, public Core::EventHandler Core::Color GetChannelColor(uint32 multichannel, uint32 index); void SetViewDuration(double seconds); + double GetFixedLength(); private slots: void OnAttributeChanged(Property* property); diff --git a/src/Studio/Plugins/View/ViewWidget.cpp b/src/Studio/Plugins/View/ViewWidget.cpp index c03d6f746..e7207889e 100644 --- a/src/Studio/Plugins/View/ViewWidget.cpp +++ b/src/Studio/Plugins/View/ViewWidget.cpp @@ -116,6 +116,13 @@ void ViewWidget::RenderCallback::Render(uint32 index, bool isHighlighted, double { //Classifier* classifier = mViewWidget->GetClassifier(); ViewPlugin* plugin = mViewWidget->GetPlugin(); + + double maxTime = plugin->GetFixedLength() * 60; // fixed length is kept in mins + double timeRange = plugin->GetTimeRange(); + if (maxTime < 0.) + maxTime = GetEngine()->GetElapsedTime().InSeconds(); + else + timeRange = maxTime; // get channel and its properties const MultiChannel& channels = plugin->GetMultiChannel(index); @@ -161,7 +168,6 @@ void ViewWidget::RenderCallback::Render(uint32 index, bool isHighlighted, double // automatically calculated, do not change these const double areaStartX = mViewWidget->mLeftTextWidth; const double areaWidth = width - areaStartX; - const double maxTime = GetEngine()->GetElapsedTime().InSeconds(); // assumes all channels have same elapsed time // draw background rect AddRect( 0, 0, width, height, FromQtColor(backgroundColor) ); @@ -171,7 +177,6 @@ void ViewWidget::RenderCallback::Render(uint32 index, bool isHighlighted, double RenderRects(); // calculate the time scale - double timeRange = plugin->GetTimeRange(); bool drawLatencyMarker = plugin->GetShowLatencyMarker(); @@ -259,8 +264,18 @@ void ViewWidget::RenderCallback::RenderTimeline(double x, double y, double width const double areaWidth = width - areaStartX; QColor color = ColorPalette::Shared::GetTextQColor(); - const double timeRange = mViewWidget->GetPlugin()->GetTimeRange(); - const double maxTime = GetEngine()->GetElapsedTime().InSeconds(); + double timeRange = mViewWidget->GetPlugin()->GetTimeRange(); + ViewPlugin* plugin = mViewWidget->GetPlugin(); + + double maxTime = plugin->GetFixedLength(); + bool scaleInMins = false; + if (maxTime < 0.) + maxTime = GetEngine()->GetElapsedTime().InSeconds(); + else + { + scaleInMins = true; + timeRange = maxTime; + } - OpenGLWidget2DHelpers::RenderTimeline( this, FromQtColor(color), timeRange, maxTime, areaStartX, y, areaWidth, height, mTempString ); + OpenGLWidget2DHelpers::RenderTimeline( this, FromQtColor(color), timeRange, maxTime, areaStartX, y, areaWidth, height, mTempString, scaleInMins); } diff --git a/src/Studio/Rendering/OpenGLWidget.cpp b/src/Studio/Rendering/OpenGLWidget.cpp index 7b0b17ff4..607b4cbb1 100644 --- a/src/Studio/Rendering/OpenGLWidget.cpp +++ b/src/Studio/Rendering/OpenGLWidget.cpp @@ -681,7 +681,7 @@ void OpenGLWidget::PostRendering() // render each sensor in an individual view -void OpenGLWidget::RenderSplitViews(uint32 numAreas, uint32 numAreasInColumn, double border) +void OpenGLWidget::RenderSplitViews(uint32 numAreas, uint32 numAreasOnLine, double border, bool horizontalView) { // get the current local cursor pos QPoint localCursorPos = mapFromGlobal( QCursor::pos() ); @@ -692,41 +692,73 @@ void OpenGLWidget::RenderSplitViews(uint32 numAreas, uint32 numAreasInColumn, do // iterate through the areas uint32 numActuallyRendered = 0; + + // the number of sensors to be shown within one column (vertically) before we start a new column and split the screen to fit more channels + bool useSeveralColumns = true; + if (numAreasOnLine == CORE_INVALIDINDEX32) + useSeveralColumns = false; + for (uint32 i=0; iIsChannelSelected(sensor->GetChannel()) == false) // continue; - // the number of sensors to be shown within one column (vertically) before we start a new column and split the screen to fit more channels - bool useSeveralColumns = true; - if (numAreasInColumn == CORE_INVALIDINDEX32) - useSeveralColumns = false; - double xStart, yStart, xEnd, yEnd, cellWidth, cellHeight; - if (useSeveralColumns == true && numAreas > numAreasInColumn) + if (horizontalView) { - // split the viewport vertically and if there are too many items, also split up into columns - const uint32 numColumns = ceil( (double)numAreas / (double)numAreasInColumn ); - const double columnWidth= (double)mWidth / (double)numColumns; - const uint32 columnNr = numActuallyRendered / numAreasInColumn; - - xStart = columnNr * columnWidth; - xEnd = xStart + columnWidth; - cellHeight = (double)viewportHeight / (double)numAreasInColumn; - cellWidth = columnWidth; - yStart = (numActuallyRendered % numAreasInColumn) * cellHeight; - yEnd = yStart + cellHeight; + if (numAreas > numAreasOnLine) + { + // Determine the number of rows required. + const uint32 numRows = uint32(ceil((double)numAreas / (double)numAreasOnLine)); + cellWidth = (double)mWidth / (double)numAreasOnLine; + cellHeight = viewportHeight / (double)numRows; + // Calculate the row and column for the current area. + const uint32 rowNr = numActuallyRendered / numAreasOnLine; + const uint32 colNr = numActuallyRendered % numAreasOnLine; + + xStart = colNr * cellWidth; + xEnd = xStart + cellWidth; + yStart = rowNr * cellHeight; + yEnd = yStart + cellHeight; + } + else + { + // Single row: distribute all areas evenly along the width. + cellWidth = (double)mWidth / (double)numAreas; + cellHeight = viewportHeight; + xStart = numActuallyRendered * cellWidth; + xEnd = xStart + cellWidth; + yStart = 0.0; + yEnd = viewportHeight; + } } else { - // split the viewport vertically (only!) and resize the the items based on the number of sensors to be rendered - xStart = 0.0; - xEnd = mWidth; - cellHeight = (double)viewportHeight / (double)numAreas; - cellWidth = mWidth; - yStart = numActuallyRendered * cellHeight; - yEnd = yStart + cellHeight; + if (useSeveralColumns && numAreas > numAreasOnLine) + { + // Calculate how many columns are needed. + const uint32 numColumns = uint32(ceil((double)numAreas / (double)numAreasOnLine)); + const double columnWidth = (double)mWidth / (double)numColumns; + const uint32 columnNr = numActuallyRendered / numAreasOnLine; + + xStart = columnNr * columnWidth; + xEnd = xStart + columnWidth; + cellHeight = viewportHeight / (double)numAreasOnLine; + cellWidth = columnWidth; + yStart = (numActuallyRendered % numAreasOnLine) * cellHeight; + yEnd = yStart + cellHeight; + } + else + { + // Single column: split the viewport vertically. + xStart = 0.0; + xEnd = mWidth; + cellHeight = viewportHeight / (double)numAreas; + cellWidth = mWidth; + yStart = numActuallyRendered * cellHeight; + yEnd = yStart + cellHeight; + } } xStart = (int32)xStart + 0.5; diff --git a/src/Studio/Rendering/OpenGLWidget.h b/src/Studio/Rendering/OpenGLWidget.h index 69fcddab1..2776b69c9 100644 --- a/src/Studio/Rendering/OpenGLWidget.h +++ b/src/Studio/Rendering/OpenGLWidget.h @@ -53,7 +53,8 @@ class OpenGLWidget : public QOpenGLWidget, protected QOpenGLFunctions bool PreRendering(); void PostRendering(); void Render(double border=2.0) { RenderSplitViews(1, CORE_INVALIDINDEX32, border); } - void RenderSplitViews(uint32 numAreas, uint32 numAreasInColumn=CORE_INVALIDINDEX32, double border=2.0); + // by default numAerasOnLIne is the number of areas vertically + void RenderSplitViews(uint32 numAreas, uint32 numAreasOnLine=CORE_INVALIDINDEX32, double border=2.0, bool horizontalView = false); int GetHeight() const { return mHeight; } diff --git a/src/Studio/Rendering/OpenGLWidget2DHelpers.cpp b/src/Studio/Rendering/OpenGLWidget2DHelpers.cpp index dbf1b23e2..b0a38ff59 100644 --- a/src/Studio/Rendering/OpenGLWidget2DHelpers.cpp +++ b/src/Studio/Rendering/OpenGLWidget2DHelpers.cpp @@ -28,6 +28,8 @@ #include "OpenGLWidget2DHelpers.h" #include +#include + using namespace Core; ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -68,16 +70,16 @@ void OpenGLWidget2DHelpers::RenderHGrid(OpenGLWidgetCallback* callback, uint32 n void OpenGLWidget2DHelpers::RenderGrid(OpenGLWidgetCallback* callback, GridInfo* gridInfo, const Core::Color& mainColor, const Core::Color& subColor, double xStart, double xEnd, double yStart, double yEnd) { const double width = xEnd - xStart; - const double height = yEnd - yStart; - const double splitDeltaY = height / (double)gridInfo->mNumYSplits; - const double subSplitDeltaY = splitDeltaY / (double)gridInfo->mNumYSubSplits; - const double splitDeltaX = width / (double)gridInfo->mNumXSplits; - const double subSplitDeltaX = splitDeltaX / (double)gridInfo->mNumXSubSplits; + const double height = yEnd - yStart; + const double splitDeltaY = height / (double)gridInfo->mNumYSplits; + const double subSplitDeltaY = splitDeltaY / (double)gridInfo->mNumYSubSplits; + const double splitDeltaX = width / (double)gridInfo->mNumXSplits; + const double subSplitDeltaX = splitDeltaX / (double)gridInfo->mNumXSubSplits; // render main horizontal lines for (uint32 i=0; i<=gridInfo->mNumYSplits; ++i) { - double splitY = yStart + height - i*splitDeltaY; + double splitY = yStart + height - i*splitDeltaY; callback->AddLine( xStart, splitY, mainColor, xEnd, splitY, mainColor ); } @@ -86,11 +88,11 @@ void OpenGLWidget2DHelpers::RenderGrid(OpenGLWidgetCallback* callback, GridInfo* { for (uint32 i=0; imNumYSplits; ++i) { - double splitY = yStart + height - i*splitDeltaY; + double splitY = yStart + height - i*splitDeltaY; for (uint32 j=1; jmNumYSubSplits; ++j) { - double subSplitY = splitY - j*subSplitDeltaY; + double subSplitY = splitY - j*subSplitDeltaY; callback->AddLine( xStart, subSplitY, subColor, xEnd, subSplitY, subColor ); } } @@ -100,7 +102,8 @@ void OpenGLWidget2DHelpers::RenderGrid(OpenGLWidgetCallback* callback, GridInfo* // render main vertical lines for (uint32 i=0; i<=gridInfo->mNumXSplits; ++i) { - double splitX = xStart + width - i*splitDeltaX; + double splitX = xStart + width - i*splitDeltaX; + callback->AddLine( splitX, yStart, mainColor, splitX, yEnd, mainColor ); } @@ -109,11 +112,11 @@ void OpenGLWidget2DHelpers::RenderGrid(OpenGLWidgetCallback* callback, GridInfo* { for (uint32 i=0; imNumXSplits; ++i) { - double splitX = xStart + width - i*splitDeltaX; + double splitX = xStart + width - i*splitDeltaX; for (uint32 j=1; jmNumXSubSplits; ++j) { - double subSplitX = splitX - j*subSplitDeltaX; + double subSplitX = splitX - j*subSplitDeltaX; callback->AddLine( subSplitX, yStart, subColor, subSplitX, yEnd, subColor ); } } @@ -123,108 +126,176 @@ void OpenGLWidget2DHelpers::RenderGrid(OpenGLWidgetCallback* callback, GridInfo* } -void OpenGLWidget2DHelpers::RenderEquallySpacedYLabels(OpenGLWidgetCallback* callback, String& tempString, const QColor& textColor, double minValue, double maxValue, uint32 numSplits, double x, double y, double width, double height) +void OpenGLWidget2DHelpers::RenderEquallySpacedYLabels(OpenGLWidgetCallback* callback, String& tempString, const QColor& textColor, double minValue, double maxValue, uint32 numSplits, double x, double y, double width, double height, bool horizontalView, bool drawFromRight, uint32 numSubsplits) { - const double range = Max(minValue, maxValue) - Min(minValue, maxValue); + const double range = Max(minValue, maxValue) - Min(minValue, maxValue); - double splitRange = 0.0; - if (numSplits > 0) - splitRange = range / numSplits; + double splitRange = 0.0; + if (numSplits > 0) + splitRange = range / numSplits; - //QRect textRect; - //const double textHeight = callback->GetTextHeight(); - //const int textMarginX = 2; + //QRect textRect; + //const double textHeight = callback->GetTextHeight(); + //const int textMarginX = 2; - // render min value at the bottom - tempString.Format("%.2f", minValue); - //textRect = QRect(x, y+height-textHeight, width, textHeight); + const char* unit = "Hz"; - callback->RenderText( tempString.AsChar(), callback->GetOpenGLWidget()->GetDefaultFontSize(), textColor, width, height, OpenGLWidget::ALIGN_BASELINE | OpenGLWidget::ALIGN_RIGHT ); + // render min value at the bottom + float fontSize = callback->GetOpenGLWidget()->GetDefaultFontSize(); + if (horizontalView == false) { + tempString.Format("%.2f", minValue); + } else { + fontSize -= 3; + tempString.Format("%.2f %s", minValue, unit); + } + //textRect = QRect(x, y+height-textHeight, width, textHeight); - // render values for the in between splits - for (uint32 i=1; iRenderText( tempString.AsChar(), fontSize, textColor, width, height, OpenGLWidget::ALIGN_BASELINE | OpenGLWidget::ALIGN_RIGHT ); + + String subline = "-"; + if (horizontalView && numSubsplits > 1) { - double y = (numSplits-i)*(height/numSplits); + for (uint32 j = 1; j < numSubsplits; ++j) + { + double y1 = height - ((height / numSplits) / numSubsplits) * j; + callback->RenderText(subline.AsChar(), fontSize + 2, textColor, width, y1, OpenGLWidget::ALIGN_MIDDLE | OpenGLWidget::ALIGN_RIGHT); + } + } + + // render values for the in between splits + for (uint32 i=1; i 5.0) + tempString.Format("%.0f", value); + else if (splitRange > 1.0) + tempString.Format("%.1f", value); + else + tempString.Format("%.2f", value); + + callback->RenderText( tempString.AsChar(), fontSize, textColor, width, y, OpenGLWidget::ALIGN_MIDDLE | OpenGLWidget::ALIGN_RIGHT ); + } else { + if (splitRange > 5.0) + tempString.Format("%.0f %s", value, unit); + else if (splitRange > 1.0) + tempString.Format("%.1f %s", value, unit); + else + tempString.Format("%.2f %s", value, unit); + + callback->RenderText( tempString.AsChar(), fontSize, textColor, width, y, OpenGLWidget::ALIGN_MIDDLE | OpenGLWidget::ALIGN_RIGHT ); + + if (numSubsplits > 1) { + for (uint32 j = 1; j < numSubsplits; ++j) { + double y1 = y - ((height / numSplits) / numSubsplits) * j; + + // const double value = ClampedRemapRange( (double)i/numSplits, 0.0, 1.0, minValue, maxValue ); + callback->RenderText( subline.AsChar(), fontSize + 2, textColor, width, y1, OpenGLWidget::ALIGN_MIDDLE | OpenGLWidget::ALIGN_RIGHT ); + } + } + } - const double value = ClampedRemapRange( (double)i/numSplits, 0.0, 1.0, minValue, maxValue ); + //textRect = QRect(x, y+height-yOffset-(textHeight/2), width, textHeight); - // draw the timestamp label + + + // if () + } + + // render max value on top + if (horizontalView == false) { if (splitRange > 5.0) - tempString.Format("%.0f", value); + tempString.Format("%.0f", maxValue); else if (splitRange > 1.0) - tempString.Format("%.1f", value); + tempString.Format("%.1f", maxValue); else - tempString.Format("%.2f", value); - - //textRect = QRect(x, y+height-yOffset-(textHeight/2), width, textHeight); - - callback->RenderText( tempString.AsChar(), callback->GetOpenGLWidget()->GetDefaultFontSize(), textColor, width, y, OpenGLWidget::ALIGN_MIDDLE | OpenGLWidget::ALIGN_RIGHT ); + tempString.Format("%.2f", maxValue); + } else { + if (splitRange > 5.0) + tempString.Format("%.0f %s", maxValue, unit); + else if (splitRange > 1.0) + tempString.Format("%.1f %s", maxValue, unit); + else + tempString.Format("%.2f %s", maxValue, unit); } - // render max value on top - if (splitRange > 5.0) - tempString.Format("%.0f", maxValue); - else if (splitRange > 1.0) - tempString.Format("%.1f", maxValue); - else - tempString.Format("%.2f", maxValue); + //textRect = QRect(x, y, width, textHeight); - //textRect = QRect(x, y, width, textHeight); - - callback->RenderText( tempString.AsChar(), callback->GetOpenGLWidget()->GetDefaultFontSize(), textColor, width, 0, OpenGLWidget::ALIGN_TOP | OpenGLWidget::ALIGN_RIGHT ); + callback->RenderText( tempString.AsChar(), fontSize, textColor, width, 0, OpenGLWidget::ALIGN_TOP | OpenGLWidget::ALIGN_RIGHT ); } -void OpenGLWidget2DHelpers::RenderEquallySpacedXLabels(OpenGLWidgetCallback* callback, String& tempString, const char* unit, const QColor& textColor, double minValue, double maxValue, uint32 numSplits, double x, double y, double width, double height) +void OpenGLWidget2DHelpers::RenderEquallySpacedXLabels(OpenGLWidgetCallback* callback, String& tempString, const char* unit, const QColor& textColor, double minValue, double maxValue, uint32 numSplits, double x, double y, double width, double height, bool horizontalView, bool drawFromRight) { - const double range = Max(minValue, maxValue) - Min(minValue, maxValue); - - double splitRange = 0.0; - if (numSplits > 0) - splitRange = range / numSplits; + const double range = Max(minValue, maxValue) - Min(minValue, maxValue); - //QRect textRect; - for (uint32 i=0; i<=numSplits; ++i) - { - double xOffset = x + i*(width/numSplits); + double splitRange = 0.0; + if (numSplits > 0) + splitRange = range / numSplits; - const double value = ClampedRemapRange( (double)i/numSplits, 0.0, 1.0, minValue, maxValue ); + //QRect textRect; + for (uint32 i=0; i<=numSplits; ++i) + { + double xOffset = x + i*(width/numSplits); - // draw the timestamp label - if (splitRange > 1.0) - tempString.Format("%.0f %s", value, unit); - else if (splitRange > 0.1) - tempString.Format("%.1f %s", value, unit); - else - tempString.Format("%.2f %s", value, unit); + double value = ClampedRemapRange( (double)i/numSplits, 0.0, 1.0, minValue, maxValue ); - //double textWidth = callback->CalcTextWidth( tempString.AsChar() ); - //const int rectHeight = 9; - //const int rectY = y + 3; - - if (i == 0) + if (horizontalView && drawFromRight) { - //textRect = QRect(x, rectY, textWidth, rectHeight); - callback->RenderText( tempString.AsChar(), callback->GetOpenGLWidget()->GetDefaultFontSize(), textColor, x, y+height, OpenGLWidget::ALIGN_BASELINE | OpenGLWidget::ALIGN_LEFT ); + value = ClampedRemapRange( (double)(numSplits - i)/numSplits, 0.0, 1.0, minValue, maxValue ); } - else if (i == numSplits) - { - //textRect = QRect(x+width-textWidth, rectY, textWidth, rectHeight); - callback->RenderText( tempString.AsChar(), callback->GetOpenGLWidget()->GetDefaultFontSize(), textColor, x+width, y+height, OpenGLWidget::ALIGN_BASELINE | OpenGLWidget::ALIGN_RIGHT ); - } - else - { - //double textX = xOffset-(textWidth*0.5); - //textRect = QRect(textX, rectY, textWidth, rectHeight); - if (xOffset > 0) - { - callback->RenderText( tempString.AsChar(), callback->GetOpenGLWidget()->GetDefaultFontSize(), textColor, xOffset, y+height, OpenGLWidget::ALIGN_BASELINE | OpenGLWidget::ALIGN_CENTER ); - } + // draw the timestamp label + if (horizontalView == false) { + if (splitRange > 1.0) + tempString.Format("%.0f %s", value, unit); + else if (splitRange > 0.1) + tempString.Format("%.1f %s", value, unit); + else + tempString.Format("%.2f %s", value, unit); + } else { + if (splitRange > 1.0) + tempString.Format("%.0f", value); + else if (splitRange > 0.1) + tempString.Format("%.1f", value); + else + tempString.Format("%.2f", value); } - } + + + //double textWidth = callback->CalcTextWidth( tempString.AsChar() ); + //const int rectHeight = 9; + //const int rectY = y + 3; + + if (i == 0) + { + //textRect = QRect(x, rectY, textWidth, rectHeight); + callback->RenderText( tempString.AsChar(), callback->GetOpenGLWidget()->GetDefaultFontSize(), textColor, x, y+height, OpenGLWidget::ALIGN_BASELINE | OpenGLWidget::ALIGN_LEFT ); + } + else if (i == numSplits) + { + //textRect = QRect(x+width-textWidth, rectY, textWidth, rectHeight); + + callback->RenderText( tempString.AsChar(), callback->GetOpenGLWidget()->GetDefaultFontSize(), textColor, x+width, y+height, OpenGLWidget::ALIGN_BASELINE | OpenGLWidget::ALIGN_RIGHT ); + } + else + { + //double textX = xOffset-(textWidth*0.5); + //textRect = QRect(textX, rectY, textWidth, rectHeight); + if (xOffset > 0) + { + callback->RenderText( tempString.AsChar(), callback->GetOpenGLWidget()->GetDefaultFontSize(), textColor, xOffset, y+height, OpenGLWidget::ALIGN_BASELINE | OpenGLWidget::ALIGN_CENTER ); + } + } + } } + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // Signal Rendering ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -232,19 +303,19 @@ void OpenGLWidget2DHelpers::RenderEquallySpacedXLabels(OpenGLWidgetCallback* cal // calculate automatic splitting void OpenGLWidget2DHelpers::AutoCalcChartSplits(double height, uint32* outNumSplits, uint32* outNumSubSplits) { - *outNumSplits = 2; *outNumSubSplits = 0; - - if (height > 200) - { - *outNumSplits = 10; *outNumSubSplits = 5; - return; - } - - if (height > 75) - { - *outNumSplits = 4; *outNumSubSplits = 2; - return; - } + *outNumSplits = 2; *outNumSubSplits = 0; + + if (height > 200) + { + *outNumSplits = 10; *outNumSubSplits = 5; + return; + } + + if (height > 75) + { + *outNumSplits = 4; *outNumSubSplits = 2; + return; + } } @@ -805,7 +876,7 @@ void OpenGLWidget2DHelpers::RenderLineSampleClipped(OpenGLWidgetCallback* callba // Timeline ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -void OpenGLWidget2DHelpers::RenderTimeline(OpenGLWidgetCallback* callback, const Color& color, double timeRange, double maxTime, double x, double y, double width, double height, String& tempString) +void OpenGLWidget2DHelpers::RenderTimeline(OpenGLWidgetCallback* callback, const Color& color, double timeRange, double maxTime, double x, double y, double width, double height, String& tempString, bool scaleInMins) { // parameters uint32 xStart = x; @@ -814,7 +885,7 @@ void OpenGLWidget2DHelpers::RenderTimeline(OpenGLWidgetCallback* callback, const // calc remaining parameters const int32 numPixels = xEnd - xStart; // width of the chart area in pixels - double minTime = maxTime - timeRange; // time of first pixel + double minTime = maxTime - timeRange; // time of first pixel // div by zero check if (IsClose(timeRange, 0.0, Math::epsilon) == true) @@ -850,8 +921,17 @@ void OpenGLWidget2DHelpers::RenderTimeline(OpenGLWidgetCallback* callback, const // draw a time tick callback->AddLine( x+0.375, windowHeight-9, color, x+0.375, windowHeight-14, color ); + if (scaleInMins) + { // draw the timestamp label - tempString.Format( "%.1f", time); + double timeInSecs = time * 60.; + int minutes = static_cast(timeInSecs) / 60; + int seconds = static_cast(timeInSecs) % 60; + + tempString.Format("%02d:%02d", minutes, seconds); + } + else + tempString.Format( "%.1f", time); //double textWidth = callback->CalcTextWidth( tempString.AsChar() ); if (x > 0) @@ -903,7 +983,7 @@ void OpenGLWidget2DHelpers::RenderSpectrum(OpenGLWidgetCallback* callback, Spect // render spectrum -void OpenGLWidget2DHelpers::RenderSpectrumChart(OpenGLWidgetCallback* callback, Spectrum* spectrum, const Color& color, uint32 minBinIndex, uint32 maxBinIndex, double rangeMin, double rangeMax, double xStart, double xEnd, double yStart, double yEnd) +void OpenGLWidget2DHelpers::RenderSpectrumChart(OpenGLWidgetCallback* callback, Spectrum* spectrum, const Color& color, uint32 minBinIndex, uint32 maxBinIndex, double rangeMin, double rangeMax, double xStart, double xEnd, double yStart, double yEnd, bool horizontalView, bool drawFromRight) { if (spectrum == NULL) return; @@ -925,68 +1005,141 @@ void OpenGLWidget2DHelpers::RenderSpectrumChart(OpenGLWidgetCallback* callback, const double xRange = xEnd - xStart; const double yRange = yEnd - yStart; - Color color2 = FromQtColor( ToQColor(color).lighter(110) ); - Color color1 = FromQtColor( ToQColor(color).darker(170) ); - Color adjustedColor2; + if (horizontalView == false) { + Color color2 = FromQtColor( ToQColor(color).lighter(110) ); + Color color1 = FromQtColor( ToQColor(color).darker(170) ); + Color adjustedColor2; - const double binWidth = ((xRange)/(double)numBins)-1.0; + const double binWidth = ((xRange)/(double)numBins)-1.0; - for (uint32 i=1; i<=rangeBins; ++i) - { - const uint32 lastBinIndex = minBinIndex + i - 1; - //const uint32 binIndex = minBinIndex + i; + for (uint32 i=1; i<=rangeBins; ++i) + { + const uint32 lastBinIndex = minBinIndex + i - 1; + + const double lastX = ClampedRemapRange(lastBinIndex, minBinIndex, maxBinIndex, xStart, xEnd); + const double lastY = ClampedRemapRange(spectrum->GetBin(lastBinIndex), rangeMin, rangeMax, yStart, yEnd); + + //const double x = ClampedRemapRange( binIndex, minBinIndex, endBinIndex, xStart, xEnd ); + //const double y = ClampedRemapRange( spectrum->GetBin(binIndex), rangeMin, rangeMax, yStart, yEnd ); - const double lastX = ClampedRemapRange(lastBinIndex, minBinIndex, maxBinIndex, xStart, xEnd); - const double lastY = ClampedRemapRange(spectrum->GetBin(lastBinIndex), rangeMin, rangeMax, yStart, yEnd); + float colorInterpolate = ClampedRemapRange(spectrum->GetBin(lastBinIndex), rangeMin, rangeMax, 0.0, 1.0); + adjustedColor2 = LinearInterpolate( color1, color2, colorInterpolate ); - //const double x = ClampedRemapRange( binIndex, minBinIndex, endBinIndex, xStart, xEnd ); - //const double y = ClampedRemapRange( spectrum->GetBin(binIndex), rangeMin, rangeMax, yStart, yEnd ); + OpenGLWidgetCallback::Rect rect; - float colorInterpolate = ClampedRemapRange(spectrum->GetBin(lastBinIndex), rangeMin, rangeMax, 0.0, 1.0); - adjustedColor2 = LinearInterpolate( color1, color2, colorInterpolate ); + rect.mColor1 = rect.mColor2 = color1; + rect.mColor3 = rect.mColor4 = adjustedColor2; - OpenGLWidgetCallback::Rect rect; + const double binHeight = yRange+lastY; - rect.mColor1 = rect.mColor2 = color1; - rect.mColor3 = rect.mColor4 = adjustedColor2; + if (binWidth > 1) + { + // 1 + rect.mX1 = lastX; + rect.mY1 = yStart; + + // 2 + rect.mX2 = lastX + binWidth; + rect.mY2 = yStart; + + // 3 + rect.mX3 = lastX + binWidth; + rect.mY3 = yStart + binHeight; - const double binHeight = yRange+lastY; + // 4 + rect.mX4 = lastX; + rect.mY4 = yStart + binHeight; - if (binWidth > 1) + callback->AddRect( rect ); + } + else + callback->AddLine( lastX, yStart, color1, lastX, yStart+binHeight, adjustedColor2 ); + } + } else { + const double binWidth = ((yRange)/(double)numBins); + + for (uint32 i = 1; i <= rangeBins; ++i) { - /* - const double xFrom = lastX; - const double yFrom = yStart; - const double xTo = lastX + binWidth; - const double yTo = yStart + binHeight; - - // draw rectangle using a batch of lines - for (double x = xFrom; x < xTo; x += 1.0) - callback->AddLine( x, yFrom, color1, x, yTo, adjustedColor2 ); - */ - - // 1 - rect.mX1 = lastX; - rect.mY1 = yStart; - - // 2 - rect.mX2 = lastX + binWidth; - rect.mY2 = yStart; - - // 3 - rect.mX3 = lastX + binWidth; - rect.mY3 = yStart + binHeight; - - // 4 - rect.mX4 = lastX; - rect.mY4 = yStart + binHeight; - - callback->AddRect( rect ); + + const uint32 lastBinIndex = minBinIndex + i - 1; + + // Assign the color based on the frequency range + Color barColor = barColorByFrequency(lastBinIndex); + + // Map bin index to X position (this will represent the height of the bar) + const double lastX = ClampedRemapRange(spectrum->GetBin(lastBinIndex), rangeMin, rangeMax, xStart, xEnd); + + // Map bin value to Y position (this will represent the vertical placement) + const double lastY = ClampedRemapRange(lastBinIndex, minBinIndex, maxBinIndex, yStart, yEnd); + + OpenGLWidgetCallback::Rect rect; + + rect.mColor1 = rect.mColor4 = barColor; // Bottom-left and Top-left + rect.mColor2 = rect.mColor3 = barColor; // Bottom-right and Top-right + + if (drawFromRight) { + // Bottom-left + rect.mX1 = std::max(xStart, xEnd - lastX); + rect.mY1 = lastY; + + // Bottom-right + rect.mX2 = xEnd; + rect.mY2 = lastY; + + // Top-right + rect.mX3 = xEnd; + rect.mY3 = lastY + binWidth; + + // Top-left + rect.mX4 = std::max(xStart, xEnd - lastX); + rect.mY4 = lastY + binWidth; + } else { + // Bottom-left + rect.mX1 = xStart; + rect.mY1 = lastY; + + // Bottom-right + rect.mX2 = std::min(xStart + lastX, xEnd); + rect.mY2 = lastY; + + // Top-right + rect.mX3 = std::min(xStart + lastX, xEnd); + rect.mY3 = lastY + binWidth; + + // Top-left + rect.mX4 = xStart; + rect.mY4 = lastY + binWidth; + } + + callback->AddRect(rect); } - else - callback->AddLine( lastX, yStart, color1, lastX, yStart+binHeight, adjustedColor2 ); } callback->RenderLines(); callback->RenderRects(); } + +// color selector for bars rendering +Core::Color OpenGLWidget2DHelpers::barColorByFrequency (uint32 binIndex) +{ + // Assign the color based on the frequency range + Color barColor; + if (binIndex >= 0 && binIndex <= 3) + barColor = Color(1.0f, 0.0f, 0.0f); // Red + else if (binIndex >= 3 && binIndex <= 8) + barColor = Color(1.0f, 0.65f, 0.0f); // Orange + else if (binIndex >= 8 && binIndex <= 12) + barColor = Color(1.0f, 1.0f, 0.0f); // Yellow + else if (binIndex >= 12 && binIndex <= 15) + barColor = Color(0.25f, 0.88f, 0.82f); // Turquoise + else if (binIndex >= 15 && binIndex <= 23) + barColor = Color(0.6f, 0.8f, 1.0f); // Blue + else if (binIndex >= 23 && binIndex <= 38) + barColor = Color(0.75f, 0.5f, 1.0f); // Purple + else if (binIndex >= 38 && binIndex <= 60) + barColor = Color(1.0f, 0.75f, 0.8f); // Pink + else + barColor = Color(0.75f, 0.75f, 0.75f); // Default to light gray if out of range + + return barColor; +} \ No newline at end of file diff --git a/src/Studio/Rendering/OpenGLWidget2DHelpers.h b/src/Studio/Rendering/OpenGLWidget2DHelpers.h index 09d1a2eee..1a918778f 100644 --- a/src/Studio/Rendering/OpenGLWidget2DHelpers.h +++ b/src/Studio/Rendering/OpenGLWidget2DHelpers.h @@ -51,10 +51,10 @@ class OpenGLWidget2DHelpers uint32 mNumYSplits; uint32 mNumYSubSplits; }; - static void RenderGrid(OpenGLWidgetCallback* callback, GridInfo* gridInfo, const Core::Color& mainColor, const Core::Color& subColor, double xStart, double xEnd, double yStart, double yEnd); - static void RenderEquallySpacedYLabels(OpenGLWidgetCallback* callback, Core::String& tempString, const QColor& textColor, double minValue, double maxValue, uint32 numSplits, double x, double y, double width, double height); - static void RenderEquallySpacedXLabels(OpenGLWidgetCallback* callback, Core::String& tempString, const char* unit, const QColor& textColor, double minValue, double maxValue, uint32 numSplits, double x, double y, double width, double height); + static void RenderGrid(OpenGLWidgetCallback* callback, GridInfo* gridInfo, const Core::Color& mainColor, const Core::Color& subColor, double xStart, double xEnd, double yStart, double yEnd); + static void RenderEquallySpacedYLabels(OpenGLWidgetCallback* callback, Core::String& tempString, const QColor& textColor, double minValue, double maxValue, uint32 numSplits, double x, double y, double width, double height, bool horizontalView = false, bool drawFromRight = false, uint32 numSubsplits = 0); + static void RenderEquallySpacedXLabels(OpenGLWidgetCallback* callback, Core::String& tempString, const char* unit, const QColor& textColor, double minValue, double maxValue, uint32 numSplits, double x, double y, double width, double height, bool horizontalView = false, bool drawFromRight = false); // // chart rendering // @@ -83,16 +83,17 @@ class OpenGLWidget2DHelpers // // Timeline Rendering // NOTE: used for feedback plugin - static void RenderTimeline(OpenGLWidgetCallback* callback, const Core::Color& color, double timeRange, double maxTime, double x, double y, double width, double height, Core::String& tempString); + static void RenderTimeline(OpenGLWidgetCallback* callback, const Core::Color& color, double timeRange, double maxTime, double x, double y, double width, double height, Core::String& tempString, bool scaleInMins = false); // // Spectrum Rendering // NOTE: used for spectrum plugin static void RenderSpectrum(OpenGLWidgetCallback* callback, Spectrum* spectrum, const Core::Color& color, uint32 minBinIndex, uint32 maxBinIndex, double rangeMin, double rangeMax, double xStart, double xEnd, double yStart, double yEnd); - static void RenderSpectrumChart(OpenGLWidgetCallback* callback, Spectrum* spectrum, const Core::Color& color, uint32 minBinIndex, uint32 maxBinIndex, double rangeMin, double rangeMax, double xStart, double xEnd, double yStart, double yEnd); + static void RenderSpectrumChart(OpenGLWidgetCallback* callback, Spectrum* spectrum, const Core::Color& color, uint32 minBinIndex, uint32 maxBinIndex, double rangeMin, double rangeMax, double xStart, double xEnd, double yStart, double yEnd, bool horizontalView, bool drawFromRight); private: static RenderSampleFunction SelectSampleRenderFunction(EChartRenderStyle style, bool clipped = false); + static Core::Color barColorByFrequency(uint32 binIndex); }; diff --git a/src/Studio/Version.h b/src/Studio/Version.h index 4545d248c..49099f741 100644 --- a/src/Studio/Version.h +++ b/src/Studio/Version.h @@ -26,7 +26,7 @@ #define NEUROMORE_STUDIO_VERSION_MAJOR 1 #define NEUROMORE_STUDIO_VERSION_MINOR 7 -#define NEUROMORE_STUDIO_VERSION_PATCH 3 +#define NEUROMORE_STUDIO_VERSION_PATCH 5 // Macros diff --git a/src/Studio/Widgets/ChannelMultiSelectionWidget.cpp b/src/Studio/Widgets/ChannelMultiSelectionWidget.cpp index 94982533b..c68339edb 100644 --- a/src/Studio/Widgets/ChannelMultiSelectionWidget.cpp +++ b/src/Studio/Widgets/ChannelMultiSelectionWidget.cpp @@ -238,7 +238,7 @@ void ChannelMultiSelectionWidget::ClearUsedChannels() void ChannelMultiSelectionWidget::SetChecked(Channel* channel, bool checked) { - const uint32 channelIndex = mSelectedChannels.Find(channel); + const uint32 channelIndex = mAvailableChannels.Find(channel); if (channelIndex != CORE_INVALIDINDEX32) SetChecked(channelIndex, checked); } @@ -256,4 +256,4 @@ void ChannelMultiSelectionWidget::SetVisible(uint32 index, bool visible) return; mChannelMultiCheckbox->GetCheckbox(index)->setVisible(visible); -} +} \ No newline at end of file