diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index ca9776d5d4..06d11f681f 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -27,7 +27,7 @@ jobs: cd Build cmake -G "Visual Studio 16 2019" -A x64 .. - name: Add msbuild to PATH - uses: microsoft/setup-msbuild@v1.1 + uses: microsoft/setup-msbuild@v2 - name: build run: | msbuild Build/ALL_BUILD.vcxproj -p:Configuration=Release -p:Platform=x64 @@ -41,6 +41,7 @@ jobs: cd Build cp -r Release open-ephys cp -r ../Resources/DLLs/FrontPanelUSB-DriverOnly-4.5.5.exe open-ephys + cp -r ../Resources/DLLs/FTD3XXDriver_WHQLCertified_1.3.0.8_Installer.exe open-ephys cp ../LICENSE open-ephys gui_ver=$(git describe --tags $(git rev-list --tags --max-count=1)) zipfile=open-ephys-latest-windows-dev.zip @@ -61,6 +62,7 @@ jobs: cd Build cp -r Release open-ephys cp -r ../Resources/DLLs/FrontPanelUSB-DriverOnly-4.5.5.exe open-ephys + cp -r ../Resources/DLLs/FTD3XXDriver_WHQLCertified_1.3.0.8_Installer.exe open-ephys cp ../LICENSE open-ephys gui_ver=$(git describe --tags $(git rev-list --tags --max-count=1)) zipfile=open-ephys-${gui_ver}-windows-beta.zip @@ -81,6 +83,7 @@ jobs: cd Build cp -r Release open-ephys cp -r ../Resources/DLLs/FrontPanelUSB-DriverOnly-4.5.5.exe open-ephys + cp -r ../Resources/DLLs/FTD3XXDriver_WHQLCertified_1.3.0.8_Installer.exe open-ephys cp ../LICENSE open-ephys gui_ver=$(git describe --tags $(git rev-list --tags --max-count=1)) zipfile=open-ephys-${gui_ver}-windows.zip diff --git a/CMakeLists.txt b/CMakeLists.txt index f4e37a0b94..0a1fd0a840 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ #Open Ephys GUI main build file cmake_minimum_required(VERSION 3.15) -set(GUI_VERSION 0.6.4) +set(GUI_VERSION 0.6.7) string(REGEX MATCHALL "[0-9]+" VERSION_LIST ${GUI_VERSION}) set(GUI_VERSION_HEX "0x") diff --git a/Plugins/ArduinoOutput/ArduinoOutput.cpp b/Plugins/ArduinoOutput/ArduinoOutput.cpp index 317bdd386c..9299aae29b 100644 --- a/Plugins/ArduinoOutput/ArduinoOutput.cpp +++ b/Plugins/ArduinoOutput/ArduinoOutput.cpp @@ -62,7 +62,9 @@ void ArduinoOutput::setDevice (String devName) Time timer; - arduino.connect (devName.toStdString()); + /* Avoid connecting to the same device twice */ + if (devName != deviceString) + arduino.connect (devName.toStdString()); LOGC("Connected"); @@ -86,13 +88,12 @@ void ArduinoOutput::setDevice (String devName) LOGC("Updating..."); arduino.update(); - std::cout << "firmata v" << arduino.getMajorFirmwareVersion() - << "." << arduino.getMinorFirmwareVersion() << std::endl; + LOGC("firmata v", arduino.getMajorFirmwareVersion(), ".", arduino.getMinorFirmwareVersion()); } if (arduino.isInitialized()) { - std::cout << "Arduino is initialized." << std::endl; + LOGC("Arduino is initialized."); arduino.sendDigitalPinMode ((int) getParameter("output_pin")->getValue(), ARD_OUTPUT); CoreServices::sendStatusMessage (("Arduino initialized at " + devName)); deviceSelected = true; @@ -100,7 +101,7 @@ void ArduinoOutput::setDevice (String devName) } else { - std::cout << "Arduino is NOT initialized." << std::endl; + LOGC("Arduino is NOT initialized."); CoreServices::sendStatusMessage (("Arduino could not be initialized at " + devName)); } } @@ -126,6 +127,17 @@ void ArduinoOutput::process (AudioBuffer& buffer) } +void ArduinoOutput::parameterValueChanged(Parameter* parameter) +{ + if (parameter->getName() == "gate_line") + { + if (int(parameter->getValue()) == 0) + gateIsOpen = true; + else + gateIsOpen = false; + } +} + void ArduinoOutput::handleTTLEvent(TTLEventPtr event) { diff --git a/Plugins/ArduinoOutput/ArduinoOutput.h b/Plugins/ArduinoOutput/ArduinoOutput.h index 72320f4df7..ddf124cbac 100644 --- a/Plugins/ArduinoOutput/ArduinoOutput.h +++ b/Plugins/ArduinoOutput/ArduinoOutput.h @@ -50,6 +50,9 @@ class ArduinoOutput : public GenericProcessor /** Searches for events and triggers the Arduino output when appropriate. */ void process (AudioBuffer& buffer) override; + /** Handle changes to gate line. */ + void parameterValueChanged(Parameter* parameter) override; + /** Convenient interface for responding to incoming events. */ void handleTTLEvent (TTLEventPtr event) override; diff --git a/Plugins/ArduinoOutput/OpenEphysLib.cpp b/Plugins/ArduinoOutput/OpenEphysLib.cpp index 38cda00594..43133ca060 100644 --- a/Plugins/ArduinoOutput/OpenEphysLib.cpp +++ b/Plugins/ArduinoOutput/OpenEphysLib.cpp @@ -38,7 +38,7 @@ extern "C" EXPORT void getLibInfo(Plugin::LibraryInfo* info) { info->apiVersion = PLUGIN_API_VER; info->name = "Arduino Output"; - info->libVersion = "0.1.0"; + info->libVersion = ProjectInfo::versionString; info->numPlugins = NUM_PLUGINS; } diff --git a/Plugins/BasicSpikeDisplay/OpenEphysLib.cpp b/Plugins/BasicSpikeDisplay/OpenEphysLib.cpp index 2d62b32a92..7638d7e463 100644 --- a/Plugins/BasicSpikeDisplay/OpenEphysLib.cpp +++ b/Plugins/BasicSpikeDisplay/OpenEphysLib.cpp @@ -39,7 +39,7 @@ extern "C" EXPORT void getLibInfo(Plugin::LibraryInfo* info) { info->apiVersion = PLUGIN_API_VER; info->name = "Basic Spike Display"; - info->libVersion = "0.1.0"; + info->libVersion = ProjectInfo::versionString; info->numPlugins = NUM_PLUGINS; } diff --git a/Plugins/BasicSpikeDisplay/SpikeDetector/SpikeDetector.cpp b/Plugins/BasicSpikeDisplay/SpikeDetector/SpikeDetector.cpp index 858c2a6f8a..ff01b5236c 100644 --- a/Plugins/BasicSpikeDisplay/SpikeDetector/SpikeDetector.cpp +++ b/Plugins/BasicSpikeDisplay/SpikeDetector/SpikeDetector.cpp @@ -550,12 +550,16 @@ SpikeChannel* SpikeDetector::addSpikeChannel (SpikeChannel::Type type, //Whenever a new spike channel is created, we need to update the unique ID //TODO: This should be automatically done in the SpikeChannel constructor next time we change the API - const Array& sourceChannels = spikeChannel->getSourceChannels(); - std::string cacheKey = std::to_string(sourceChannels[0]->getSourceNodeId()); - cacheKey += "|" + spikeChannel->getStreamName().toStdString(); - cacheKey += "|" + name.toStdString(); + // | | | + std::string stream_source = std::to_string(getDataStream(currentStream)->getSourceNodeId()); + std::string stream_name = getDataStream(currentStream)->getName().toStdString(); + std::string spike_source = std::to_string(spikeChannel->getSourceNodeId()); + std::string channel_name = spikeChannel->getName().toStdString(); + + std::string cacheKey = stream_source + "|" + stream_name + "|" + spike_source + "|" + channel_name; spikeChannel->setIdentifier(cacheKey); + LOGD("Added SpikeChannel w/ identifier: ", cacheKey); return spikeChannel; diff --git a/Plugins/BasicSpikeDisplay/SpikeDisplayNode/SpikeDisplayCanvas.cpp b/Plugins/BasicSpikeDisplay/SpikeDisplayNode/SpikeDisplayCanvas.cpp index e2f45bf101..d2e6a555a2 100644 --- a/Plugins/BasicSpikeDisplay/SpikeDisplayNode/SpikeDisplayCanvas.cpp +++ b/Plugins/BasicSpikeDisplay/SpikeDisplayNode/SpikeDisplayCanvas.cpp @@ -112,6 +112,17 @@ SpikeDisplayCanvas::SpikeDisplayCanvas(SpikeDisplayNode* processor_) : cache = std::make_unique(); } +void SpikeDisplayCanvas::applyCachedDisplaySettings(int plotIdx, std::string key) +{ + spikeDisplay->setMonitorStateForPlot(plotIdx, cache->isMonitored(key)); + + for (int j = 0; j < processor->getNumberOfChannelsForElectrode(plotIdx); j++) + { + spikeDisplay->setThresholdForWaveAxis(plotIdx,j,cache->getThreshold(key,j)); + spikeDisplay->setRangeForWaveAxis(plotIdx,j,cache->getRange(key, j)); + } +} + void SpikeDisplayCanvas::update() { @@ -132,17 +143,27 @@ void SpikeDisplayCanvas::update() std::string cacheKey = processor->getSpikeChannel(i)->getIdentifier().toStdString(); - if (cache && cache->hasCachedDisplaySettings(cacheKey)) + if (cache) { - - spikeDisplay->setMonitorStateForPlot(i, cache->isMonitored(cacheKey)); - - for (int j = 0; j < processor->getNumberOfChannelsForElectrode(i); j++) + //TODO: Should be able to call spikeChannel->getStreamIndex() here... + int streamIdx = 0; + for (auto& stream : processor->getDataStreams()) { - spikeDisplay->setThresholdForWaveAxis(i,j,cache->getThreshold(cacheKey,j)); - spikeDisplay->setRangeForWaveAxis(i,j,cache->getRange(cacheKey, j)); + if (stream->getStreamId() == processor->getSpikeChannel(i)->getStreamId()) + break; + streamIdx++; } + if (cache->hasCachedDisplaySettings(cacheKey)) + { + LOGD("SpikeDisplayCanvas::update: found exact key for ", cacheKey); + applyCachedDisplaySettings(i, cacheKey); + } + else if (cache->findSimilarKey(cacheKey, streamIdx).size() > 0) + { + LOGD("SpikeDisplayCanvas::update: found similar key for ", cacheKey); + applyCachedDisplaySettings(i, cache->findSimilarKey(cacheKey, streamIdx)); + } } } @@ -228,9 +249,17 @@ void SpikeDisplayCanvas::saveCustomParametersToXml(XmlElement* xml) spikePlotIdx++; + const SpikeChannel* spikeChannel = processor->getSpikeChannel(i); + + const uint16 streamId = spikeChannel->getStreamId(); + XmlElement* plotNode = xmlNode->createNewChildElement("PLOT"); - plotNode->setAttribute("name", processor->getSpikeChannel(i)->getIdentifier()); + plotNode->setAttribute("stream_source", processor->getDataStream(streamId)->getSourceNodeId()); + plotNode->setAttribute("stream_name", processor->getDataStream(streamId)->getName()); + plotNode->setAttribute("source_node", spikeChannel->getSourceNodeId()); + plotNode->setAttribute("name", spikeChannel->getName()); + plotNode->setAttribute("isMonitored", spikeDisplay->getMonitorStateForPlot(i)); for (int j = 0; j < spikeDisplay->getNumChannelsForPlot(i); j++) @@ -248,7 +277,6 @@ void SpikeDisplayCanvas::saveCustomParametersToXml(XmlElement* xml) void SpikeDisplayCanvas::loadCustomParametersFromXml(XmlElement* xml) { - for (auto* xmlNode : xml->getChildIterator()) { if (xmlNode->hasTagName("SPIKEDISPLAY")) @@ -273,7 +301,12 @@ void SpikeDisplayCanvas::loadCustomParametersFromXml(XmlElement* xml) { plotIndex++; - std::string cacheKey = processor->getSpikeChannel(plotIndex)->getIdentifier().toStdString(); + std::string stream_source = plotNode->getStringAttribute("stream_source").toStdString(); + std::string stream_name = plotNode->getStringAttribute("stream_name").toStdString(); + std::string source = plotNode->getStringAttribute("source_node").toStdString(); + std::string name = plotNode->getStringAttribute("name").toStdString(); + + std::string cacheKey = stream_source + "|" + stream_name + "|" + source + "|" + name; cache->setMonitor(cacheKey, plotNode->getBoolAttribute("isMonitored")); diff --git a/Plugins/BasicSpikeDisplay/SpikeDisplayNode/SpikeDisplayCanvas.h b/Plugins/BasicSpikeDisplay/SpikeDisplayNode/SpikeDisplayCanvas.h index 20cb27587c..a2d5c8513b 100644 --- a/Plugins/BasicSpikeDisplay/SpikeDisplayNode/SpikeDisplayCanvas.h +++ b/Plugins/BasicSpikeDisplay/SpikeDisplayNode/SpikeDisplayCanvas.h @@ -81,6 +81,44 @@ class SpikeDisplayCache return thresholds.count(cacheKey) > 0; }; + std::string findSimilarKey(std::string key, int streamIndex) + { + std::vector keys = extract_keys(ranges); + + unsigned sourcePos = 0; + unsigned streamPos = key.find_first_of("|"); + unsigned namePos = key.find_last_of("|"); + + // First check for a source ID change (match only stream + electrode name) + for (int i = 0; i < keys.size(); i++) + { + std::string partToMatch = key.substr(streamPos, key.length() - streamPos); + std::string possibleMatch = keys[i].substr(streamPos, keys[i].length() - streamPos); + if (partToMatch.compare(possibleMatch) == 0) + return keys[i]; + } + + // Next check for a stream name change (match only node + electrode name) + std::vector matches; + for (int i = 0; i < keys.size(); i++) + { + int namePos2 = keys[i].find_last_of("|"); + std::string partToMatch = key.substr(sourcePos, streamPos - sourcePos) + key.substr(namePos, key.length() - namePos); + std::string possibleMatch = keys[i].substr(sourcePos, streamPos - sourcePos) + keys[i].substr(namePos2, keys[i].length() - namePos2); + if (partToMatch.compare(possibleMatch) == 0) + matches.push_back(keys[i]); + } + + // Check if multiple matches, if so, default to stream index + if (matches.size() == 1) + return matches[0]; + else if (matches.size() > streamIndex) + return matches[streamIndex]; + + // No match found + return ""; + } + private: std::map> ranges; @@ -178,6 +216,9 @@ class SpikeDisplayCanvas : public Visualizer, /** Loads display parameters */ void loadCustomParametersFromXml(XmlElement* xml); + /** Apply cached settings */ + void applyCachedDisplaySettings(int plotIdx, std::string cacheKey); + /** Pointer to the underlying SpikeDisplayNode*/ SpikeDisplayNode* processor; diff --git a/Plugins/BasicSpikeDisplay/SpikeDisplayNode/SpikePlots.cpp b/Plugins/BasicSpikeDisplay/SpikeDisplayNode/SpikePlots.cpp index c948fee22c..30d0abab9c 100644 --- a/Plugins/BasicSpikeDisplay/SpikeDisplayNode/SpikePlots.cpp +++ b/Plugins/BasicSpikeDisplay/SpikeDisplayNode/SpikePlots.cpp @@ -293,6 +293,7 @@ void SpikePlot::buttonClicked(Button* button) setLimitsOnAxes(); canvas->cache->setMonitor(identifier, monitorButton->getToggleState()); + canvas->cache->setRange(identifier, index, ranges[index]); } @@ -374,6 +375,7 @@ float SpikePlot::getRangeForChannel(int i) void SpikePlot::setRangeForChannel(int i, float range) { //std::cout << "Setting range to " << range << std::endl; + ranges.set(i, range); waveAxes[i]->setRange(range); rangeButtons[i]->setLabel(String(int(range))); } diff --git a/Plugins/ChannelMappingNode/OpenEphysLib.cpp b/Plugins/ChannelMappingNode/OpenEphysLib.cpp index 3149669e47..76b2f9fc2f 100644 --- a/Plugins/ChannelMappingNode/OpenEphysLib.cpp +++ b/Plugins/ChannelMappingNode/OpenEphysLib.cpp @@ -38,7 +38,7 @@ extern "C" EXPORT void getLibInfo(Plugin::LibraryInfo* info) { info->apiVersion = PLUGIN_API_VER; info->name = "Channel Mapper"; - info->libVersion = "0.1.0"; + info->libVersion = ProjectInfo::versionString; info->numPlugins = NUM_PLUGINS; } diff --git a/Plugins/CommonAverageRef/OpenEphysLib.cpp b/Plugins/CommonAverageRef/OpenEphysLib.cpp index 2e1a6eb4f6..877d020bda 100644 --- a/Plugins/CommonAverageRef/OpenEphysLib.cpp +++ b/Plugins/CommonAverageRef/OpenEphysLib.cpp @@ -38,7 +38,7 @@ extern "C" EXPORT void getLibInfo(Plugin::LibraryInfo* info) { info->apiVersion = PLUGIN_API_VER; info->name = "Common Average Reference"; - info->libVersion = "0.1.0"; + info->libVersion = ProjectInfo::versionString; info->numPlugins = NUM_PLUGINS; } diff --git a/Plugins/FilterNode/OpenEphysLib.cpp b/Plugins/FilterNode/OpenEphysLib.cpp index f7ab8ed0f6..5f93620fd1 100644 --- a/Plugins/FilterNode/OpenEphysLib.cpp +++ b/Plugins/FilterNode/OpenEphysLib.cpp @@ -38,7 +38,7 @@ extern "C" EXPORT void getLibInfo(Plugin::LibraryInfo* info) { info->apiVersion = PLUGIN_API_VER; info->name = "Bandpass Filter"; - info->libVersion = "0.1.0"; + info->libVersion = ProjectInfo::versionString; info->numPlugins = NUM_PLUGINS; } diff --git a/Plugins/LfpDisplayNode/DisplayBuffer.cpp b/Plugins/LfpDisplayNode/DisplayBuffer.cpp index 969f0c0154..b8f325af69 100644 --- a/Plugins/LfpDisplayNode/DisplayBuffer.cpp +++ b/Plugins/LfpDisplayNode/DisplayBuffer.cpp @@ -64,7 +64,8 @@ void DisplayBuffer::addChannel( ContinuousChannel::Type type, bool isRecorded, int group, - float ypos, + float ypos, + String description, String structure) { ChannelMetadata metadata = ChannelMetadata(); @@ -75,6 +76,7 @@ void DisplayBuffer::addChannel( metadata.structure = structure; metadata.type = type; metadata.isRecorded = isRecorded; + metadata.description = description; channelMetadata.add(metadata); channelMap[channelNum] = numChannels; diff --git a/Plugins/LfpDisplayNode/DisplayBuffer.h b/Plugins/LfpDisplayNode/DisplayBuffer.h index d84420974f..868633a845 100644 --- a/Plugins/LfpDisplayNode/DisplayBuffer.h +++ b/Plugins/LfpDisplayNode/DisplayBuffer.h @@ -60,7 +60,8 @@ namespace LfpViewer { ContinuousChannel::Type channelType, bool isRecorded, int group = 0, - float ypos = 0, + float ypos = 0, + String description = "", String structure = "None"); /** Initializes the event channel at the start of each buffer */ @@ -87,10 +88,11 @@ namespace LfpViewer { String structure = "None"; ContinuousChannel::Type type; bool isRecorded = false; + String description = ""; }; Array channelMetadata; - + String name; int id; @@ -128,4 +130,4 @@ namespace LfpViewer { }; -#endif //__DISPLAYBUFFER_H__ \ No newline at end of file +#endif //__DISPLAYBUFFER_H__ diff --git a/Plugins/LfpDisplayNode/LfpChannelDisplay.cpp b/Plugins/LfpDisplayNode/LfpChannelDisplay.cpp index 511cce34f2..532231a73d 100644 --- a/Plugins/LfpDisplayNode/LfpChannelDisplay.cpp +++ b/Plugins/LfpDisplayNode/LfpChannelDisplay.cpp @@ -780,6 +780,11 @@ void LfpChannelDisplay::setInputInverted(bool isInverted) } } +bool LfpChannelDisplay::getInputInverted() +{ + return inputInverted; +} + void LfpChannelDisplay::setDrawMethod(bool isDrawMethod) { diff --git a/Plugins/LfpDisplayNode/LfpChannelDisplay.h b/Plugins/LfpDisplayNode/LfpChannelDisplay.h index aeaf3a86dc..b438f99868 100644 --- a/Plugins/LfpDisplayNode/LfpChannelDisplay.h +++ b/Plugins/LfpDisplayNode/LfpChannelDisplay.h @@ -129,6 +129,9 @@ class LfpChannelDisplay : public Component /** Sets whether this channel display should be inverted */ void setInputInverted(bool); + /** Returns whether this channel display is inverted */ + bool getInputInverted(); + /** Sets whether this channel display can be inverted */ void setCanBeInverted(bool); diff --git a/Plugins/LfpDisplayNode/LfpDisplay.cpp b/Plugins/LfpDisplayNode/LfpDisplay.cpp index 94cf722cd4..f65507669b 100644 --- a/Plugins/LfpDisplayNode/LfpDisplay.cpp +++ b/Plugins/LfpDisplayNode/LfpDisplay.cpp @@ -113,9 +113,9 @@ LfpDisplay::LfpDisplay(LfpDisplaySplitter* c, Viewport* v) addMouseListener(this, true); - for (int i = 0; i < 8; i++) + for (int ttlLine = 0; ttlLine < 8; ttlLine++) { - eventDisplayEnabled[i] = true; + eventDisplayEnabled[ttlLine] = true; } savedChannelState.insertMultiple(0, true, 10000); // max 10k channels @@ -444,7 +444,8 @@ void LfpDisplay::refresh() totalPixelsToFill = canvasSplit->screenBufferWidth - fillfrom + fillto; } - //std::cout << fillfrom << " : " << fillto << " ::: " << "totalPixelsToFill: " << totalPixelsToFill << std::endl; + //if (totalPixelsToFill > 0) + // std::cout << fillfrom << " : " << fillto << " ::: " << "totalPixelsToFill: " << totalPixelsToFill << std::endl; int topBorder = viewport->getViewPositionY(); int bottomBorder = viewport->getViewHeight() + topBorder; @@ -493,11 +494,11 @@ void LfpDisplay::refresh() fillfrom_local = lastBitmapIndex; fillto_local = (lastBitmapIndex + totalPixelsToFill) % totalXPixels; - /*if (fillto != 0) - { - std::cout << fillfrom << " : " << fillto << " ::: " << - fillfrom_local << " : " << fillto_local << " :: " << totalPixelsToFill << " ::: " << totalXPixels << std::endl; - }*/ + //if (fillto != 0) + //{ + // std::cout << fillfrom << " : " << fillto << " ::: " << + // fillfrom_local << " : " << fillto_local << " :: " << totalPixelsToFill << " ::: " << totalXPixels << std::endl; + // } for (int i = 0; i < numChans; i++) @@ -591,6 +592,7 @@ void LfpDisplay::refresh() void LfpDisplay::setRange(float r, ContinuousChannel::Type type) { + range[type] = r; if (channels.size() > 0) @@ -601,7 +603,6 @@ void LfpDisplay::setRange(float r, ContinuousChannel::Type type) if (channels[i]->getType() == type) channels[i]->setRange(range[type]); } - canvasSplit->fullredraw = true; //issue full redraw if (displayIsPaused) { @@ -659,6 +660,19 @@ void LfpDisplay::setInputInverted(bool isInverted) } +Array LfpDisplay::getInputInverted() +{ + + Array invertedState; + + for (int i = 0; i < numChans; i++) + { + invertedState.add(channels[i]->getInputInverted()); + } + + return invertedState; +} + void LfpDisplay::setDrawMethod(bool isDrawMethod) { for (int i = 0; i < numChans; i++) @@ -715,6 +729,13 @@ void LfpDisplay::orderChannelsByDepth(bool state) } +bool LfpDisplay::shouldOrderChannelsByDepth() +{ + + return channelsOrderedByDepth; + +} + int LfpDisplay::getChannelDisplaySkipAmount() { return displaySkipAmt; @@ -820,8 +841,6 @@ void LfpDisplay::mouseWheelMove(const MouseEvent& e, const MouseWheelDetails& viewport->setViewPosition(oldX,oldY+scrollBy); // set back to previous position plus offset options->setSpreadSelection(newHeight); // update combobox - - canvasSplit->fullredraw = true;//issue full redraw - scrolling without modifier doesnt require a full redraw } else { @@ -844,8 +863,7 @@ void LfpDisplay::mouseWheelMove(const MouseEvent& e, const MouseWheelDetails& } options->setRangeSelection(h); // update combobox - canvasSplit->fullredraw = true; //issue full redraw - scrolling without modifier doesnt require a full redraw - + } else // just scroll { @@ -853,11 +871,7 @@ void LfpDisplay::mouseWheelMove(const MouseEvent& e, const MouseWheelDetails& if (viewport != nullptr && e.eventComponent == this) // passes only if it's not a listening event { viewport->mouseWheelMove(e.getEventRelativeTo(canvasSplit), wheel); - - //canvasSplit->syncDisplayBuffer(); } - - } } @@ -868,8 +882,6 @@ void LfpDisplay::mouseWheelMove(const MouseEvent& e, const MouseWheelDetails& scrollY = viewport->getViewPositionY(); } - // refresh(); // doesn't seem to be needed now that channels draw to bitmap - } void LfpDisplay::toggleSingleChannel(LfpChannelTrack drawableChannel) @@ -981,27 +993,38 @@ void LfpDisplay::rebuildDrawableChannelsList() removeAllChildren(); // start with clean slate Array channelsToDraw; // all visible channels will be added to this array - + Array filteredChannels; + if(canvasSplit -> displayBuffer) { + filteredChannels = canvasSplit -> getFilteredChannels(); + } // iterate over all channels and select drawable ones - for (int i = 0, drawableChannelNum = 0; i < channels.size(); i++) + for (int i = 0, drawableChannelNum = 0, filterChannelIndex = 0; i < channels.size(); i++) { - //std::cout << "Checking for hidden channels" << std::endl; - if (displaySkipAmt == 0 || (i % displaySkipAmt == 0)) // no skips, add all channels - { - channels[i]->setHidden(false); - channelInfo[i]->setHidden(false); - - channelInfo[i]->setDrawableChannelNumber(drawableChannelNum++); - channelInfo[i]->resized(); // to update the conditional drawing of enableButton and channel num - - channelsToDraw.add(LfpDisplay::LfpChannelTrack{ - channels[i], - channelInfo[i] - }); + int channelNumber = filteredChannels.size() ? canvasSplit->displayBuffer->channelMetadata[i].description.getIntValue(): -1; + //the filter list can have channels that aren't selected for acqusition; this skips those filtered channels + while(filterChannelIndex < filteredChannels.size() && channelNumber > filteredChannels[filterChannelIndex]){ + filterChannelIndex++; + } + if(filteredChannels.size() == 0 || (filterChannelIndex < filteredChannels.size() && channelNumber == filteredChannels[filterChannelIndex])) { + if (displaySkipAmt == 0 || ((filteredChannels.size() ? filterChannelIndex : i) % displaySkipAmt == 0)) // no skips, add all channels + { - addAndMakeVisible(channels[i]); - addAndMakeVisible(channelInfo[i]); + channels[i]->setHidden(false); + channelInfo[i]->setHidden(false); + + channelInfo[i]->setDrawableChannelNumber(drawableChannelNum++); + channelInfo[i]->resized(); // to update the conditional drawing of enableButton and channel num + + channelsToDraw.add(LfpDisplay::LfpChannelTrack{ + channels[i], + channelInfo[i] + }); + + addAndMakeVisible(channels[i]); + addAndMakeVisible(channelInfo[i]); + } + filterChannelIndex++; } else // skip some channels { @@ -1227,15 +1250,15 @@ void LfpDisplay::mouseDown(const MouseEvent& event) } -bool LfpDisplay::setEventDisplayState(int ch, bool state) +bool LfpDisplay::setEventDisplayState(int ttlLine, bool state) { - eventDisplayEnabled[ch] = state; - return eventDisplayEnabled[ch]; + eventDisplayEnabled[ttlLine] = state; + return eventDisplayEnabled[ttlLine]; } -bool LfpDisplay::getEventDisplayState(int ch) +bool LfpDisplay::getEventDisplayState(int ttlLine) { - return eventDisplayEnabled[ch]; + return eventDisplayEnabled[ttlLine]; } void LfpDisplay::setEnabledState(bool state, int chan, bool updateSaved) diff --git a/Plugins/LfpDisplayNode/LfpDisplay.h b/Plugins/LfpDisplayNode/LfpDisplay.h index fadd7e374f..86224bc16b 100644 --- a/Plugins/LfpDisplayNode/LfpDisplay.h +++ b/Plugins/LfpDisplayNode/LfpDisplay.h @@ -44,7 +44,8 @@ namespace LfpViewer { bitmap is drawn by the LfpViewport using Viewport::setViewedComponent. */ -class LfpDisplay : public Component, public Timer +class LfpDisplay : public Component, + public Timer { public: @@ -96,15 +97,25 @@ class LfpDisplay : public Component, public Timer /** Returns the display range for the specified channel type */ int getRange(ContinuousChannel::Type type); + /** Sets the channel height in pixels */ void setChannelHeight(int r, bool resetSingle = true); + + /** Returns the channel height in pixels */ int getChannelHeight(); - - ChannelColourScheme * getColourSchemePtr(); - + /** Caches a new channel height without updating the channels */ void cacheNewChannelHeight(int r); + /** Gets a pointer to the current color scheme */ + ChannelColourScheme* getColourSchemePtr(); + + /** Sets whether the input should be inverted */ void setInputInverted(bool); + + /** Returns whether the input should be inverted across all channels */ + Array getInputInverted(); + + /** Changes between super-sampled and per-pixel plotter */ void setDrawMethod(bool); /** Returns a bool indicating if the channels are displayed in reverse order (true) */ @@ -115,6 +126,9 @@ class LfpDisplay : public Component, public Timer /** Reorders the displayed channels by depth if state == true and normal if false */ void orderChannelsByDepth(bool state); + + /** Returns true if channels are ordered by depth */ + bool shouldOrderChannelsByDepth(); /** Returns a factor of 2 by which the displayed channels should skip */ int getChannelDisplaySkipAmount(); @@ -122,23 +136,40 @@ class LfpDisplay : public Component, public Timer /** Set the amount of channels to skip (hide) between each that is displayed */ void setChannelDisplaySkipAmount(int skipAmt); + /** Updates colors across channels */ void setColors(); + /** Sets the index of the selected color scheme */ void setActiveColourSchemeIdx(int index); + + /** Gets the index of the selected color scheme */ int getActiveColourSchemeIdx(); + /** Returns the number of available color schemes*/ int getNumColourSchemes(); + + /** Returns the names of the available color schemes*/ StringArray getColourSchemeNameArray(); - bool setEventDisplayState(int ch, bool state); - bool getEventDisplayState(int ch); + /** Sets whether events are displayed for a particular ttl line*/ + bool setEventDisplayState(int ttlLine, bool state); + + /** Returns whether events are displayed for a particular ttl line */ + bool getEventDisplayState(int ttlLine); + /** Returns the number of adjacent channels of each color */ int getColorGrouping(); + + /** Sets the number of adjacent channels of each color */ void setColorGrouping(int i); + /** Sets whether a particular channel is enabled */ void setEnabledState(bool state, int chan, bool updateSavedChans = true); - bool getEnabledState(int); + + /** Returns whether a particular channel is enabled */ + bool getEnabledState(int chan); + /** Sets the scroll offset for this display*/ void setScrollPosition(int x, int y); /** Returns true if the median offset is enabled for plotting, else false */ @@ -187,9 +218,8 @@ class LfpDisplay : public Component, public Timer LfpChannelDisplayInfo* channelInfo; }; - Array drawableChannels; // holds the channels and info that are - // drawable to the screen - + /** Holds the channels that are being drawn */ + Array drawableChannels; /** Set the viewport's channel focus behavior. @@ -214,18 +244,25 @@ class LfpDisplay : public Component, public Timer /** Returns a const pointer to the internally managed plotter method class */ LfpBitmapPlotter * const getPlotterPtr() const; + /** Current background color (based on the selected color scheme)*/ Colour backgroundColour; + /** Array of channel colors (based on the selected color scheme*/ Array channelColours; - OwnedArray channels; // all channels - OwnedArray channelInfo; // all channelInfos + /** All available channels (even ones that are not drawn) */ + OwnedArray channels; - void timerCallback() override; - + /** All available display info objects (even ones that are not drawn) */ + OwnedArray channelInfo; + + /** Holds state of event display for first 8 ttl lines */ bool eventDisplayEnabled[8]; - bool displayIsPaused = false; // simple pause function, skips screen buffer updates + /** Enables simple pause function by skipping screen buffer updates */ + bool displayIsPaused = false; + + /** Pointer to display options*/ LfpDisplayOptions* options; /** Convenience struct to store all variables particular to zooming mechanics */ @@ -246,13 +283,19 @@ class LfpDisplay : public Component, public Timer bool unpauseOnScrollEnd; }; - TrackZoomInfo_Struct trackZoomInfo; // and create an instance here + /** Instance of trackZoomInfo struct */ + TrackZoomInfo_Struct trackZoomInfo; + /** Stores whether or not channels are enabled */ Array savedChannelState; + /** x-index of display bitmap updated on previous refresh */ int lastBitmapIndex; private: + + /** Used to throttle refresh speed when scrolling backwards */ + void timerCallback() override; int singleChan; @@ -266,7 +309,10 @@ class LfpDisplay : public Component, public Timer int numChans; int displaySkipAmt; - int cachedDisplayChannelHeight; // holds a channel height if reset during single channel focus + + /** Holds a channel height if reset during single channel focus */ + int cachedDisplayChannelHeight; + float drawableSampleRate; uint32 drawableSubprocessor; @@ -297,5 +343,5 @@ class LfpDisplay : public Component, public Timer OwnedArray colourSchemeList; }; -}; // namespace +}; // end LfpViewer namespace #endif diff --git a/Plugins/LfpDisplayNode/LfpDisplayCanvas.cpp b/Plugins/LfpDisplayNode/LfpDisplayCanvas.cpp index 5b216777ec..18087a32e0 100644 --- a/Plugins/LfpDisplayNode/LfpDisplayCanvas.cpp +++ b/Plugins/LfpDisplayNode/LfpDisplayCanvas.cpp @@ -560,7 +560,7 @@ void LfpDisplayCanvas::mouseUp(const MouseEvent& e) { if (borderToDrag >= 0) { - std::cout << "Mouse up" << std::endl; + //std::cout << "Mouse up" << std::endl; resized(); borderToDrag = -1; @@ -744,10 +744,10 @@ void LfpDisplaySplitter::resized() viewport->setBounds(0, timescaleHeight, getWidth(), getHeight() - 32); } - if (screenBufferMean != nullptr) - { + //if (screenBufferMean != nullptr) + //{ refreshScreenBuffer(); - } + //} if (nChans > 0) { @@ -967,22 +967,22 @@ void LfpDisplaySplitter::updateSettings() } - lfpDisplay->rebuildDrawableChannelsList(); + lfpDisplay->rebuildDrawableChannelsList(); // calls setColors(), which calls refresh isLoading = false; - syncDisplay(); - syncDisplayBuffer(); + syncDisplay(); // sets lastBitmapIndex to 0 + syncDisplayBuffer(); // sets displayBufferIndex to 0 isUpdating = false; - lfpDisplay->setColors(); + //lfpDisplay->setColors(); // calls refresh resized(); lfpDisplay->restoreViewPosition(); - lfpDisplay->refresh(); + //lfpDisplay->refresh(); // calls refresh } @@ -1483,12 +1483,24 @@ void LfpDisplaySplitter::setTimebase(float t) { timebase = t; + /*if (timebase <= 0.1) + { + stopTimer(); + startTimer(1000); + } + else { + stopTimer(); + startTimer(50); + }*/ + if (trialAveraging) { numTrials = -1; } syncDisplay(); + syncDisplayBuffer(); + refreshScreenBuffer(); reachedEnd = true; } @@ -1660,9 +1672,17 @@ void LfpDisplaySplitter::visibleAreaChanged() void LfpDisplaySplitter::refresh() { + updateScreenBuffer(); - - lfpDisplay->refresh(); // redraws only the new part of the screen buffer, unless fullredraw is set to true + + if (shouldRebuildChannelList) + { + shouldRebuildChannelList = false; + lfpDisplay->rebuildDrawableChannelsList(); // calls resized()/refresh() after rebuilding list + } + else { + lfpDisplay->refresh(); // redraws only the new part of the screen buffer, unless fullredraw is set to true + } } void LfpDisplaySplitter::comboBoxChanged(juce::ComboBox *comboBox) diff --git a/Plugins/LfpDisplayNode/LfpDisplayCanvas.h b/Plugins/LfpDisplayNode/LfpDisplayCanvas.h index 9b5d9aae49..884a81edcf 100644 --- a/Plugins/LfpDisplayNode/LfpDisplayCanvas.h +++ b/Plugins/LfpDisplayNode/LfpDisplayCanvas.h @@ -340,6 +340,11 @@ class LfpDisplaySplitter : public Component, uint16 selectedStreamId; void refreshScreenBuffer(); + + bool shouldRebuildChannelList = false; + + void setFilteredChannels(Array channels){filteredChannels = channels;} + Array getFilteredChannels(){return filteredChannels;} private: @@ -381,6 +386,8 @@ class LfpDisplaySplitter : public Component, int displayBufferSize; int scrollBarThickness; + + Array filteredChannels = Array(); JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(LfpDisplaySplitter); diff --git a/Plugins/LfpDisplayNode/LfpDisplayNode.cpp b/Plugins/LfpDisplayNode/LfpDisplayNode.cpp index d4d7cd1c7f..c6b0278473 100644 --- a/Plugins/LfpDisplayNode/LfpDisplayNode.cpp +++ b/Plugins/LfpDisplayNode/LfpDisplayNode.cpp @@ -91,13 +91,14 @@ void LfpDisplayNode::updateSettings() displayBufferMap[streamId]->sampleRate = channel->getSampleRate(); displayBufferMap[streamId]->name = name; } - +// displayBufferMap[streamId]->addChannel(channel->getName(), // name ch, // index channel->getChannelType(), // type channel->isRecorded, 0, // group - channel->position.y // ypos + channel->position.y, // ypos + channel-> getDescription() ); } @@ -179,6 +180,11 @@ bool LfpDisplayNode::stopAcquisition() LfpDisplayEditor* editor = (LfpDisplayEditor*) getEditor(); editor->disable(); + for(auto split : splitDisplays) { + Array emptyArray = Array(); + split -> setFilteredChannels(emptyArray); + } + for (auto buffer : displayBuffers) buffer->ttlState = 0; @@ -327,3 +333,63 @@ void LfpDisplayNode::acknowledgeTrigger(int id) { latestTrigger.set(id, -1); } + +bool LfpDisplayNode::getIntField(DynamicObject::Ptr payload, String name, int& value, int lowerBound, int upperBound) { + if(!payload->hasProperty(name) || !payload->getProperty(name).isInt()) + return false; + int tempVal = payload->getProperty(name); + + if ((upperBound != INT32_MIN && tempVal > upperBound) || (lowerBound != INT32_MAX && tempVal < lowerBound)) + return false; + value = tempVal; + return true; +} + + +void LfpDisplayNode::handleBroadcastMessage(String msg) { + var parsedMessage = JSON::parse(msg); + if(!parsedMessage.isObject()) + return; + DynamicObject::Ptr jsonMessage = parsedMessage.getDynamicObject(); + if(jsonMessage == nullptr) + return; + String pluginName= jsonMessage -> getProperty("plugin"); + if(pluginName != "LFPViewer") { + return; + } + String command = jsonMessage -> getProperty("command"); + DynamicObject::Ptr payload = jsonMessage -> getProperty("payload").getDynamicObject(); + if(command == "filter") { + if(payload.get() == nullptr){ + LOGD("Tried to filter in LFPViewer, but could not find a payload"); + return; + } + int split, start, rows, cols, colsPerRow, end; + if(!getIntField(payload, "split", split, 0, 2) || !getIntField(payload, "start", start, 0)) { + LOGD("Tried to filter in LFPViewer, but a valid split and start weren't provided"); + return; + } + Array channelNames; + //If an end is specificed add channels from start to end + //Else calculate the rectangular selection based on rows and columns + if(getIntField(payload, "end", end, 0)) { + for(int index = 0; index < (end - start); index++) { + channelNames.add(start + index); + } + } + else { + if(!getIntField(payload, "rows", rows, 0) || !getIntField(payload, "cols", cols, 0) || !getIntField(payload, "colsPerRow", colsPerRow, 0)) { + LOGD("Tried to filter by rectangular selection in LFPViewer, but valid row/column/columnsPerRow counts weren't provided"); + return; + } + for(int row = 0; row < rows; row++) { + for(int col = 0; col < cols; col++) { + channelNames.add(start + col + row*colsPerRow); + } + } + } + splitDisplays[split] -> setFilteredChannels(channelNames); + splitDisplays[split] -> shouldRebuildChannelList = true; + } +} + diff --git a/Plugins/LfpDisplayNode/LfpDisplayNode.h b/Plugins/LfpDisplayNode/LfpDisplayNode.h index f7c738cfca..5759deedfe 100644 --- a/Plugins/LfpDisplayNode/LfpDisplayNode.h +++ b/Plugins/LfpDisplayNode/LfpDisplayNode.h @@ -100,6 +100,12 @@ class LfpDisplayNode : public GenericProcessor /** Acknowledges receipt of a trigger for a given split display*/ void acknowledgeTrigger(int splitId); + /** Reads from int value from payload, returns if the value was found and is within bounds*/ + bool getIntField(DynamicObject::Ptr payload, String name, int& value, int lowerBound = INT32_MAX, int upperBound = INT32_MIN); + + /** Handles messages from other processors during acquisition*/ + void handleBroadcastMessage(String msg) override; + private: /** Initializes trigger channels within a process block*/ diff --git a/Plugins/LfpDisplayNode/LfpDisplayOptions.cpp b/Plugins/LfpDisplayNode/LfpDisplayOptions.cpp index ebf5e2930b..3a370d17b3 100644 --- a/Plugins/LfpDisplayNode/LfpDisplayOptions.cpp +++ b/Plugins/LfpDisplayNode/LfpDisplayOptions.cpp @@ -61,8 +61,6 @@ LfpDisplayOptions::LfpDisplayOptions(LfpDisplayCanvas* canvas_, LfpDisplaySplitt // MAIN OPTIONS // Timebase - timebases.add("0.010"); - timebases.add("0.025"); timebases.add("0.050"); timebases.add("0.100"); timebases.add("0.250"); @@ -74,7 +72,7 @@ LfpDisplayOptions::LfpDisplayOptions(LfpDisplayCanvas* canvas_, LfpDisplaySplitt timebases.add("5.0"); timebases.add("10.0"); timebases.add("20.0"); - selectedTimebase = 8; + selectedTimebase = 6; selectedTimebaseValue = timebases[selectedTimebase - 1]; timebaseSelection = std::make_unique("Timebase"); @@ -121,7 +119,7 @@ LfpDisplayOptions::LfpDisplayOptions(LfpDisplayCanvas* canvas_, LfpDisplaySplitt voltageRanges[ContinuousChannel::Type::ELECTRODE].add("15000"); selectedVoltageRange[ContinuousChannel::Type::ELECTRODE] = 4; rangeGain[ContinuousChannel::Type::ELECTRODE] = 1; //uV - rangeSteps[ContinuousChannel::Type::ELECTRODE] = 10; + rangeSteps[ContinuousChannel::Type::ELECTRODE] = 20; rangeUnits.add(CharPointer_UTF8("\xC2\xB5V")); typeNames.add("DATA"); @@ -775,6 +773,10 @@ void LfpDisplayOptions::togglePauseButton(bool sendUpdate) void LfpDisplayOptions::setChannelsReversed(bool state) { + + if (lfpDisplay->getChannelsReversed() == state) // ignore if we're not changing state + return; + lfpDisplay->setChannelsReversed(state); canvasSplit->fullredraw = true; @@ -791,6 +793,7 @@ void LfpDisplayOptions::setChannelsReversed(bool state) void LfpDisplayOptions::setInputInverted(bool state) { + lfpDisplay->setInputInverted(state); invertInputButton->setToggleState(state, dontSendNotification); @@ -849,6 +852,9 @@ void LfpDisplayOptions::setAveraging(bool state) void LfpDisplayOptions::setSortByDepth(bool state) { + if (lfpDisplay->shouldOrderChannelsByDepth() == state) + return; + if (canvasSplit->displayBuffer != nullptr) lfpDisplay->orderChannelsByDepth(state); @@ -1504,7 +1510,10 @@ void LfpDisplayOptions::loadParameters(XmlElement* xml) //LOGD(" --> setShowChannelNumbers: ", MS_FROM_START, " milliseconds"); start = Time::getHighResolutionTicks(); - setInputInverted(xmlNode->getBoolAttribute("isInverted", false)); + bool shouldInvert = xmlNode->getBoolAttribute("isInverted", false); + + if (invertInputButton->getToggleState() != shouldInvert) + setInputInverted(shouldInvert); //LOGD(" --> setInputInverted: ", MS_FROM_START, " milliseconds"); start = Time::getHighResolutionTicks(); diff --git a/Plugins/LfpDisplayNode/LfpTimescale.cpp b/Plugins/LfpDisplayNode/LfpTimescale.cpp index 4ee235a549..37d343a124 100644 --- a/Plugins/LfpDisplayNode/LfpTimescale.cpp +++ b/Plugins/LfpDisplayNode/LfpTimescale.cpp @@ -126,7 +126,7 @@ void LfpTimescale::setPausedState(bool isPaused_) else { lfpDisplay->pause(true); isPaused = true; - startTimer(200); + startTimer(100); } repaint(); diff --git a/Plugins/LfpDisplayNode/OpenEphysLib.cpp b/Plugins/LfpDisplayNode/OpenEphysLib.cpp index e7943aaf0d..8729d78a8f 100644 --- a/Plugins/LfpDisplayNode/OpenEphysLib.cpp +++ b/Plugins/LfpDisplayNode/OpenEphysLib.cpp @@ -40,7 +40,7 @@ extern "C" EXPORT void getLibInfo(Plugin::LibraryInfo* info) { info->apiVersion = PLUGIN_API_VER; info->name = "LFP viewer"; - info->libVersion = "0.1.0"; + info->libVersion = ProjectInfo::versionString; info->numPlugins = NUM_PLUGINS; } diff --git a/Plugins/PhaseDetector/OpenEphysLib.cpp b/Plugins/PhaseDetector/OpenEphysLib.cpp index 8df98951e6..99dd278b82 100644 --- a/Plugins/PhaseDetector/OpenEphysLib.cpp +++ b/Plugins/PhaseDetector/OpenEphysLib.cpp @@ -38,7 +38,7 @@ extern "C" EXPORT void getLibInfo(Plugin::LibraryInfo* info) { info->apiVersion = PLUGIN_API_VER; info->name = "Phase Detector"; - info->libVersion = "0.1.0"; + info->libVersion = ProjectInfo::versionString; info->numPlugins = NUM_PLUGINS; } diff --git a/Plugins/PhaseDetector/PhaseDetector.cpp b/Plugins/PhaseDetector/PhaseDetector.cpp index 0d74151a45..9e111ce300 100644 --- a/Plugins/PhaseDetector/PhaseDetector.cpp +++ b/Plugins/PhaseDetector/PhaseDetector.cpp @@ -212,7 +212,7 @@ void PhaseDetector::process (AudioBuffer& buffer) addEvent(ptr, i); - //LOGD("PEAK"); + //LOGD("[phase detector] PEAK found!"); } module->currentPhase = FALLING_POS; diff --git a/Plugins/RecordControl/OpenEphysLib.cpp b/Plugins/RecordControl/OpenEphysLib.cpp index 2e27d06081..59f6a76258 100644 --- a/Plugins/RecordControl/OpenEphysLib.cpp +++ b/Plugins/RecordControl/OpenEphysLib.cpp @@ -38,7 +38,7 @@ extern "C" EXPORT void getLibInfo(Plugin::LibraryInfo* info) { info->apiVersion = PLUGIN_API_VER; info->name = "Record Control"; - info->libVersion = "0.1.0"; + info->libVersion = ProjectInfo::versionString; info->numPlugins = NUM_PLUGINS; } diff --git a/README.md b/README.md index 0c2e8be6a8..6c68029107 100644 --- a/README.md +++ b/README.md @@ -33,11 +33,11 @@ Our primary user base is scientists performing electrophysiology experiments wit The easiest way to get started is to download the installer for your platform of choice: -- [Windows](https://openephysgui.jfrog.io/artifactory/Release-Installer/windows/Install-Open-Ephys-GUI-v0.6.4.exe) -- [Ubuntu/Debian](https://openephysgui.jfrog.io/artifactory/Release-Installer/linux/open-ephys-gui-v0.6.4.deb) -- [macOS](https://openephysgui.jfrog.io/artifactory/Release-Installer/mac/Open_Ephys_GUI_v0.6.4.dmg) +- [Windows](https://openephysgui.jfrog.io/artifactory/Release-Installer/windows/Install-Open-Ephys-GUI-v0.6.7.exe) +- [Ubuntu/Debian](https://openephysgui.jfrog.io/artifactory/Release-Installer/linux/open-ephys-gui-v0.6.7.deb) +- [macOS](https://openephysgui.jfrog.io/artifactory/Release-Installer/mac/Open_Ephys_GUI_v0.6.7.dmg) -It’s also possible to obtain the binaries as a .zip file for [Windows](https://openephysgui.jfrog.io/artifactory/Release/windows/open-ephys-v0.6.4-windows.zip), [Linux](https://openephysgui.jfrog.io/artifactory/Release/linux/open-ephys-v0.6.4-linux.zip), or [Mac](https://openephysgui.jfrog.io/artifactory/Release/mac/open-ephys-v0.6.4-mac.zip). +It’s also possible to obtain the binaries as a .zip file for [Windows](https://openephysgui.jfrog.io/artifactory/Release/windows/open-ephys-v0.6.7-windows.zip), [Linux](https://openephysgui.jfrog.io/artifactory/Release/linux/open-ephys-v0.6.7-linux.zip), or [Mac](https://openephysgui.jfrog.io/artifactory/Release/mac/open-ephys-v0.6.7-mac.zip). Detailed installation instructions can be found [here](https://open-ephys.github.io/gui-docs/User-Manual/Installing-the-GUI.html). diff --git a/Resources/Configs/acq_board_config.xml b/Resources/Configs/acq_board_config.xml index f5e83a84f8..962fee6cc0 100644 --- a/Resources/Configs/acq_board_config.xml +++ b/Resources/Configs/acq_board_config.xml @@ -2,7 +2,7 @@ - 0.6.4 + 0.6.7 8 unknown Windows, Linux, or macOS diff --git a/Resources/Configs/file_reader_config.xml b/Resources/Configs/file_reader_config.xml index dc466d4c4b..29ff65acf7 100644 --- a/Resources/Configs/file_reader_config.xml +++ b/Resources/Configs/file_reader_config.xml @@ -2,7 +2,7 @@ - 0.6.4 + 0.6.7 8 unknown Windows, Linux, or macOS diff --git a/Resources/Configs/neuropixels_pxi_config.xml b/Resources/Configs/neuropixels_pxi_config.xml index a69e6da96c..3a035ed777 100644 --- a/Resources/Configs/neuropixels_pxi_config.xml +++ b/Resources/Configs/neuropixels_pxi_config.xml @@ -2,7 +2,7 @@ - 0.6.4 + 0.6.7 8 unknown Windows diff --git a/Resources/Configs/oe_acq_board_config.xml b/Resources/Configs/oe_acq_board_config.xml new file mode 100644 index 0000000000..c7fe72a0ee --- /dev/null +++ b/Resources/Configs/oe_acq_board_config.xml @@ -0,0 +1,252 @@ + + + + + 0.6.7 + 8 + unknown + Windows, Linux, or macOS + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + diff --git a/Resources/DLLs/FTD3XXDriver_WHQLCertified_1.3.0.8_Installer.exe b/Resources/DLLs/FTD3XXDriver_WHQLCertified_1.3.0.8_Installer.exe new file mode 100644 index 0000000000..8cb2be8454 Binary files /dev/null and b/Resources/DLLs/FTD3XXDriver_WHQLCertified_1.3.0.8_Installer.exe differ diff --git a/Resources/Installers/Linux/Open-Ephys_Installer/DEBIAN/control b/Resources/Installers/Linux/Open-Ephys_Installer/DEBIAN/control index 436e1170e8..52d1708f5f 100644 --- a/Resources/Installers/Linux/Open-Ephys_Installer/DEBIAN/control +++ b/Resources/Installers/Linux/Open-Ephys_Installer/DEBIAN/control @@ -1,5 +1,5 @@ Package: open-ephys -Version: 0.6.4 +Version: 0.6.7 Architecture: amd64 Installed-Size: 18644 Section: science diff --git a/Resources/Installers/Linux/Open-Ephys_Installer/DEBIAN/copyright b/Resources/Installers/Linux/Open-Ephys_Installer/DEBIAN/copyright index ec2328a4f0..a7a333eff2 100644 --- a/Resources/Installers/Linux/Open-Ephys_Installer/DEBIAN/copyright +++ b/Resources/Installers/Linux/Open-Ephys_Installer/DEBIAN/copyright @@ -3,7 +3,7 @@ Upstream-Name: Open Ephys Plugin GUI Source: https://github.com/open-ephys/plugin-GUI/ Files: * -Copyright: 2022 Open Ephys +Copyright: 2023 Open Ephys License: GPL-3+ This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/Resources/Installers/Windows/vcredist_x64.exe b/Resources/Installers/Windows/vcredist_x64.exe deleted file mode 100644 index 7476d75aaa..0000000000 Binary files a/Resources/Installers/Windows/vcredist_x64.exe and /dev/null differ diff --git a/Resources/Installers/Windows/windows_installer_script.iss b/Resources/Installers/Windows/windows_installer_script.iss index e68ada0b74..40eba3cff8 100644 --- a/Resources/Installers/Windows/windows_installer_script.iss +++ b/Resources/Installers/Windows/windows_installer_script.iss @@ -1,8 +1,8 @@ [Setup] AppName=Open Ephys -AppVersion=0.6.4 -AppVerName=Open Ephys 0.6.4 -AppCopyright=Copyright (C) 2010-2022, Open Ephys & Contributors +AppVersion=0.6.7 +AppVerName=Open Ephys 0.6.7 +AppCopyright=Copyright (C) 2010-2024, Open Ephys & Contributors AppPublisher=open-ephys.org AppPublisherURL=https://open-ephys.org/gui DefaultDirName={autopf}\Open Ephys @@ -20,11 +20,16 @@ WizardStyle=modern [Tasks] Name: desktopicon; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}" -Name: install_usb; Description: "Install Opal Kelly Front Panel USB driver for Open Ephys Acquisition Board"; GroupDescription: "External drivers:"; +Name: install_usb1; Description: "Install FTDI D3XX driver (Open Ephys FPGA board)"; GroupDescription: "Acquisition Board drivers:"; +Name: install_usb2; Description: "Install Opal Kelly Front Panel USB driver (Opal Kelly FPGA board)"; GroupDescription: "Acquisition Board drivers:"; Flags: unchecked; + +[Dirs] +Name: "{commonappdata}\Open Ephys"; Permissions: users-modify; Flags: uninsneveruninstall; [Files] Source: "..\..\..\Build\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs; BeforeInstall: UpdateProgress(0); -Source: "..\..\..\Build\Release\shared\*"; DestDir: "{commonappdata}\Open Ephys\shared-api8"; Flags: ignoreversion recursesubdirs; BeforeInstall: UpdateProgress(55); +Source: "..\..\..\Build\Release\shared\*"; DestDir: "{commonappdata}\Open Ephys\shared-api8"; Flags: ignoreversion recursesubdirs uninsneveruninstall; BeforeInstall: UpdateProgress(55); +Source: "..\..\DLLs\FTD3XXDriver_WHQLCertified_1.3.0.8_Installer.exe"; DestDir: {tmp}; Flags: deleteafterinstall; BeforeInstall: UpdateProgress(80); Source: "..\..\DLLs\FrontPanelUSB-DriverOnly-4.5.5.exe"; DestDir: {tmp}; Flags: deleteafterinstall; BeforeInstall: UpdateProgress(90); [Icons] @@ -33,7 +38,9 @@ Name: "{autodesktop}\Open Ephys"; Filename: "{app}\open-ephys.exe"; Tasks: deskt Name: "{autoprograms}\Open Ephys"; Filename: "{app}\open-ephys.exe" [Run] -Filename: "{tmp}\FrontPanelUSB-DriverOnly-4.5.5.exe"; StatusMsg: "Installing Front Panel USB driver..."; Tasks: install_usb; Flags: skipifsilent +Filename: "{tmp}\FTD3XXDriver_WHQLCertified_1.3.0.8_Installer.exe"; StatusMsg: "Installing FTDI D3XX driver..."; Tasks: install_usb1; Flags: skipifsilent +Filename: "{tmp}\FrontPanelUSB-DriverOnly-4.5.5.exe"; StatusMsg: "Installing Front Panel USB driver..."; Tasks: install_usb2; Flags: skipifsilent +Filename: "{app}\open-ephys.exe"; Description: "Launch Open Ephys GUI"; Flags: postinstall nowait skipifsilent [Code] // types and variables diff --git a/Source/AutoUpdater.cpp b/Source/AutoUpdater.cpp new file mode 100644 index 0000000000..68ff47bbc7 --- /dev/null +++ b/Source/AutoUpdater.cpp @@ -0,0 +1,527 @@ +/* + ------------------------------------------------------------------ + + This file is part of the Open Ephys GUI + Copyright (C) 2023 Open Ephys + + ------------------------------------------------------------------ + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + Reference : https://github.com/juce-framework/JUCE/blob/6.0.8/extras/Projucer/Source/Application/jucer_AutoUpdater.cpp + +*/ + +#include "AutoUpdater.h" +#include "CoreServices.h" +#include "MainWindow.h" +#ifdef _WIN32 +#include +#include +#endif + +//============================================================================== +LatestVersionCheckerAndUpdater::LatestVersionCheckerAndUpdater() + : Thread ("VersionChecker") + , mainWindow(nullptr) +{ +} + +LatestVersionCheckerAndUpdater::~LatestVersionCheckerAndUpdater() +{ + stopThread (6000); + clearSingletonInstance(); +} + +void LatestVersionCheckerAndUpdater::checkForNewVersion (bool background, MainWindow* mw) +{ + if (! isThreadRunning()) + { + backgroundCheck = background; + mainWindow = mw; + startThread (3); + } +} + +//============================================================================== +void LatestVersionCheckerAndUpdater::run() +{ + LOGC("Checking for a newer version of the GUI..."); + URL latestVersionURL ("https://api.github.com/repos/open-ephys/plugin-GUI/releases/latest"); + + std::unique_ptr inStream (latestVersionURL.createInputStream (URL::InputStreamOptions (URL::ParameterHandling::inAddress) + .withConnectionTimeoutMs (5000))); + const String commErr = "Failed to communicate with the Open Ephys update server.\n" + "Please try again in a few minutes.\n\n" + "If this problem persists you can download the latest version of Open Ephys GUI from open-ephys.org/gui"; + + if (inStream == nullptr) + { + if (! backgroundCheck) + AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, + "Update Server Communication Error", + commErr); + + return; + } + + auto content = inStream->readEntireStreamAsString(); + auto latestReleaseDetails = JSON::parse (content); + + auto* json = latestReleaseDetails.getDynamicObject(); + + if (json == nullptr) + { + if (! backgroundCheck) + AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, + "Update Server Communication Error", + commErr); + + return; + } + + auto versionString = json->getProperty ("tag_name").toString(); + + if (versionString.isEmpty()) + return; + + auto* assets = json->getProperty ("assets").getArray(); + + if (assets == nullptr) + return; + + auto releaseNotes = json->getProperty ("body").toString(); + + std::vector parsedAssets; + + for (auto& asset : *assets) + { + if (auto* assetJson = asset.getDynamicObject()) + { + parsedAssets.push_back ({ assetJson->getProperty ("name").toString(), + assetJson->getProperty ("url").toString(), + (int)assetJson->getProperty("size")}); + jassert (parsedAssets.back().name.isNotEmpty()); + jassert (parsedAssets.back().url.isNotEmpty()); + jassert (parsedAssets.back().size != 0); + + } + else + { + jassertfalse; + } + } + + String latestVer = versionString.substring(1); + + + if (latestVer.compareNatural(CoreServices::getGUIVersion()) <= 0) + { + if (! backgroundCheck) + AlertWindow::showMessageBoxAsync (AlertWindow::InfoIcon, + "No Newer Version Available", + "You are running the latest available version of the Open Ephys GUI."); + return; + } + + auto osString = [] + { + #if JUCE_MAC + return "mac"; + #elif JUCE_WINDOWS + return "windows"; + #elif JUCE_LINUX + return "linux"; + #else + jassertfalse; + return "Unknown"; + #endif + }(); + + String requiredFilename ("open-ephys-" + versionString + "-" + osString + ".zip"); + +#if JUCE_WINDOWS + File exeDir = File::getSpecialLocation(File::SpecialLocationType::currentExecutableFile).getParentDirectory(); + if(exeDir.findChildFiles(File::findFiles, false, "unins*").size() > 0) + { + requiredFilename = "Install-Open-Ephys-GUI-" + versionString + ".exe"; + } +#elif JUCE_LINUX + File exeDir = File::getSpecialLocation(File::SpecialLocationType::currentExecutableFile).getParentDirectory(); + if(exeDir.getFullPathName().contains("/usr/local/bin")) + { + requiredFilename = "open-ephys-gui-" + versionString + ".deb"; + } +#elif JUCE_MAC + File exeDir = File::getSpecialLocation(File::SpecialLocationType::currentApplicationFile).getParentDirectory(); + File globalAppDir = File::getSpecialLocation(File::SpecialLocationType::globalApplicationsDirectory); + if(exeDir.getFullPathName().contains(globalAppDir.getFullPathName())) + { + requiredFilename = "Open_Ephys_GUI_" + versionString + ".dmg"; + } +#endif + + for (auto& asset : parsedAssets) + { + if (asset.name == requiredFilename) + { + + MessageManager::callAsync ([this, versionString, releaseNotes, asset] + { + askUserAboutNewVersion (versionString, releaseNotes, asset); + }); + + return; + } + } + + if (! backgroundCheck) + AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, + "Failed to find any new downloads", + "Please try again in a few minutes."); +} + +//============================================================================== +class UpdateDialog : public Component +{ +public: + UpdateDialog (const String& newVersion, const String& releaseNotes, bool automaticVerCheck) + { + titleLabel.setText ("Open Ephys GUI version " + newVersion, dontSendNotification); + titleLabel.setFont (Font("Fira Sans", "SemiBold", 18.0f)); + titleLabel.setJustificationType (Justification::centred); + addAndMakeVisible (titleLabel); + + contentLabel.setText ("A newer version of Open Ephys GUI is available - would you like to download it?", dontSendNotification); + contentLabel.setFont (Font("Fira Sans", "Regular", 16.0f)); + contentLabel.setJustificationType (Justification::topLeft); + contentLabel.setMinimumHorizontalScale(1.0); + addAndMakeVisible (contentLabel); + + releaseNotesEditor.setMultiLine (true); + releaseNotesEditor.setReadOnly (true); + releaseNotesEditor.setText (releaseNotes); + addAndMakeVisible (releaseNotesEditor); + + addAndMakeVisible (downloadButton); + downloadButton.onClick = [this] { exitModalStateWithResult (1); }; + + addAndMakeVisible (cancelButton); + cancelButton.onClick = [this] + { + if(dontAskAgainButton.getToggleState()) + exitModalStateWithResult (-1); + else + exitModalStateWithResult(0); + }; + + dontAskAgainButton.setToggleState (!automaticVerCheck, dontSendNotification); + addAndMakeVisible (dontAskAgainButton); + +#if JUCE_MAC + File iconDir = File::getSpecialLocation(File::currentApplicationFile).getChildFile("Contents/Resources"); +#else + File iconDir = File::getSpecialLocation(File::currentApplicationFile).getParentDirectory(); +#endif + juceIcon = Drawable::createFromImageFile(iconDir.getChildFile("icon-small.png")); + lookAndFeelChanged(); + + setSize (640, 480); + } + + void resized() override + { + auto b = getLocalBounds().reduced (10); + + auto topSlice = b.removeFromTop (juceIconBounds.getHeight()) + .withTrimmedLeft (juceIconBounds.getWidth()); + + titleLabel.setBounds (topSlice.removeFromTop (25)); + topSlice.removeFromTop (5); + contentLabel.setBounds (topSlice.removeFromTop (25)); + + auto buttonBounds = b.removeFromBottom (60); + buttonBounds.removeFromBottom (25); + downloadButton.setBounds (buttonBounds.removeFromLeft (buttonBounds.getWidth() / 2).reduced (20, 0)); + cancelButton.setBounds (buttonBounds.reduced (20, 0)); + dontAskAgainButton.setBounds (cancelButton.getBounds().withY (cancelButton.getBottom() + 5).withHeight (20)); + + releaseNotesEditor.setBounds (b.reduced (0, 10)); + } + + void paint (Graphics& g) override + { + g.fillAll (Colours::lightgrey); + + if (juceIcon != nullptr) + juceIcon->drawWithin (g, juceIconBounds.toFloat(), + RectanglePlacement::stretchToFit, 1.0f); + } + + static std::unique_ptr launchDialog (const String& newVersionString, + const String& releaseNotes, + bool automaticVerCheck) + { + DialogWindow::LaunchOptions options; + + options.dialogTitle = "Download Open Ephys GUI version " + newVersionString + "?"; + options.resizable = false; + + auto* content = new UpdateDialog (newVersionString, releaseNotes, automaticVerCheck); + options.content.set (content, true); + + std::unique_ptr dialog (options.create()); + + content->setParentWindow (dialog.get()); + dialog->enterModalState (true, nullptr, true); + + return dialog; + } + +private: + void lookAndFeelChanged() override + { + cancelButton.setColour (TextButton::buttonColourId, Colours::crimson); + releaseNotesEditor.applyFontToAllText (Font("Fira Sans", "Regular", 16.0f)); + } + + void setParentWindow (DialogWindow* parent) + { + parentWindow = parent; + } + + void exitModalStateWithResult (int result) + { + if (parentWindow != nullptr) + parentWindow->exitModalState (result); + } + + Label titleLabel, contentLabel, releaseNotesLabel; + TextEditor releaseNotesEditor; + TextButton downloadButton { "Download" }, cancelButton { "Cancel" }; + ToggleButton dontAskAgainButton { "Don't ask again" }; + std::unique_ptr juceIcon; + juce::Rectangle juceIconBounds { 10, 10, 64, 64 }; + + DialogWindow* parentWindow = nullptr; +}; + +void LatestVersionCheckerAndUpdater::askUserForLocationToDownload (const Asset& asset) +{ + FileChooser chooser ("Please select the location into which you would like to download the new version", + { File::getSpecialLocation(File::userDesktopDirectory) }, + "*"); + + if (chooser.browseForDirectory()) + { + auto targetFolder = chooser.getResult(); + if (targetFolder == File{}) + return; + + File targetFile = targetFolder.getChildFile(asset.name).getNonexistentSibling(); + + downloadAndInstall (asset, targetFile); + } +} + +void LatestVersionCheckerAndUpdater::askUserAboutNewVersion (const String& newVersionString, + const String& releaseNotes, + const Asset& asset) +{ + dialogWindow = UpdateDialog::launchDialog (newVersionString, + releaseNotes, + mainWindow->automaticVersionChecking); + + if (auto* mm = ModalComponentManager::getInstance()) + { + mm->attachCallback (dialogWindow.get(), + ModalCallbackFunction::create ([this, asset] (int result) + { + if (result == 1) + askUserForLocationToDownload (asset); + else if(result == -1) + mainWindow->automaticVersionChecking = false; + else if(result == 0) + mainWindow->automaticVersionChecking = true; + + dialogWindow.reset(); + })); + } +} + +//============================================================================== +class DownloadThread : private ThreadWithProgressWindow +{ +public: + DownloadThread (const LatestVersionCheckerAndUpdater::Asset& a, + const File& t, + std::function&& cb) + : ThreadWithProgressWindow ("Downloading New Version", true, true), + asset (a), targetFile (t), completionCallback (std::move (cb)) + { + launchThread (3); + } + +private: + void run() override + { + setProgress (0.0); + + auto result = download (targetFile); + + if (result.failed()) + { + AlertWindow::showMessageBoxAsync (AlertWindow::WarningIcon, + "Downloading Failed", + result.getErrorMessage()); + } + else + { + setProgress (-1.0); + MessageManager::callAsync (completionCallback); + } + } + + Result download (File& dest) + { + setStatusMessage ("Downloading..."); + + int statusCode = 0; + URL downloadUrl (asset.url); + StringPairArray responseHeaders; + + auto inStream = downloadUrl.createInputStream (URL::InputStreamOptions (URL::ParameterHandling::inAddress) + .withExtraHeaders ("Accept: application/octet-stream") + .withConnectionTimeoutMs (5000) + .withResponseHeaders (&responseHeaders) + .withStatusCode (&statusCode) + .withNumRedirectsToFollow (1)); + + if (inStream != nullptr && statusCode == 200) + { + int64 total = 0; + + //Use the Url's input stream and write it to a file using output stream + std::unique_ptr out = dest.createOutputStream(); + + for (;;) + { + if (threadShouldExit()) + return Result::fail ("Cancelled"); + + + + auto written = out->writeFromInputStream(*inStream, 8192); + + if (written == 0) + break; + + total += written; + + setProgress((double)total / (double)asset.size); + + setStatusMessage ("Downloading... " + + File::descriptionOfSizeInBytes (total) + + " / " + + File::descriptionOfSizeInBytes (asset.size)); + } + + out->flush(); + return Result::ok(); + } + + return Result::fail ("Failed to download from: " + asset.url); + } + + const LatestVersionCheckerAndUpdater::Asset asset; + File targetFile; + std::function completionCallback; +}; + +static void runInstaller (const File& targetFile) +{ + bool runInstaller = AlertWindow::showOkCancelBox(AlertWindow::WarningIcon, + "Quit Open Ephys GUI?", + "To run the installer, the current instance of GUI needs to be closed." + "\nAre you sure you want to continue?", + "Yes", "No"); + + if(runInstaller) + { + #if JUCE_WINDOWS + if (targetFile.existsAsFile()) + { + auto returnCode = ShellExecute(NULL, (LPCSTR)"runas", targetFile.getFullPathName().toRawUTF8(), NULL, NULL, SW_SHOW); + + if((int)returnCode > 31) + JUCEApplication::getInstance()->systemRequestedQuit(); + else + LOGE("Failed to run the installer: ", GetLastError()); + } + #endif + } +} + +void LatestVersionCheckerAndUpdater::downloadAndInstall (const Asset& asset, const File& targetFile) +{ +#if JUCE_WINDOWS + File exeDir = File::getSpecialLocation( + File::SpecialLocationType::currentExecutableFile).getParentDirectory(); + + if(exeDir.findChildFiles(File::findFiles, false, "unins*").size() > 0) + { + downloader.reset (new DownloadThread (asset, targetFile, + [this, targetFile] + { + downloader.reset(); + runInstaller(targetFile); + + })); + } + else +#endif + { + String msgBoxString = String(); + + if(targetFile.getFileExtension().equalsIgnoreCase(".zip")) + { + msgBoxString = "Please extract the zip file located at: \n" + + targetFile.getFullPathName().quoted() + + "\nto your desired location and then run the updated version from there. " + "You can also overwrite the current installation after quitting the current instance."; + + } + else + { + msgBoxString = "Please quit the GUI first, then launch the installer file located at: \n" + + targetFile.getFullPathName().quoted() + + "\nand follow the steps to finish updating the GUI."; + } + + + downloader.reset (new DownloadThread (asset, targetFile, + [this, msgBoxString] + { + downloader.reset(); + + AlertWindow::showMessageBoxAsync + (AlertWindow::InfoIcon, + "Download successful!", + msgBoxString); + + })); + } +} + +//============================================================================== +JUCE_IMPLEMENT_SINGLETON (LatestVersionCheckerAndUpdater) diff --git a/Source/AutoUpdater.h b/Source/AutoUpdater.h new file mode 100644 index 0000000000..ede844c0c6 --- /dev/null +++ b/Source/AutoUpdater.h @@ -0,0 +1,74 @@ +/* + ------------------------------------------------------------------ + + This file is part of the Open Ephys GUI + Copyright (C) 2023 Open Ephys + + ------------------------------------------------------------------ + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + + Reference : https://github.com/juce-framework/JUCE/blob/6.0.8/extras/Projucer/Source/Application/jucer_AutoUpdater.h + +*/ + +#pragma once + +#include "../JuceLibraryCode/JuceHeader.h" + +class MainWindow; +class DownloadThread; + +/** + Helper class to check for new versions of the application and download them. +*/ +class LatestVersionCheckerAndUpdater : public DeletedAtShutdown, + private Thread +{ +public: + + /** Constructor */ + LatestVersionCheckerAndUpdater(); + + /** Destructor */ + ~LatestVersionCheckerAndUpdater() override; + + /** Holds information about a file to download */ + struct Asset + { + const String name; + const String url; + const int size; + }; + + /** Checks for a newer version of the GUI */ + void checkForNewVersion (bool isBackgroundCheck, MainWindow* mw); + + JUCE_DECLARE_SINGLETON_SINGLETHREADED_MINIMAL (LatestVersionCheckerAndUpdater) + +private: + + /** Download new version in background thread */ + void run() override; + + void askUserAboutNewVersion (const String&, const String&, const Asset& asset); + void askUserForLocationToDownload (const Asset& asset); + void downloadAndInstall (const Asset& asset, const File& targetFile); + + //============================================================================== + bool backgroundCheck = false; + MainWindow* mainWindow; + + std::unique_ptr downloader; + std::unique_ptr dialogWindow; +}; diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt index 543150a832..4b33825166 100644 --- a/Source/CMakeLists.txt +++ b/Source/CMakeLists.txt @@ -4,6 +4,8 @@ add_sources(open-ephys AccessClass.h AccessClass.cpp + AutoUpdater.cpp + AutoUpdater.h CoreServices.h CoreServices.cpp MainWindow.h diff --git a/Source/CoreServices.cpp b/Source/CoreServices.cpp index e92ef84a76..fd17571547 100644 --- a/Source/CoreServices.cpp +++ b/Source/CoreServices.cpp @@ -313,7 +313,7 @@ namespace CoreServices for (auto* node : getProcessorGraph()->getRecordNodes()) { if (node->getNodeId() == nodeId) - node->createNewDirectory(); + node->createNewDirectory(true); } } diff --git a/Source/MainWindow.cpp b/Source/MainWindow.cpp index 27f62bc8bb..a24d31b93d 100644 --- a/Source/MainWindow.cpp +++ b/Source/MainWindow.cpp @@ -25,6 +25,7 @@ #include "Utils/OpenEphysHttpServer.h" #include "UI/UIComponent.h" #include "UI/EditorViewport.h" +#include "AutoUpdater.h" #include @@ -44,15 +45,14 @@ MainWindow::MainWindow(const File& fileToLoad) if (activityLog.exists()) activityLog.deleteFile(); - OELogger::instance().createLogFile(activityLog.getFullPathName().toStdString()); + OELogger::GetInstance(activityLog.getFullPathName().toStdString()); + + LOGC("Session Start Time: ", Time::getCurrentTime().toString(true, true, true, true)); - std::cout << "Session Start Time: " << Time::getCurrentTime().toString(true, true, true, true) << std::endl; - std::cout << std::endl; LOGC("Open Ephys GUI v", JUCEApplication::getInstance()->getApplicationVersion(), " (Plugin API v", PLUGIN_API_VER, ")"); LOGC(SystemStats::getJUCEVersion()); LOGC("Operating System: ", SystemStats::getOperatingSystemName()); LOGC("CPU: ", SystemStats::getCpuModel(), " (", SystemStats::getNumCpus(), " core)"); - std::cout << std::endl; setResizable(true, // isResizable false); // useBottomCornerRisizer -- doesn't work very well @@ -60,6 +60,7 @@ MainWindow::MainWindow(const File& fileToLoad) shouldReloadOnStartup = true; shouldEnableHttpServer = true; openDefaultConfigWindow = false; + automaticVersionChecking = true; // Create ProcessorGraph and AudioComponent, and connect them. // Callbacks will be set by the play button in the control panel @@ -161,6 +162,10 @@ MainWindow::MainWindow(const File& fileToLoad) disableHttpServer(); } +#ifdef NDEBUG + if(automaticVersionChecking) + LatestVersionCheckerAndUpdater::getInstance()->checkForNewVersion (true, this); +#endif } MainWindow::~MainWindow() @@ -268,6 +273,7 @@ void MainWindow::saveWindowBounds() xml->setAttribute("version", JUCEApplication::getInstance()->getApplicationVersion()); xml->setAttribute("shouldReloadOnStartup", shouldReloadOnStartup); xml->setAttribute("shouldEnableHttpServer", shouldEnableHttpServer); + xml->setAttribute("automaticVersionChecking", automaticVersionChecking); XmlElement* bounds = new XmlElement("BOUNDS"); bounds->setAttribute("x",getScreenX()); @@ -331,6 +337,7 @@ void MainWindow::loadWindowBounds() shouldReloadOnStartup = xml->getBoolAttribute("shouldReloadOnStartup", false); shouldEnableHttpServer = xml->getBoolAttribute("shouldEnableHttpServer", false); + automaticVersionChecking = xml->getBoolAttribute("automaticVersionChecking", true); for (auto* e : xml->getChildIterator()) { @@ -338,21 +345,17 @@ void MainWindow::loadWindowBounds() if (e->hasTagName("BOUNDS")) { - int x = e->getIntAttribute("x"); - int y = e->getIntAttribute("y"); - int w = e->getIntAttribute("w"); - int h = e->getIntAttribute("h"); + String x = String(e->getIntAttribute("x")); + String y = String(e->getIntAttribute("y")); + String w = String(e->getIntAttribute("w")); + String h = String(e->getIntAttribute("h")); - // bool fs = e->getBoolAttribute("fullscreen"); + String windowBoundsString; + windowBoundsString = x + " " + y + " " + w + " " + h; - // without the correction, you get drift over time -#ifdef _WIN32 - setTopLeftPosition(x,y); //Windows doesn't need correction -#else - setTopLeftPosition(x,y-27); -#endif - getContentComponent()->setBounds(0,0,w-10,h-33); - //setFullScreen(fs); + LOGD("Loading Window Bounds: ", windowBoundsString); + restoreWindowStateFromString(windowBoundsString); + } else if (e->hasTagName("RECENTDIRECTORYNAMES")) { diff --git a/Source/MainWindow.h b/Source/MainWindow.h index 4839520267..551b0bf30d 100644 --- a/Source/MainWindow.h +++ b/Source/MainWindow.h @@ -68,8 +68,12 @@ class MainWindow : public DocumentWindow /** Determines whether the ProcessorGraph http server is enabled. */ bool shouldEnableHttpServer; + /** Determines whether the default config selection window needs to open on startup. */ bool openDefaultConfigWindow; + /** Determines whether the Auto Updater needs to run on startup. */ + bool automaticVersionChecking; + /** Ends the process() callbacks and disables all processors.*/ void shutDownGUI(); diff --git a/Source/Processors/Editors/CMakeLists.txt b/Source/Processors/Editors/CMakeLists.txt index 852d9fcb50..725b179444 100644 --- a/Source/Processors/Editors/CMakeLists.txt +++ b/Source/Processors/Editors/CMakeLists.txt @@ -20,6 +20,4 @@ add_sources(open-ephys PopupChannelSelector.h ) -#add nested directories - - +#add nested directories \ No newline at end of file diff --git a/Source/Processors/Editors/PopupChannelSelector.cpp b/Source/Processors/Editors/PopupChannelSelector.cpp index 8dd984f9a2..2abaac9369 100644 --- a/Source/Processors/Editors/PopupChannelSelector.cpp +++ b/Source/Processors/Editors/PopupChannelSelector.cpp @@ -60,7 +60,7 @@ void ChannelButton::paintButton(Graphics &g, bool isMouseOver, bool isButtonDown g.setColour(Colour(0,0,0)); g.fillRoundedRectangle(0.0f, 0.0f, getWidth(), getHeight(), 0.001*getWidth()); - if (isMouseOver) + if (isMouseOver && parent->isEditable()) { if (getToggleState()) g.setColour(parent->buttonColour.brighter()); @@ -130,12 +130,12 @@ PopupChannelSelector::PopupChannelSelector(PopupChannelSelector::Listener* liste maxSelectable(-1) { - int width = 368; //can use any multiples of 16 here for dynamic resizing + width = 368; //can use any multiples of 16 here for dynamic resizing - int nColumns = 16; - int nRows = nChannels / nColumns + (int)(!(nChannels % nColumns == 0)); - int buttonSize = width / 16; - int height = buttonSize * nRows; + nColumns = 16; + nRows = nChannels / nColumns + (int)(!(nChannels % nColumns == 0)); + buttonSize = width / 16; + height = buttonSize * nRows; maxSelectable = (maxSelectable == -1) ? nChannels : maxSelectable; maxSelectable = (maxSelectable > nChannels) ? nChannels : maxSelectable; @@ -209,6 +209,35 @@ PopupChannelSelector::PopupChannelSelector(PopupChannelSelector::Listener* liste } +void PopupChannelSelector::setEditable(bool editable) +{ + this->editable = editable; + + if (editable) + { + for (auto* btn : selectButtons) + btn->setVisible(true); + + if (nChannels > 8) + rangeEditor->setVisible(true); + } + else + { + for (auto* btn : selectButtons) + btn->setVisible(false); + + if (nChannels > 8) + rangeEditor->setVisible(false); + } + + //Resize window + if (editable) + setSize (width, buttonSize * nRows + buttonSize); + else + setSize(width, height); + +} + void PopupChannelSelector::setMaximumSelectableChannels(int num) { maxSelectable = num; diff --git a/Source/Processors/Editors/PopupChannelSelector.h b/Source/Processors/Editors/PopupChannelSelector.h index a249b01a84..3554032361 100644 --- a/Source/Processors/Editors/PopupChannelSelector.h +++ b/Source/Processors/Editors/PopupChannelSelector.h @@ -165,6 +165,10 @@ class PLUGIN_API PopupChannelSelector : OwnedArray channelButtons; + void setEditable(bool editable); + + bool isEditable() { return editable; } + private: Listener* listener; @@ -194,6 +198,12 @@ class PLUGIN_API PopupChannelSelector : Array channelStates; Array selectedButtons; Array activeChannels; + + int buttonSize; + int width; + int height; + int nRows; + int nColumns; }; diff --git a/Source/Processors/FileReader/FileReader.cpp b/Source/Processors/FileReader/FileReader.cpp index 7f2639164e..0f720d53bb 100644 --- a/Source/Processors/FileReader/FileReader.cpp +++ b/Source/Processors/FileReader/FileReader.cpp @@ -553,9 +553,9 @@ void FileReader::addEventsInRange(int64 start, int64 stop) { juce::int64 absoluteCurrentTimestamp = events.timestamps[i] + loopCount * (stopSample - startSample); - String msg = events.text[i]; - if (!msg.isEmpty()) + if (events.text.size() && !events.text[i].isEmpty()) { + String msg = events.text[i]; LOGD("Broadcasting message: ", msg, " at timestamp: ", absoluteCurrentTimestamp, " channel: ", events.channels[i]); broadcastMessage(msg); } diff --git a/Source/Processors/PluginManager/PluginManager.cpp b/Source/Processors/PluginManager/PluginManager.cpp index 9646dcac69..7903d9ceb1 100644 --- a/Source/Processors/PluginManager/PluginManager.cpp +++ b/Source/Processors/PluginManager/PluginManager.cpp @@ -36,15 +36,18 @@ static inline void closeHandle(decltype(LoadedLibInfo::handle) handle) { - if (handle) { + if (handle) { #ifdef _WIN32 - FreeLibrary(handle); + FreeLibrary(handle); #elif defined(__APPLE__) - CFRelease(handle); + CF::CFBundleUnloadExecutable(handle); + CF::CFRelease(handle); #else - dlclose(handle); + if (dlclose(handle) != 0) { + LOGE("Failed to close handle"); + } #endif - } + } } @@ -106,7 +109,7 @@ PluginManager::PluginManager() if(appDir.contains("plugin-GUI\\Build\\")) { - SetDllDirectory(sharedPath.getFullPathName().toUTF8()); + SetDllDirectory(sharedPath.getFullPathName().toRawUTF8()); } else { @@ -115,7 +118,7 @@ PluginManager::PluginManager() LOGD("Copying shared dependencies to ", installSharedPath.getFullPathName()); sharedPath.copyDirectoryTo(installSharedPath); } - SetDllDirectory(installSharedPath.getFullPathName().toUTF8()); + SetDllDirectory(installSharedPath.getFullPathName().toRawUTF8()); } #elif __linux__ @@ -228,29 +231,34 @@ void PluginManager::loadPlugins(const File &pluginPath) { */ int PluginManager::loadPlugin(const String& pluginLoc) { - /* - Load in the selected processor. This takes the - dynamic object (.so) and copies it into RAM - Dynamic linker requires a C-style string, so we - we have to convert first. - */ - const char* processorLocCString = static_cast(pluginLoc.toUTF8()); #ifdef _WIN32 HINSTANCE handle; - handle = LoadLibrary(processorLocCString); + const wchar_t* processorLocLPCWSTR = pluginLoc.toWideCharPointer(); + handle = LoadLibraryW(processorLocLPCWSTR); #elif defined(__APPLE__) - CF::CFURLRef bundleURL = CF::CFURLCreateFromFileSystemRepresentation(CF::kCFAllocatorDefault, - reinterpret_cast(processorLocCString), - strlen(processorLocCString), - true); - assert(bundleURL); - CF::CFBundleRef handle = CF::CFBundleCreate(CF::kCFAllocatorDefault, bundleURL); - CFRelease(bundleURL); + CF::CFStringRef processorLocCFString = pluginLoc.toCFString(); + CF::CFURLRef bundleURL = CF::CFURLCreateWithFileSystemPath(CF::kCFAllocatorDefault, + processorLocCFString, + CF::kCFURLPOSIXPathStyle, + true); + + assert(bundleURL); + CF::CFBundleRef handle = CF::CFBundleCreate(CF::kCFAllocatorDefault, bundleURL); + CF::CFRelease(bundleURL); + CF::CFRelease(processorLocCFString); #else // Clear errors dlerror(); + /* + Load in the selected processor. This takes the + dynamic object (.so) and copies it into RAM + Dynamic linker requires a C-style string, so we + we have to convert first. + */ + const char* processorLocCString = pluginLoc.toRawUTF8(); + /* Changing this to resolve all variables immediately upon loading. This will provide for quicker testing of the custom @@ -311,7 +319,7 @@ int PluginManager::loadPlugin(const String& pluginLoc) { return -1; } - LoadedLibInfo lib; + LoadedLibInfo lib{}; lib.apiVersion = libInfo.apiVersion; lib.name = libInfo.name; lib.libVersion = libInfo.libVersion; @@ -335,9 +343,8 @@ int PluginManager::loadPlugin(const String& pluginLoc) { info.name = pInfo.processor.name; info.type = pInfo.processor.type; info.libIndex = libArray.size()-1; - Plugin::ProcessorInfo pi = getProcessorInfo(String::fromUTF8(info.name)); - if(pi.name == nullptr) - processorPlugins.add(info); + processorPlugins.add(info); + break; } case Plugin::RECORD_ENGINE: @@ -347,9 +354,8 @@ int PluginManager::loadPlugin(const String& pluginLoc) { info.creator = pInfo.recordEngine.creator; info.name = pInfo.recordEngine.name; info.libIndex = libArray.size() - 1; - Plugin::RecordEngineInfo rei = getRecordEngineInfo(String::fromUTF8(info.name)); - if(rei.name == nullptr) - recordEnginePlugins.add(info); + recordEnginePlugins.add(info); + break; } case Plugin::DATA_THREAD: @@ -359,9 +365,8 @@ int PluginManager::loadPlugin(const String& pluginLoc) { info.creator = pInfo.dataThread.creator; info.name = pInfo.dataThread.name; info.libIndex = libArray.size() - 1; - Plugin::DataThreadInfo dti = getDataThreadInfo(String::fromUTF8(info.name)); - if(dti.name == nullptr) - dataThreadPlugins.add(info); + dataThreadPlugins.add(info); + break; } case Plugin::FILE_SOURCE: @@ -372,9 +377,8 @@ int PluginManager::loadPlugin(const String& pluginLoc) { info.name = pInfo.fileSource.name; info.extensions = pInfo.fileSource.extensions; info.libIndex = libArray.size(); - Plugin::FileSourceInfo fsi = getFileSourceInfo(String::fromUTF8(info.name)); - if(fsi.name == nullptr) - fileSourcePlugins.add(info); + fileSourcePlugins.add(info); + break; } default: @@ -539,8 +543,10 @@ bool PluginManager::findPlugin(String name, String libName, const Array indexToRemove) + processorPlugins[j].setLibIndex(processorPlugins[j].libIndex - 1); } + if(pluginIndex != -1) + processorPlugins.remove(pluginIndex); + break; } case Plugin::RECORD_ENGINE: { - LOGD("Removing record engine plugin"); + LOGD("Removing record engine plugin: ", pInfo.recordEngine.name); for(int j = 0; j < recordEnginePlugins.size(); j++) { if(recordEnginePlugins[j].name == pInfo.recordEngine.name) - { - recordEnginePlugins.remove(j); - break; - } + pluginIndex = j; + + if (recordEnginePlugins[j].libIndex > indexToRemove) + recordEnginePlugins[j].setLibIndex(recordEnginePlugins[j].libIndex - 1); } + if(pluginIndex != -1) + recordEnginePlugins.remove(pluginIndex); + break; } case Plugin::DATA_THREAD: { - LOGD("Adding data thread plugin"); + LOGD("Removing data thread plugin: ", pInfo.dataThread.name); for(int j = 0; j < dataThreadPlugins.size(); j++) { if(dataThreadPlugins[j].name == pInfo.dataThread.name) - { - dataThreadPlugins.remove(j); - break; - } + pluginIndex = j; + + if (dataThreadPlugins[j].libIndex > indexToRemove) + dataThreadPlugins[j].setLibIndex(dataThreadPlugins[j].libIndex - 1); } + if(pluginIndex != -1) + dataThreadPlugins.remove(pluginIndex); + break; } case Plugin::FILE_SOURCE: { - LOGD("Adding file source plugin"); + LOGD("Removing file source plugin: ", pInfo.fileSource.name); for(int j = 0; j < fileSourcePlugins.size(); j++) { if(fileSourcePlugins[j].name == pInfo.fileSource.name) - { - fileSourcePlugins.remove(j); - break; - } + pluginIndex = j; + + if (fileSourcePlugins[j].libIndex > indexToRemove) + fileSourcePlugins[j].setLibIndex(fileSourcePlugins[j].libIndex - 1); } + if(pluginIndex != -1) + fileSourcePlugins.remove(pluginIndex); + break; } default: { - LOGE("Inavlid plugin"); + LOGE("Invalid plugin"); break; } } } + closeHandle(lib.handle); libArray.remove(indexToRemove); return true; } diff --git a/Source/Processors/PluginManager/PluginManager.h b/Source/Processors/PluginManager/PluginManager.h index 9258269ce4..446ecb85cb 100644 --- a/Source/Processors/PluginManager/PluginManager.h +++ b/Source/Processors/PluginManager/PluginManager.h @@ -26,6 +26,7 @@ #include #include +#include #include #include "../../../JuceLibraryCode/JuceHeader.h" #include "OpenEphysPlugin.h" @@ -54,6 +55,12 @@ template struct LoadedPluginInfo : public T { int libIndex; + + // Setter function to modify the libIndex + void setLibIndex(int index) + { + libIndex = index; + } }; diff --git a/Source/Processors/ProcessorGraph/ProcessorGraph.cpp b/Source/Processors/ProcessorGraph/ProcessorGraph.cpp index d4cd5b2206..24abae4664 100644 --- a/Source/Processors/ProcessorGraph/ProcessorGraph.cpp +++ b/Source/Processors/ProcessorGraph/ProcessorGraph.cpp @@ -117,6 +117,7 @@ void ProcessorGraph::moveProcessor(GenericProcessor* processor, LOGD("New source: ", newSource->getName()); if (newDest != nullptr) LOGD("New dest: ", newDest->getName()); + LOGD("Move downstream: ", moveDownstream); processor->setSourceNode(nullptr); processor->setDestNode(nullptr); @@ -154,15 +155,27 @@ void ProcessorGraph::moveProcessor(GenericProcessor* processor, newDest->setSourceNode(processor); } else { processor->setDestNode(nullptr); + updateSettings(newDest); } } checkForNewRootNodes(processor, false, true); if (moveDownstream) // processor is further down the signal chain, its original dest may have changed - updateSettings(originalDest); + { + //LOGD("MOVE: Updating settings for ", originalDest->getNodeId()); + if (originalDest != nullptr) + updateSettings(originalDest); + else + updateSettings(processor); + } + else // processor is upstream of its original dest, so we can just update that + { + //LOGD("MOVE: Updating settings for ", processor->getNodeId()); updateSettings(processor); + } + } GenericProcessor* ProcessorGraph::createProcessor(Plugin::Description& description, @@ -437,7 +450,7 @@ bool ProcessorGraph::checkForNewRootNodes(GenericProcessor* processor, } else { - Merger* merger = (Merger*) processor->getDestNode(); + Merger* merger = (Merger*)p; GenericProcessor* sourceA = merger->getSourceNode(0); GenericProcessor* sourceB = merger->getSourceNode(1); @@ -1489,9 +1502,17 @@ void ProcessorGraph::setRecordState(bool isRecording) GenericProcessor* p = (GenericProcessor*) node->getProcessor(); if (isRecording) + { p->startRecording(); + if (p->getEditor() != nullptr) + p->getEditor()->startRecording(); + } else + { p->stopRecording(); + if (p->getEditor() != nullptr) + p->getEditor()->stopRecording(); + } } } diff --git a/Source/Processors/RecordNode/RecordNode.cpp b/Source/Processors/RecordNode/RecordNode.cpp index 3f456469ae..0bfcdc0a58 100755 --- a/Source/Processors/RecordNode/RecordNode.cpp +++ b/Source/Processors/RecordNode/RecordNode.cpp @@ -85,7 +85,7 @@ RecordNode::~RecordNode() void RecordNode::checkDiskSpace() { - int diskSpaceWarningThreshold = 5; //GB + float diskSpaceWarningThreshold = 5; //GB File dataDir(dataDirectory); int64 freeSpace = dataDir.getBytesFreeOnVolume(); @@ -94,7 +94,7 @@ void RecordNode::checkDiskSpace() if (availableBytes < diskSpaceWarningThreshold && !isRecording) { - String msg = "Less than " + String(diskSpaceWarningThreshold) + " GB of disk space available in " + String(dataDirectory.getFullPathName()); + String msg = "Less than " + String(int(diskSpaceWarningThreshold)) + " GB of disk space available in " + String(dataDirectory.getFullPathName()); msg += ". Recording may fail. Please free up space or change the recording directory."; AlertWindow::showMessageBoxAsync(AlertWindow::WarningIcon, "WARNING", msg); } @@ -103,18 +103,122 @@ void RecordNode::checkDiskSpace() String RecordNode::handleConfigMessage(String msg) { + /* + Available messages: + - engine= -- changes the record engine + - SELECT NONE / ALL / -- selects which channels to record, e.g.: + "SELECT 0 NONE" -- deselect all channels for stream 0 + "SELECT 1 1 2 3 4 5 6 7 8" -- select channels 1-8 for stream 1 + + */ + + if (CoreServices::getAcquisitionStatus()) + { + return "Cannot configure Record Node while acquisition is active."; + } const MessageManagerLock mml; StringArray tokens; tokens.addTokens (msg, "=", "\""); - if (tokens.size() != 2) return "Invalid msg"; + LOGD(tokens[0]); if (tokens[0] == "engine") - static_cast (getEditor())->engineSelectCombo->setSelectedItemIndex(std::stoi(tokens[1].toStdString()), sendNotification); - else - LOGD("Record Node: invalid engine key"); + { + if (tokens.size() == 2) + { + RecordNodeEditor* ed = static_cast (getEditor()); + + int engineIndex = tokens[1].getIntValue(); + + int numEngines = ed->engineSelectCombo->getNumItems(); + + if (engineIndex >= 0 && engineIndex < numEngines) + { + ed->engineSelectCombo->setSelectedItemIndex(engineIndex, sendNotification); + return "Record Node: updated record engine to " + ed->engineSelectCombo->getText(); + } + else { + return "Record Node: invalid engine index (max = " + String(numEngines - 1) + ")"; + } + + } + else + { + return "Record Node: invalid engine key"; + } + } + + tokens.clear(); + tokens.addTokens(msg, " ", ""); + + LOGD(tokens[0]); + + if (tokens[0] == "SELECT") + { + + if (tokens.size() >= 3) + { + + int streamIndex = tokens[1].getIntValue(); + uint16 streamId; + std::vector channelStates; + int channelCount; + + if (streamIndex >= 0 && streamIndex < dataStreams.size()) + { + streamId = dataStreams[streamIndex]->getStreamId(); + channelCount = dataStreams[streamIndex]->getChannelCount(); + } + else { + return "Record Node: Invalid stream index; max = " + String(dataStreams.size() - 1); + } + + if (tokens[2] == "NONE") + { + //select no channels + for (int i = 0; i < channelCount; i++) + { + channelStates.push_back(false); + } + } + else if (tokens[2] == "ALL") + { + //select all channels + for (int i = 0; i < channelCount; i++) + { + channelStates.push_back(true); + } + } + else + { + + Array channels; + + for (int i = 2; i < tokens.size(); i++) + { + int ch = tokens[i].getIntValue() - 1; + channels.add(ch); + } + + //select some channels + for (int i = 0; i < channelCount; i++) + { + if (channels.contains(i)) + channelStates.push_back(true); + else + channelStates.push_back(false); + } + } + + updateChannelStates(streamId, channelStates); + } + else + { + LOGD("Record Node: invalid config message"); + } + } return "Record Node received config: " + msg; } @@ -212,10 +316,12 @@ void RecordNode::setDataDirectory(File directory) dataDirectory = directory; newDirectoryNeeded = true; + createNewDirectory(); + checkDiskSpace(); } -void RecordNode::createNewDirectory() +void RecordNode::createNewDirectory(bool resetCounters) { LOGD("CREATE NEW DIRECTORY"); @@ -227,7 +333,7 @@ void RecordNode::createNewDirectory() File recordingDirectory = rootFolder; int index = 0; - while (recordingDirectory.exists()) + while (resetCounters && recordingDirectory.exists()) { index += 1; recordingDirectory = File(rootFolder.getFullPathName() + " (" + String(index) + ")"); @@ -240,9 +346,12 @@ void RecordNode::createNewDirectory() newDirectoryNeeded = false; - recordingNumber = 0; - experimentNumber = 1; - LOGD("RecordNode::createNewDirectory(): experimentNumber = 1"); + if (resetCounters) + { + recordingNumber = 0; + experimentNumber = 1; + LOGD("RecordNode::createNewDirectory(): experimentNumber = 1"); + } settingsNeeded = true; } diff --git a/Source/Processors/RecordNode/RecordNode.h b/Source/Processors/RecordNode/RecordNode.h index 9ee50db2b1..ff8c88b080 100755 --- a/Source/Processors/RecordNode/RecordNode.h +++ b/Source/Processors/RecordNode/RecordNode.h @@ -119,7 +119,7 @@ class RecordNode : String generateDirectoryName(); /* Creates a new recording directory*/ - void createNewDirectory(); + void createNewDirectory(bool resetCounters = false); /* Callback for responding to changes in data-directory-related settings*/ void filenameComponentChanged(FilenameComponent*); diff --git a/Source/Processors/RecordNode/RecordNodeEditor.cpp b/Source/Processors/RecordNode/RecordNodeEditor.cpp index e86fc8b107..2395707271 100644 --- a/Source/Processors/RecordNode/RecordNodeEditor.cpp +++ b/Source/Processors/RecordNode/RecordNodeEditor.cpp @@ -148,7 +148,6 @@ void RecordNodeEditor::stopRecording() spikeRecord->setEnabled(true); } - void RecordNodeEditor::comboBoxChanged(ComboBox* box) { @@ -261,7 +260,7 @@ void RecordNodeEditor::updateSettings() spikeRecord->setToggleState(recordNode->recordSpikes, dontSendNotification); dataPathLabel->setText(recordNode->getDataDirectory().getFullPathName(), dontSendNotification); - + dataPathLabel->setTooltip(dataPathLabel->getText()); } void RecordNodeEditor::buttonClicked(Button *button) @@ -505,6 +504,7 @@ void FifoMonitor::mouseDown(const MouseEvent &event) bool editable = !recordNode->recordThread->isThreadRunning(); auto* channelSelector = new PopupChannelSelector(this, channelStates); channelSelector->setChannelButtonColour(Colours::red); + channelSelector->setEditable(!recordNode->getRecordingStatus()); CallOutBox& myBox = CallOutBox::launchAsynchronously (std::unique_ptr(channelSelector), getScreenBounds(), nullptr); @@ -576,19 +576,20 @@ void FifoMonitor::timerCallback() lastUpdateTime = currentTime; lastFreeSpace = bytesFree; - recordingTimeLeftInSeconds = (int) (bytesFree / dataRate / 1000.0f); + recordingTimeLeftInSeconds = bytesFree / dataRate / 1000.0f; LOGD("Data rate: ", dataRate, " bytes/ms"); // Stop recording and show warning when less than 5 minutes of disk space left - if (dataRate > 0 && recordingTimeLeftInSeconds < 60*5) { + if (dataRate > 0.0f && recordingTimeLeftInSeconds < (60.0f * 5.0f)) + { CoreServices::setRecordingStatus(false); String msg = "Recording stopped. Less than 5 minutes of disk space remaining."; AlertWindow::showMessageBoxAsync(AlertWindow::WarningIcon, "WARNING", msg); } String msg = String(bytesFree / pow(2, 30)) + " GB available\n"; - msg += String(recordingTimeLeftInSeconds / 60) + " minutes remaining\n"; + msg += String(int(recordingTimeLeftInSeconds / 60.0f)) + " minutes remaining\n"; msg += "Data rate: " + String(dataRate * 1000 / pow(2, 20), 2) + " MB/s"; setTooltip(msg); } diff --git a/Source/Processors/RecordNode/RecordNodeEditor.h b/Source/Processors/RecordNode/RecordNodeEditor.h index 75f025a81a..d2bd1dbff1 100644 --- a/Source/Processors/RecordNode/RecordNodeEditor.h +++ b/Source/Processors/RecordNode/RecordNodeEditor.h @@ -101,7 +101,7 @@ private : float dataRate; float lastFreeSpace; float lastUpdateTime; - int recordingTimeLeftInSeconds; + float recordingTimeLeftInSeconds; }; diff --git a/Source/Processors/RecordNode/SyncChannelSelector.cpp b/Source/Processors/RecordNode/SyncChannelSelector.cpp index 3ca7145a42..47abe95d08 100644 --- a/Source/Processors/RecordNode/SyncChannelSelector.cpp +++ b/Source/Processors/RecordNode/SyncChannelSelector.cpp @@ -98,7 +98,8 @@ SyncChannelSelector::SyncChannelSelector(int nChans, int selectedIdx, bool isPri nChannels(nChans), selectedId(selectedIdx), isPrimary(isPrimary_), - detectedChange(false) + detectedChange(false), + editable(true) { width = 368; //can use any multiples of 16 here for dynamic resizing @@ -145,6 +146,16 @@ SyncChannelSelector::SyncChannelSelector(int nChans, int selectedIdx, bool isPri } +void SyncChannelSelector::setEditable(bool editable) +{ + this->editable = editable; + + for (int i = 0; i < buttons.size(); i++) + buttons[i]->setEnabled(editable); + + setSize(width, buttonSize + buttonSize * (editable && !isPrimary)); +} + SyncChannelSelector::~SyncChannelSelector() {} void SyncChannelSelector::mouseMove(const MouseEvent &event) {} diff --git a/Source/Processors/RecordNode/SyncChannelSelector.h b/Source/Processors/RecordNode/SyncChannelSelector.h index 223a541fd7..43bbd8a9f4 100644 --- a/Source/Processors/RecordNode/SyncChannelSelector.h +++ b/Source/Processors/RecordNode/SyncChannelSelector.h @@ -104,8 +104,14 @@ class SyncChannelSelector : public Component, public Button::Listener OwnedArray buttons; + void setEditable(bool editable); + + bool isEditable() { return editable; } + private: + bool editable; + ScopedPointer setPrimaryStreamButton; }; diff --git a/Source/Processors/RecordNode/SyncControlButton.cpp b/Source/Processors/RecordNode/SyncControlButton.cpp index 787c1608cc..61d452566d 100644 --- a/Source/Processors/RecordNode/SyncControlButton.cpp +++ b/Source/Processors/RecordNode/SyncControlButton.cpp @@ -86,13 +86,15 @@ void SyncControlButton::componentBeingDeleted(Component &component) void SyncControlButton::mouseUp(const MouseEvent &event) { - if (!CoreServices::getRecordingStatus() && event.mods.isLeftButtonDown()) + if (event.mods.isLeftButtonDown()) { int syncLine = node->getSyncLine(streamId); SyncChannelSelector* channelSelector = new SyncChannelSelector (ttlLineCount, syncLine, node->isMainDataStream(streamId)); + channelSelector->setEditable(!CoreServices::getRecordingStatus()); + CallOutBox& myBox = CallOutBox::launchAsynchronously (std::unique_ptr(channelSelector), getScreenBounds(), nullptr); diff --git a/Source/UI/DataViewport.cpp b/Source/UI/DataViewport.cpp index 262c1878a6..45a18c9fe1 100755 --- a/Source/UI/DataViewport.cpp +++ b/Source/UI/DataViewport.cpp @@ -47,7 +47,8 @@ int DataViewport::addTabToDataViewport(String name, if (tabArray.size() == 0) setVisible(true); - + //if (tabArray.contains(tabIndex)) + // return tabIndex; addTab(name, Colours::lightgrey, component, false, tabIndex); @@ -59,11 +60,11 @@ int DataViewport::addTabToDataViewport(String name, tabArray.add(tabIndex); //std::cout << "Tab Array: "; - // for (int i = 0; i < tabArray.size(); i++) - // std::cout << tabArray[i] << " "; - // std::cout << std::endl; + //for (int i = 0; i < tabArray.size(); i++) + // std::cout << tabArray[i] << " "; + //std::cout << std::endl; - //LOGD("Data Viewport adding tab with index ", tabIndex); + LOGD("Data Viewport adding tab with index ", tabIndex); setCurrentTabIndex(tabArray.size()-1); @@ -76,10 +77,13 @@ int DataViewport::addTabToDataViewport(String name, void DataViewport::addTabAtIndex(int tabIndex_, String tabName, Component* tabComponent) { - savedTabIndices.add(tabIndex_); - savedTabComponents.add(tabComponent); - savedTabNames.add(tabName); - + if (!savedTabIndices.contains(tabIndex_)) + { + savedTabIndices.add(tabIndex_); + savedTabComponents.add(tabComponent); + savedTabNames.add(tabName); + } + } @@ -108,7 +112,7 @@ void DataViewport::destroyTab(int index) setCurrentTabIndex(tabArray.size()-1); //std::cout << "Tab Array: "; - // for (int i = 0; i < tabArray.size(); i++) + //for (int i = 0; i < tabArray.size(); i++) // std::cout << tabArray[i] << " "; //std::cout << std::endl; @@ -126,6 +130,8 @@ void DataViewport::saveStateToXml(XmlElement* xml) void DataViewport::loadStateFromXml(XmlElement* xml) { + //LOGD("DataViewport::loadStateFromXml()"); + std::vector tabOrder(savedTabIndices.size()); std::iota(tabOrder.begin(), tabOrder.end(), 0); //Initializing sort(tabOrder.begin(), tabOrder.end(), [&](int i, int j) diff --git a/Source/UI/DefaultConfig.cpp b/Source/UI/DefaultConfig.cpp index 88b7a89daf..f9cb88df1f 100644 --- a/Source/UI/DefaultConfig.cpp +++ b/Source/UI/DefaultConfig.cpp @@ -212,17 +212,45 @@ void DefaultConfigComponent::resized() void DefaultConfigComponent::buttonClicked(Button* button) { - if(button == goButton.get()) + if (button == goButton.get()) { // Get selected config file name with full path String filePath; - if(acqBoardButton->getToggleState()) - filePath = "configs" + File::getSeparatorString() + "acq_board_config.xml"; - else if(fileReaderButton->getToggleState()) + if (acqBoardButton->getToggleState()) + { + int response = AlertWindow::showYesNoCancelBox(AlertWindow::QuestionIcon, "Select acquisition board type", + "What type of FPGA does your acquisition board have? \n\n" + "If it was delivered by Open Ephys Production Site after " + "November 2022, it has a custom FPGA designed by Open Ephys. \n\n" + "Older acquisition boards likely use an Opal Kelly FPGA.", + "Open Ephys FPGA", "Opal Kelly FPGA", "Cancel"); + + if (response == 1) // OE FPGA + { + LOGA("Selected Open Ephys FPGA"); + filePath = "configs" + File::getSeparatorString() + "oe_acq_board_config.xml"; + } + else if (response == 2) + { + LOGA("Selected Opal Kelly FPGA"); + filePath = "configs" + File::getSeparatorString() + "acq_board_config.xml"; + } + else { + return; + } + + } + else if (fileReaderButton->getToggleState()) + { filePath = "configs" + File::getSeparatorString() + "file_reader_config.xml"; + } + else + { filePath = "configs" + File::getSeparatorString() + "neuropixels_pxi_config.xml"; + } + #ifdef __APPLE__ File configFile = File::getSpecialLocation(File::currentApplicationFile) @@ -242,7 +270,7 @@ void DefaultConfigComponent::buttonClicked(Button* button) dw->exitModalState (0); } - else if(button->getRadioGroupId() == 101) + else if (button->getRadioGroupId() == 101) { this->repaint(); } diff --git a/Source/UI/EditorViewport.cpp b/Source/UI/EditorViewport.cpp index bfe0f862c5..601e357616 100755 --- a/Source/UI/EditorViewport.cpp +++ b/Source/UI/EditorViewport.cpp @@ -40,7 +40,7 @@ EditorViewport::EditorViewport(SignalChainTabComponent* s_) somethingIsBeingDraggedOver(false), shiftDown(false), lastEditorClicked(0), - selectionIndex(0), + selectionIndex(-1), insertionPoint(0), componentWantsToMove(false), indexOfMovingComponent(-1), @@ -398,13 +398,41 @@ void EditorViewport::moveSelection(const KeyPress& key) if (key.getKeyCode() == key.leftKey) { - if (mk.isShiftDown()) + if (mk.isShiftDown() + && lastEditorClicked != 0 + && editorArray.contains(lastEditorClicked)) { - selectionIndex--; + int primaryIndex = editorArray.indexOf(lastEditorClicked); + + // set new selection index + if (selectionIndex == -1) + { + // if no selection index has been set yet, set it to the primary index + selectionIndex = primaryIndex == 0 ? 0 : primaryIndex - 1; + } + else if (selectionIndex == 0) + { + // if the selection index is already at the left edge, return + return; + } + else if(selectionIndex <= primaryIndex) + { + // if previous selection index is to the left of the primary index, decrement it + selectionIndex--; + } + + // switch selection state of the editor at the new selection index + if (selectionIndex != primaryIndex) + editorArray[selectionIndex]->switchSelectedState(); + + // if the selection index is to the right of the primary index, + // decrement it after switching the selection state + if (selectionIndex > primaryIndex) + selectionIndex--; } else { - selectionIndex = 0; + selectionIndex = -1; for (int i = 0; i < editorArray.size(); i++) { @@ -421,14 +449,41 @@ void EditorViewport::moveSelection(const KeyPress& key) else if (key.getKeyCode() == key.rightKey) { - if (mk.isShiftDown()) + if (mk.isShiftDown() + && lastEditorClicked != 0 + && editorArray.contains(lastEditorClicked)) { - selectionIndex++; + int primaryIndex = editorArray.indexOf(lastEditorClicked); + + if (selectionIndex == -1) + { + // if no selection index has been set yet, set it to the primary index + selectionIndex = primaryIndex == (editorArray.size() - 1) ? primaryIndex : primaryIndex + 1; + } + else if (selectionIndex == editorArray.size() - 1) + { + // if the selection index is already at the right edge, return + return; + } + else if (selectionIndex >= primaryIndex) + { + // if previous selection index is to the right of the primary index, increment it + selectionIndex++; + } + + // switch selection state of the editor at the new selection index + if (selectionIndex != primaryIndex) + editorArray[selectionIndex]->switchSelectedState(); + + // if the selection index is to the left of the primary index, + // increment it after switching the selection state + if (selectionIndex < primaryIndex) + selectionIndex++; } else { - selectionIndex = 0; + selectionIndex = -1; // bool stopSelection = false; int i = 0; @@ -452,30 +507,6 @@ void EditorViewport::moveSelection(const KeyPress& key) } } } - - if (mk.isShiftDown() && lastEditorClicked != 0 && editorArray.contains(lastEditorClicked)) - { - - LOGDD("Selection index: ", selectionIndex); - - int startIndex = editorArray.indexOf(lastEditorClicked); - - if (selectionIndex < 0) - { - - for (int i = startIndex-1; i >= startIndex + selectionIndex; i--) - { - editorArray[i]->select(); - } - - } else if (selectionIndex > 0) - { - for (int i = startIndex+1; i <= startIndex + selectionIndex; i++) - { - editorArray[i]->select(); - } - } - } } bool EditorViewport::keyPressed(const KeyPress& key) @@ -781,6 +812,13 @@ void EditorViewport::mouseDown(const MouseEvent& e) m.addItem(6, "Save image...", true); + Plugin::Type type = editorArray[i]->getProcessor()->getPluginType(); + if (type != Plugin::Type::BUILT_IN && type != Plugin::Type::INVALID) + { + m.addSeparator(); + String pluginVer = editorArray[i]->getProcessor()->getLibVersion(); + m.addItem(7, "Plugin v" + pluginVer, false); + } const int result = m.show(); @@ -894,11 +932,12 @@ void EditorViewport::mouseDown(const MouseEvent& e) } } - lastEditorClicked = editorArray[i]; + selectionIndex = i; break; } lastEditorClicked = editorArray[i]; + selectionIndex = -1; } else { @@ -1677,6 +1716,8 @@ const String EditorViewport::loadStateFromXml(XmlElement* xml) return "Failed To Open " + currentFile.getFileName(); } + /* commented out in version 0.6.6 + if (!sameVersion) { String responseString = "Your configuration file was saved from a different version of the GUI than the one you're using. \n"; @@ -1702,7 +1743,7 @@ const String EditorViewport::loadStateFromXml(XmlElement* xml) { return "Failed To Open " + currentFile.getFileName(); } - } + }*/ MouseCursor::showWaitCursor(); diff --git a/Source/UI/EditorViewportActions.cpp b/Source/UI/EditorViewportActions.cpp index e45ddcdb34..e39eb10f29 100644 --- a/Source/UI/EditorViewportActions.cpp +++ b/Source/UI/EditorViewportActions.cpp @@ -291,7 +291,7 @@ MoveProcessor::~MoveProcessor() bool MoveProcessor::perform() { - LOGDD("Peforming MOVE for processor ", nodeId); + LOGD("Peforming MOVE for processor ", nodeId); GenericProcessor* processor = AccessClass::getProcessorGraph()->getProcessorWithNodeId(nodeId); @@ -310,7 +310,7 @@ bool MoveProcessor::perform() bool MoveProcessor::undo() { - LOGDD("Undoing MOVE for processor ", nodeId); + LOGD("Undoing MOVE for processor ", nodeId); GenericProcessor* processor = AccessClass::getProcessorGraph()->getProcessorWithNodeId(nodeId); @@ -321,7 +321,7 @@ bool MoveProcessor::undo() AccessClass::getProcessorGraph()->moveProcessor(processor, sourceProcessor, destProcessor, - moveDownstream); + !moveDownstream); if (processor->isSource() && originalDestNodeDestNodeId > -1) { @@ -330,7 +330,7 @@ bool MoveProcessor::undo() AccessClass::getProcessorGraph()->moveProcessor(originalDest, destProcessor, originalDest->getDestNode(), - moveDownstream); + !moveDownstream); } return true; diff --git a/Source/UI/PluginInstaller.cpp b/Source/UI/PluginInstaller.cpp index 771443b59f..7f3dc0a3cf 100644 --- a/Source/UI/PluginInstaller.cpp +++ b/Source/UI/PluginInstaller.cpp @@ -35,6 +35,7 @@ #include #endif +namespace fs = std::filesystem; //----------------------------------------------------------------------- static inline File getPluginsDirectory() { @@ -483,7 +484,7 @@ void PluginInstallerComponent::run() checkForUpdates = false; } - if(updatablePlugins.size() > 0) + /*if (updatablePlugins.size() > 0) { const String updatemsg = "Some of your plugins have updates available! " "Please update them to get the latest features and bug-fixes."; @@ -491,7 +492,7 @@ void PluginInstallerComponent::run() AlertWindow::showMessageBoxAsync(AlertWindow::AlertIconType::InfoIcon, "Updates Available", updatemsg, "OK", this); - } + }*/ } void PluginInstallerComponent::buttonClicked(Button* button) @@ -996,9 +997,17 @@ void PluginInfoComponent::buttonClicked(Button* button) if(!uninstallPlugin(pInfo.pluginName)) { LOGE("Failed to uninstall ", pInfo.displayName); + AlertWindow::showMessageBoxAsync(AlertWindow::WarningIcon, + "[Plugin Installer] " + pInfo.displayName, + "Failed to uninstall " + pInfo.displayName); } else + { LOGC(pInfo.displayName, " uninstalled successfully!"); + AlertWindow::showMessageBoxAsync(AlertWindow::InfoIcon, + "[Plugin Installer] " + pInfo.displayName, + pInfo.displayName + " uninstalled successfully"); + } } else if (button == &documentationButton) { @@ -1015,6 +1024,46 @@ void PluginInfoComponent::setDownloadURL(const String& url) void PluginInfoComponent::run() { + // Check if plugin already present in signal chain + bool pluginInUse = false; + if(pInfo.type == "RecordEngine" && AccessClass::getProcessorGraph()->hasRecordNode()) + { + pluginInUse = true; + } + else + { + auto processors = AccessClass::getProcessorGraph()->getListOfProcessors(); + for (auto* p : processors) + { + if (p->getLibName().equalsIgnoreCase(pInfo.displayName)) + { + pluginInUse = true; + break; + } + } + } + + if (pluginInUse) + { + AlertWindow::showMessageBoxAsync(AlertWindow::WarningIcon, + "[Plugin Installer] " + pInfo.displayName, + pInfo.displayName + " is already in use. Please remove all instances of it from the signal chain and try again."); + + LOGE("Error.. Plugin already in use. Please remove it from the signal chain and try again."); + return; + } + + // Remove older version of the plugin if present + if(!AccessClass::getPluginManager()->removePlugin(pInfo.displayName)) + { + AlertWindow::showMessageBoxAsync(AlertWindow::WarningIcon, + "[Plugin Installer] ERROR", + "Unable to remove current installed version of " + pInfo.displayName + "... Plugin update failed."); + + LOGE("Unable to remove current installed version of " + pInfo.displayName + "... Plugin update failed."); + return; + } + // If a plugin has depencies outside its zip, download them for (int i = 0; i < pInfo.dependencies.size(); i++) { @@ -1023,7 +1072,11 @@ void PluginInfoComponent::run() int retCode = downloadPlugin(pInfo.dependencies[i], pInfo.dependencyVersions[i], true); - if (retCode == 2) + if (retCode == 1) + { + continue; + } + else if (retCode == 2) { AlertWindow::showMessageBoxAsync(AlertWindow::WarningIcon, "[Plugin Installer] " + pInfo.dependencies[i], @@ -1052,11 +1105,21 @@ void PluginInfoComponent::run() LOGE("HTTP request failed!! Please check your internet connection..."); return; } + else + { + AlertWindow::showMessageBoxAsync(AlertWindow::WarningIcon, + "[Plugin Installer] " + pInfo.dependencies[i], + "An unknown error occured while installing dependencies for " + pInfo.displayName + + ". Please contact the developers."); + + LOGE("Download Failed!!"); + return; + } } setStatusMessage("Downloading " + pInfo.displayName + " ..."); - LOGD("Downloading Plugin: ", pInfo.pluginName, "... "); + LOGC("Downloading Plugin: ", pInfo.displayName, " | Version: ", pInfo.selectedVersion); // download the plugin int dlReturnCode = downloadPlugin(pInfo.pluginName, pInfo.selectedVersion, false); @@ -1067,7 +1130,7 @@ void PluginInfoComponent::run() "[Plugin Installer] " + pInfo.displayName, pInfo.displayName + " Installed Successfully"); - LOGD("Download Successfull!!"); + LOGC("Download Successfull!!"); pInfo.installedVersion = pInfo.selectedVersion; installedVerText.setText(pInfo.installedVersion, dontSendNotification); @@ -1142,18 +1205,9 @@ void PluginInfoComponent::run() if(pInfo.latestVersion.equalsIgnoreCase(pInfo.latestVersion)) { updatablePlugins.removeString(pInfo.pluginName); - this->getParentComponent()->resized(); + this->getParentComponent()->repaint(); } } - else if (dlReturnCode == PLUGIN_IN_USE || dlReturnCode == RECNODE_IN_USE) - { - String name = (dlReturnCode == PLUGIN_IN_USE) ? pInfo.displayName : "Record Node"; - AlertWindow::showMessageBoxAsync(AlertWindow::WarningIcon, - "[Plugin Installer] " + pInfo.displayName, - name + " is already in use. Please remove it from the signal chain and try again."); - - LOGE("Error.. Plugin already in use. Please remove it from the signal chain and try again."); - } else if(dlReturnCode == HTTP_ERR) { AlertWindow::showMessageBoxAsync(AlertWindow::WarningIcon, @@ -1282,14 +1336,18 @@ bool PluginInfoComponent::uninstallPlugin(const String& plugin) LOGC("Uninstalling plugin: ", pInfo.displayName); // Check whether the plugin is loaded in a signal chain - if(AccessClass::getProcessorGraph()->processorWithSameNameExists(pInfo.displayName)) + auto processors = AccessClass::getProcessorGraph()->getListOfProcessors(); + for (auto* p : processors) { - AlertWindow::showMessageBoxAsync(AlertWindow::WarningIcon, - "[Plugin Installer] " + pInfo.displayName, - pInfo.displayName + " is already in use. Please remove it from the signal chain and try again."); - - LOGD("Plugin present in signal chain! Please remove it before uninstalling the plugin."); - return false; + if (p->getLibName().equalsIgnoreCase(pInfo.displayName)) + { + AlertWindow::showMessageBoxAsync(AlertWindow::WarningIcon, + "[Plugin Installer] " + pInfo.displayName, + pInfo.displayName + " is already in use. Please remove all instances of it from the signal chain and try again."); + + LOGD("Plugin present in signal chain! Please remove it before uninstalling the plugin."); + return false; + } } // Open installedPluings.xml file @@ -1313,38 +1371,15 @@ bool PluginInfoComponent::uninstallPlugin(const String& plugin) dllName = pluginElement->getAttributeValue(1); } - // Remove plugin from PluginManager + // Remove and unload plugin via PluginManager if(!AccessClass::getPluginManager()->removePlugin(pInfo.displayName)) return false; - //delete plugin file - File pluginFile = getPluginsDirectory().getChildFile(dllName); - if(!pluginFile.deleteRecursively()) - { - LOGD("Unable to delete ", pluginFile.getFullPathName(), " ...Trying again!"); - -#ifdef _WIN32 - const char* processorLocCString = static_cast(pluginFile.getFullPathName().toUTF8()); - HMODULE md = GetModuleHandleA(processorLocCString); - - if(FreeLibrary(md)) - LOGD("Unloaded ", dllName); - - if(!pluginFile.deleteFile()) - { - return false; - } -#else - return false; -#endif - } - // Remove plugin XML entry xml->getFirstChildElement()->removeChildElement(pluginElement, true); if (! xml->writeTo(xmlFile)) { LOGD("Error! Couldn't write to installedPlugins.xml"); - return false; } AccessClass::getProcessorList()->fillItemList(); @@ -1358,6 +1393,14 @@ bool PluginInfoComponent::uninstallPlugin(const String& plugin) downloadButton.setButtonText("Install"); installedVerText.setText("No", dontSendNotification); + //delete plugin file + File pluginFile = getPluginsDirectory().getChildFile(dllName); + if(!pluginFile.deleteRecursively()) + { + LOGE("Unable to delete plugin file ", pluginFile.getFullPathName(), " ... Please remove it manually!!"); + return false; + } + return true; } @@ -1380,7 +1423,7 @@ int PluginInfoComponent::downloadPlugin(const String& plugin, const String& vers // Could not retrieve data if(!fileStream) - return 9; + return 7; // ZIP file empty, return. if(fileStream->getTotalLength() == 0) @@ -1466,76 +1509,79 @@ int PluginInfoComponent::downloadPlugin(const String& plugin, const String& vers child->addChildElement(pluginEntry.release()); } - - // Check if plugin already present in signal chain - if(pInfo.type == "RecordEngine" && AccessClass::getProcessorGraph()->hasRecordNode()) - { - pluginFile.deleteFile(); - return 8; - } - else if(AccessClass::getProcessorGraph()->processorWithSameNameExists(pInfo.displayName)) - { - pluginFile.deleteFile(); - return 7; - } } - // Uncompress plugin zip file in temp directory - String pluginDllPath; - + // Create temp directory to uncompress the plugin File tempDir = File::getSpecialLocation(File::tempDirectory).getChildFile("open-ephys"); tempDir.createDirectory(); - pluginZip.uncompressTo(tempDir); + // Delete any existing files in temp directory + if (tempDir.getChildFile("plugins").exists()) + tempDir.getChildFile("plugins").deleteRecursively(); - if(!isDependency) - { - // copy plugin DLL from temp directory to actual location - bool copySuccess = tempDir.getChildFile("plugins").getChildFile(dllName) - .copyFileTo(getPluginsDirectory().getChildFile(dllName)); + if (tempDir.getChildFile("shared").exists()) + tempDir.getChildFile("shared").deleteRecursively(); - File dllFile = getPluginsDirectory().getChildFile(dllName); - pluginDllPath = dllFile.getFullPathName(); + // Uncompress the plugin zip file to temp directory + juce::Result res = pluginZip.uncompressTo(tempDir); - if(!copySuccess && dllFile.exists()) - { -#ifdef _WIN32 - const char* processorLocCString = static_cast(pluginDllPath.toUTF8()); - HMODULE md = GetModuleHandleA(processorLocCString); + if (res.failed()) + { + LOGE("Failed to uncompress plugin zip file: ", res.getErrorMessage()); + tempDir.deleteRecursively(); + pluginFile.deleteFile(); + return 2; + } - if(FreeLibrary(md)) - LOGD("Unloaded old ", dllName); - - // try copying again after unloading old DLL - copySuccess = tempDir.getChildFile("plugins").getChildFile(dllName) - .copyFileTo(getPluginsDirectory().getChildFile(dllName)); + String pluginDllPath; - if(!copySuccess) - { - LOGC("Unable to replace/update exisiting plugin file!"); + // copy plugin DLL from temp directory to actual location + if(!isDependency) + { + fs::path tempPluginPath = tempDir.getChildFile("plugins").getFullPathName().toStdString(); + fs::path destPluginPath = getPluginsDirectory().getFullPathName().toStdString(); + + // Copy only if plugin file exists + if(fs::exists(tempPluginPath)) + { + const auto copyOptions = fs::copy_options::overwrite_existing + | fs::copy_options::recursive; + try { + fs::copy(tempPluginPath, destPluginPath, copyOptions); + } catch(fs::filesystem_error& e) { + LOGE("Could not copy plugin files: \"", e.what(), "\""); + tempDir.deleteRecursively(); pluginFile.deleteFile(); return 2; } -#endif } + else + { + LOGE("Plugin file not found in temp directory!!"); + tempDir.deleteRecursively(); + pluginFile.deleteFile(); + return 2; + } + + pluginDllPath = getPluginsDirectory().getChildFile(dllName).getFullPathName(); } /* Copy shared files * Uses C++17's filesystem::copy functionality to allow copying symlinks */ - std::filesystem::path tempSharedPath = tempDir.getChildFile("shared").getFullPathName().toStdString(); - std::filesystem::path destSharedPath = getSharedDirectory().getFullPathName().toStdString(); + fs::path tempSharedPath = tempDir.getChildFile("shared").getFullPathName().toStdString(); + fs::path destSharedPath = getSharedDirectory().getFullPathName().toStdString(); // Copy only if shared files exist - if(std::filesystem::exists(tempSharedPath)) + if(fs::exists(tempSharedPath)) { - const auto copyOptions = std::filesystem::copy_options::overwrite_existing - | std::filesystem::copy_options::recursive - | std::filesystem::copy_options::copy_symlinks + const auto copyOptions = fs::copy_options::overwrite_existing + | fs::copy_options::recursive + | fs::copy_options::copy_symlinks ; try { - std::filesystem::copy(tempSharedPath, destSharedPath, copyOptions); - } catch(std::filesystem::filesystem_error& e) { + fs::copy(tempSharedPath, destSharedPath, copyOptions); + } catch(fs::filesystem_error& e) { LOGE("Could not copy shared files: \"", e.what(), "\""); } } @@ -1555,15 +1601,15 @@ int PluginInfoComponent::downloadPlugin(const String& plugin, const String& vers int loadPlugin = AccessClass::getPluginManager()->loadPlugin(pluginDllPath); - if (loadPlugin == -1) - return 6; - AccessClass::getProcessorList()->fillItemList(); AccessClass::getProcessorList()->repaint(); if(pInfo.type == "RecordEngine") AccessClass::getControlPanel()->updateRecordEngineList(); + if (loadPlugin == -1) + return 6; + } return 1; diff --git a/Source/UI/PluginInstaller.h b/Source/UI/PluginInstaller.h index 73a64142fb..15711f7e7c 100644 --- a/Source/UI/PluginInstaller.h +++ b/Source/UI/PluginInstaller.h @@ -144,8 +144,6 @@ class PluginInfoComponent : public Component, VER_EXISTS_ERR, XML_WRITE_ERR, LOAD_ERR, - PLUGIN_IN_USE, - RECNODE_IN_USE, HTTP_ERR }; diff --git a/Source/UI/UIComponent.cpp b/Source/UI/UIComponent.cpp index 5e33a45a74..179cc74e44 100755 --- a/Source/UI/UIComponent.cpp +++ b/Source/UI/UIComponent.cpp @@ -36,6 +36,7 @@ #include "../Processors/ProcessorGraph/ProcessorGraph.h" #include "../Audio/AudioComponent.h" #include "../MainWindow.h" +#include "../AutoUpdater.h" UIComponent::UIComponent(MainWindow* mainWindow_, ProcessorGraph* pgraph, AudioComponent* audio_) : mainWindow(mainWindow_), processorGraph(pgraph), audio(audio_), messageCenterIsCollapsed(true) @@ -476,6 +477,8 @@ PopupMenu UIComponent::getMenuForIndex(int menuIndex, const String& menuName) else if (menuIndex == 3) { menu.addCommandItem(commandManager, showHelp); + menu.addSeparator(); + menu.addCommandItem(commandManager, checkForUpdates); } return menu; @@ -518,6 +521,7 @@ void UIComponent::getAllCommands(Array & commands) setClockModeDefault, setClockModeHHMMSS, showHelp, + checkForUpdates, resizeWindow, openPluginInstaller, openDefaultConfigWindow @@ -651,6 +655,11 @@ void UIComponent::getCommandInfo(CommandID commandID, ApplicationCommandInfo& re result.setActive(true); break; + case checkForUpdates: + result.setInfo("Check for updates...", "Checks if a newer version of the GUI is available", "General", 0); + result.setActive(true); + break; + case resizeWindow: result.setInfo("Reset window bounds", "Reset window bounds", "General", 0); break; @@ -852,6 +861,12 @@ bool UIComponent::perform(const InvocationInfo& info) url.launchInDefaultBrowser(); break; } + + case checkForUpdates: + { + LatestVersionCheckerAndUpdater::getInstance()->checkForNewVersion (false, mainWindow); + break; + } case toggleProcessorList: processorList->toggleState(); diff --git a/Source/UI/UIComponent.h b/Source/UI/UIComponent.h index 7dca1d685e..c21399a52a 100755 --- a/Source/UI/UIComponent.h +++ b/Source/UI/UIComponent.h @@ -206,6 +206,7 @@ class UIComponent : public Component, setClockModeHHMMSS = 0x2112, toggleHttpServer = 0x4001, showHelp = 0x2011, + checkForUpdates = 0x2022, resizeWindow = 0x2012, reloadOnStartup = 0x2013, saveSignalChainAs = 0x2014, diff --git a/Source/Utils/OpenEphysHttpServer.h b/Source/Utils/OpenEphysHttpServer.h index 3b07d2acad..6395d77ceb 100644 --- a/Source/Utils/OpenEphysHttpServer.h +++ b/Source/Utils/OpenEphysHttpServer.h @@ -34,6 +34,7 @@ #include "../MainWindow.h" #include "../AccessClass.h" #include "../UI/ProcessorList.h" +#include "../UI/EditorViewport.h" #include "Utils.h" @@ -852,6 +853,32 @@ class OpenEphysHttpServer : juce::Thread { } + }); + + svr_->Get("/api/undo", [this](const httplib::Request& req, httplib::Response& res) { + std::string message_str; + LOGD( "Received undo request" ); + + json ret; + res.set_content(ret.dump(), "application/json"); + res.status = 400; + + const MessageManagerLock mml; + AccessClass::getEditorViewport()->undo(); + + }); + + svr_->Get("/api/redo", [this](const httplib::Request& req, httplib::Response& res) { + std::string message_str; + LOGD( "Received redo request" ); + + json ret; + res.set_content(ret.dump(), "application/json"); + res.status = 400; + + const MessageManagerLock mml; + AccessClass::getEditorViewport()->redo(); + }); LOGC("Beginning HTTP server on port ", PORT); diff --git a/Source/Utils/Utils.h b/Source/Utils/Utils.h index 8db87e44d5..d18d4c3d16 100644 --- a/Source/Utils/Utils.h +++ b/Source/Utils/Utils.h @@ -11,41 +11,41 @@ /* Log Action -- taken by user */ #define LOGA(...) \ - OELogger::instance().LOGFile("[open-ephys][action] ", __VA_ARGS__); + OELogger::GetInstance().LOGFile("[open-ephys][action] ", __VA_ARGS__); /* Log Buffer -- related logs i.e. inside process() method */ #define LOGB(...) \ - OELogger::instance().LOGFile("[open-ephys][buffer] ", __VA_ARGS__); + OELogger::GetInstance().LOGFile("[open-ephys][buffer] ", __VA_ARGS__); /* Log Console -- gets printed to the GUI Debug Console */ #define LOGC(...) \ - OELogger::instance().LOGConsole("[open-ephys] ", __VA_ARGS__); + OELogger::GetInstance().LOGConsole("[open-ephys] ", __VA_ARGS__); /* Log Debug -- gets printed to the console in debug mode, to file otherwise */ #ifdef DEBUG #define LOGD(...) \ - OELogger::instance().LOGConsole("[open-ephys][debug] ", __VA_ARGS__); + OELogger::GetInstance().LOGConsole("[open-ephys][debug] ", __VA_ARGS__); #else /* Log Debug -- gets printed to the log file */ #define LOGD(...) \ - OELogger::instance().LOGFile("[open-ephys][debug] ", __VA_ARGS__); + OELogger::GetInstance().LOGFile("[open-ephys][debug] ", __VA_ARGS__); #endif /* Log Deep Debug -- gets printed to log file (e.g. enable after a crash to get more details) */ #define LOGDD(...) \ - OELogger::instance().LOGFile("[open-ephys][ddebug] ", __VA_ARGS__); + OELogger::GetInstance().LOGFile("[open-ephys][ddebug] ", __VA_ARGS__); /* Log Error -- gets printed to console with flare */ #define LOGE(...) \ - OELogger::instance().LOGError("[open-ephys] ***ERROR*** ", __VA_ARGS__); + OELogger::GetInstance().LOGError("[open-ephys] ***ERROR*** ", __VA_ARGS__); /* Log File -- gets printed directly to main output file */ #define LOGF(...) LOGD(...) /* Log Graph -- gets logs related to processor graph generation/modification events */ #define LOGG(...) \ - OELogger::instance().LOGFile("[open-ephys][graph] ", __VA_ARGS__); + OELogger::GetInstance().LOGFile("[open-ephys][graph] ", __VA_ARGS__); /* Thread-safe logger */ class OELogger @@ -57,10 +57,10 @@ std::ofstream logFile; OELogger() { } public: - static OELogger& instance() + static OELogger& GetInstance(const std::string& log_file = "activity.log") { - static OELogger lg; - return lg; + static OELogger instance(log_file); + return instance; } OELogger(OELogger const&) = delete; @@ -100,12 +100,18 @@ std::ofstream logFile; // Each time the GUI is launched, a new error log is generated. // In case of a crash, the most recent file is appended with a datestring logFile.open(filePath, std::ios::out | std::ios::app); - time_t now = time(0); - logFile << "[open-ephys] Session start time: " << ctime(&now); } private: + OELogger(const std::string& log_file) : log_file(log_file) + { + if (!logFileExists) + createLogFile(log_file); + logFileExists = true; + } std::mutex mt; + std::string log_file; + bool logFileExists = false; };