diff --git a/devdoc/Introduction.md b/devdoc/Introduction.md index 9d5cb4c5..9267acad 100644 --- a/devdoc/Introduction.md +++ b/devdoc/Introduction.md @@ -33,6 +33,9 @@ End user documentation is located at https://www.ngscopeclient.org/manual/conten \defgroup spectrometerdrivers Spectrometer drivers \ingroup drivers +\defgroup matrixdrivers Switch matrix drivers +\ingroup drivers + \defgroup vnadrivers VNA drivers \ingroup drivers @@ -50,6 +53,9 @@ End user documentation is located at https://www.ngscopeclient.org/manual/conten \defgroup math Basic math functions \ingroup libscopeprotocols +\defgroup rf RF +\ingroup libscopeprotocols + \defgroup ngscopeclient Ngscopeclient (GUI) \defgroup dialogs Dialog boxes diff --git a/lib b/lib index 7f13107a..cb21bd2c 160000 --- a/lib +++ b/lib @@ -1 +1 @@ -Subproject commit 7f13107aaed51b1540c9595e18d9b4acce3736b6 +Subproject commit cb21bd2ce3f10488cca12fa6e31daf24b4ebbea7 diff --git a/src/ngscopeclient/FilterGraphEditor.cpp b/src/ngscopeclient/FilterGraphEditor.cpp index 7e1e1755..342bbcc6 100644 --- a/src/ngscopeclient/FilterGraphEditor.cpp +++ b/src/ngscopeclient/FilterGraphEditor.cpp @@ -494,7 +494,7 @@ bool FilterGraphEditor::DoRender() for(auto it : chans) { for(auto chan : it.second) - DoNodeForChannel(chan, it.first, multiInst); + DoNodeForChannel(chan, it.first, multiInst, 0); //TODO: acquisition time etc? } //Make a newly dragged node spawn at the mouse position @@ -518,9 +518,10 @@ bool FilterGraphEditor::DoRender() //Filters auto filters = Filter::GetAllInstances(); + auto filterperf = m_session.GetFilterGraphRuntime(); for(auto f : filters) { - DoNodeForChannel(f, nullptr, false); + DoNodeForChannel(f, nullptr, false, filterperf[f]); //Add a reference to the channel so even if we remove the last user of it this frame, it won't be deleted until we're ready f->AddRef(); @@ -1942,8 +1943,14 @@ void FilterGraphEditor::DoNodeForTrigger(Trigger* trig) TODO: this seems to fail hard if we do not have at least one input OR output on the node. Why? */ -void FilterGraphEditor::DoNodeForChannel(InstrumentChannel* channel, shared_ptr inst, bool multiInst) +void FilterGraphEditor::DoNodeForChannel( + InstrumentChannel* channel, + shared_ptr inst, + bool multiInst, + int64_t runtime) { + Unit fs(Unit::UNIT_FS); + //If the channel has no color, make it neutral gray //(this is often true for e.g. external trigger) string displaycolor = channel->m_displaycolor; @@ -2136,6 +2143,49 @@ void FilterGraphEditor::DoNodeForChannel(InstrumentChannel* channel, shared_ptr< headercolor, headerText.c_str()); + //TODO: add an option for toggling this + //TODO: add preference for colors + //Draw a bubble above the text with the runtime stats + if(runtime > 0) + { + auto runtimeText = fs.PrettyPrint(runtime); + auto runtimeSize = headerfont->CalcTextSizeA(headerfontsize, FLT_MAX, 0, runtimeText.c_str()); + + auto timebgColor = ColorFromString("#404040"); + auto timeTextColor = ColorFromString("#ffffff"); + float timespacing = 0.1 * headerheight; + float runtimeBot = pos.y - timespacing; + float bubbleHeight = runtimeSize.y + 2*ImGui::GetStyle().FramePadding.y; + + ImVec2 clockiconpos( + pos.x + ImGui::GetStyle().FramePadding.x, + runtimeBot - bubbleHeight + ImGui::GetStyle().FramePadding.y); + ImVec2 clockiconsize(runtimeSize.y, runtimeSize.y); + + ImVec2 textpos( + clockiconpos.x + clockiconsize.x + ImGui::GetStyle().ItemSpacing.x, + clockiconpos.y ); + + bgList->AddRectFilled( + ImVec2(pos.x + 1, runtimeBot - bubbleHeight), + ImVec2(textpos.x + runtimeSize.x + ImGui::GetStyle().FramePadding.y, runtimeBot), + timebgColor, + rounding, + ImDrawFlags_RoundCornersAll); + + bgList->AddImage( + m_parent->GetTextureManager()->GetTexture("time"), + clockiconpos, + clockiconpos + clockiconsize ); + + bgList->AddText( + headerfont, + headerfontsize, + textpos, + timeTextColor, + runtimeText.c_str()); + } + //Draw the force vector if(ImGui::IsKeyDown(ImGuiKey_Q)) RenderForceVector(bgList, pos, size, m_nodeForces[id]); diff --git a/src/ngscopeclient/FilterGraphEditor.h b/src/ngscopeclient/FilterGraphEditor.h index 3c7d8ab6..8c0f36fc 100644 --- a/src/ngscopeclient/FilterGraphEditor.h +++ b/src/ngscopeclient/FilterGraphEditor.h @@ -177,7 +177,11 @@ class FilterGraphEditor : public Dialog void DoInternalLinksForGroup(std::shared_ptr group); void DoNodeForGroupOutputs(std::shared_ptr group); void DoNodeForGroupInputs(std::shared_ptr group); - void DoNodeForChannel(InstrumentChannel* channel, std::shared_ptr inst, bool multiInst); + void DoNodeForChannel( + InstrumentChannel* channel, + std::shared_ptr inst, + bool multiInst, + int64_t runtime); void DoNodeForTrigger(Trigger* trig); bool HandleNodeProperties(); void HandleDoubleClicks(); diff --git a/src/ngscopeclient/InstrumentThread.cpp b/src/ngscopeclient/InstrumentThread.cpp index 81477f32..21072c83 100644 --- a/src/ngscopeclient/InstrumentThread.cpp +++ b/src/ngscopeclient/InstrumentThread.cpp @@ -65,6 +65,8 @@ void InstrumentThread(InstrumentThreadArgs args) auto bertstate = args.bertstate; auto psustate = args.psustate; + bool triggerUpToDate = false; + while(!*args.shuttingDown) { //Flush any pending commands @@ -86,6 +88,15 @@ void InstrumentThread(InstrumentThreadArgs args) { //LogTrace("Scope isn't armed, sleeping\n"); this_thread::sleep_for(chrono::milliseconds(5)); + if(!triggerUpToDate) + { // Check for trigger state change + auto stat = scope->PollTrigger(); + session->GetInstrumentConnectionState(inst)->m_lastTriggerState = stat; + if(stat == Oscilloscope::TRIGGER_MODE_STOP || stat == Oscilloscope::TRIGGER_MODE_RUN || stat == Oscilloscope::TRIGGER_MODE_TRIGGERED) + { // Final state + triggerUpToDate = true; + } + } } //Grab data if it's ready @@ -93,8 +104,10 @@ void InstrumentThread(InstrumentThreadArgs args) else { auto stat = scope->PollTrigger(); + session->GetInstrumentConnectionState(inst)->m_lastTriggerState = stat; if(stat == Oscilloscope::TRIGGER_MODE_TRIGGERED) scope->AcquireData(); + triggerUpToDate = false; } } diff --git a/src/ngscopeclient/MainWindow.cpp b/src/ngscopeclient/MainWindow.cpp index 11246d0a..4dd24686 100644 --- a/src/ngscopeclient/MainWindow.cpp +++ b/src/ngscopeclient/MainWindow.cpp @@ -1277,7 +1277,7 @@ void MainWindow::DockingArea() rightPanelID = topNode->ChildNodes[1]->ID; } else - ImGui::DockBuilderSplitNode(topNode->ID, ImGuiDir_Left, 0.1, &leftPanelID, &rightPanelID); + ImGui::DockBuilderSplitNode(topNode->ID, ImGuiDir_Left, 0.2, &leftPanelID, &rightPanelID); ImGui::DockBuilderDockWindow(m_streamBrowser->GetTitleAndID().c_str(), leftPanelID); ImGui::DockBuilderDockWindow(m_initialWorkspaceDockRequest->GetTitleAndID().c_str(), rightPanelID); @@ -1490,7 +1490,9 @@ void MainWindow::AddToRecentInstrumentList(shared_ptr inst) if(inst == nullptr) return; - LogTrace("Adding instrument \"%s\" to recent instrument list\n", inst->m_nickname.c_str()); + LogTrace("Adding instrument \"%s\" to recent instrument list (had %zu)\n", + inst->m_nickname.c_str(), m_recentInstruments.size()); + LogIndenter li; auto now = time(NULL); @@ -1499,6 +1501,7 @@ void MainWindow::AddToRecentInstrumentList(shared_ptr inst) inst->GetDriverName() + ":" + inst->GetTransportName() + ":" + inst->GetTransportConnectionString(); + LogTrace("Connection string: %s\n", connectionString.c_str()); m_recentInstruments[connectionString] = now; //Delete anything old @@ -1517,8 +1520,12 @@ void MainWindow::AddToRecentInstrumentList(shared_ptr inst) } } + LogTrace("Removing oldest instrument (%s) to make room\n", oldestPath.c_str()); m_recentInstruments.erase(oldestPath); } + + LogTrace("Added (now have %zu total recent instruments)\n", m_recentInstruments.size()); + SaveRecentInstrumentList(); } /** diff --git a/src/ngscopeclient/MainWindow_Icons.cpp b/src/ngscopeclient/MainWindow_Icons.cpp index 2ad1d465..e82ff56b 100644 --- a/src/ngscopeclient/MainWindow_Icons.cpp +++ b/src/ngscopeclient/MainWindow_Icons.cpp @@ -171,6 +171,8 @@ void MainWindow::LoadStatusBarIcons() m_texmgr.LoadTexture("mouse_move", FindDataFile("icons/contrib/blender/24x24/mouse_move.png")); m_texmgr.LoadTexture("mouse_wheel", FindDataFile("icons/contrib/blender/24x24/mouse_wheel.png")); + + m_texmgr.LoadTexture("time", FindDataFile("icons/contrib/blender/24x24/time.png")); } /** diff --git a/src/ngscopeclient/MainWindow_Menus.cpp b/src/ngscopeclient/MainWindow_Menus.cpp index e9811d88..c86b93f4 100644 --- a/src/ngscopeclient/MainWindow_Menus.cpp +++ b/src/ngscopeclient/MainWindow_Menus.cpp @@ -322,8 +322,17 @@ void MainWindow::DoAddSubMenu( for(auto cstring : cstrings) { auto fields = explode(cstring, ':'); + + //make sure it's well formed if(fields.size() < 4) - continue; + { + //Special case: null transport allows 3 fields + if( (fields.size() == 3) && (fields[2] == "null") ) + {} + + else + continue; + } auto nick = fields[0]; auto drivername = fields[1]; @@ -333,9 +342,13 @@ void MainWindow::DoAddSubMenu( { if(ImGui::MenuItem(nick.c_str())) { - auto path = fields[3]; - for(size_t j=4; j= 4) + { + path = fields[3]; + for(size_t j=4; j lock(m_lastFilterGraphRuntimeMutex); + m_lastFilterGraphRuntimeStats = m_graphExecutor.GetRunTimes(); + } } /** @@ -3291,6 +3294,10 @@ bool Session::RefreshDirtyFilters() } m_lastFilterGraphExecTime = (GetTime() - tstart) * FS_PER_SECOND; + { + lock_guard lock(m_lastFilterGraphRuntimeMutex); + m_lastFilterGraphRuntimeStats = m_graphExecutor.GetRunTimes(); + } return true; } diff --git a/src/ngscopeclient/Session.h b/src/ngscopeclient/Session.h index db08cded..ffa7d2ea 100644 --- a/src/ngscopeclient/Session.h +++ b/src/ngscopeclient/Session.h @@ -58,6 +58,7 @@ class InstrumentConnectionState m_shuttingDown = false; args.shuttingDown = &m_shuttingDown; m_thread = std::make_unique(InstrumentThread, args); + m_lastTriggerState = Oscilloscope::TRIGGER_MODE_WAIT; } ~InstrumentConnectionState() @@ -79,6 +80,9 @@ class InstrumentConnectionState ///@brief Thread for polling the instrument std::unique_ptr m_thread; + + ///@brief Cached trigger state, to reflect in the UI + Oscilloscope::TriggerMode m_lastTriggerState; }; /** @@ -132,6 +136,7 @@ class Session void AddInstrument(std::shared_ptr inst, bool createDialogs = true); void RemoveInstrument(std::shared_ptr inst); + std::shared_ptr GetInstrumentConnectionState(std::shared_ptr inst) { return m_instrumentStates[inst]; } bool IsMultiScope() { return m_multiScope; } @@ -319,6 +324,13 @@ class Session void OnMarkerChanged(); + ///@brief Return the last filter graph runtime stats + std::map GetFilterGraphRuntime() + { + std::lock_guard lock(m_lastFilterGraphRuntimeMutex); + return m_lastFilterGraphRuntimeStats; + } + protected: void UpdatePacketManagers(const std::set& nodes); @@ -440,6 +452,12 @@ class Session ///@brief Time spent on the last filter graph execution std::atomic m_lastFilterGraphExecTime; + ///@brief Mutex for controlling access to m_lastFilterGraphRuntimeStats + std::mutex m_lastFilterGraphRuntimeMutex; + + ///@brief Performance stats from last graph execution + std::map m_lastFilterGraphRuntimeStats; + ///@brief Mutex for controlling access to performance counters std::mutex m_perfClockMutex; diff --git a/src/ngscopeclient/StreamBrowserDialog.cpp b/src/ngscopeclient/StreamBrowserDialog.cpp index c82fe439..334f63cc 100644 --- a/src/ngscopeclient/StreamBrowserDialog.cpp +++ b/src/ngscopeclient/StreamBrowserDialog.cpp @@ -43,7 +43,7 @@ using namespace std; // Construction / destruction StreamBrowserDialog::StreamBrowserDialog(Session& session, MainWindow* parent) - : Dialog("Stream Browser", "Stream Browser", ImVec2(300, 400)) + : Dialog("Stream Browser", "Stream Browser", ImVec2(550, 400)) , m_session(session) , m_parent(parent) { @@ -65,20 +65,287 @@ StreamBrowserDialog::~StreamBrowserDialog() */ bool StreamBrowserDialog::DoRender() { + // Some helpers for rendering widgets that appear in the StreamBrowserDialog. + + // Render a link of the "Sample rate: 4 GSa/s" type that shows up in the + // scope properties box. + auto renderInfoLink = [](const char *label, const char *linktext, bool &clicked, bool &hovered) + { + ImGui::Text("%s: ", label); + ImGui::SameLine(0, 0); + clicked |= ImGui::TextLink(linktext); + hovered |= ImGui::IsItemHovered(); + }; + + float badgeXMin; // left edge over which we must not overrun + float badgeXCur; // right edge to render the next badge against + auto startBadgeLine = [&badgeXMin, &badgeXCur]() + { + ImGuiWindow *window = ImGui::GetCurrentWindowRead(); + // roughly, what ImGui::GetCursorPosPrevLineX would be, if it existed; convert from absolute-space to window-space + badgeXMin = (window->DC.CursorPosPrevLine - window->Pos + window->Scroll).x; + badgeXCur = ImGui::GetWindowContentRegionMax().x; + }; + auto renderBadge = [&badgeXMin, &badgeXCur](ImVec4 color, ... /* labels, ending in NULL */) + { + va_list ap; + va_start(ap, color); + + /* XXX: maybe color should be a prefs string? */ + + while (const char *label = va_arg(ap, const char *)) { + float xsz = ImGui::CalcTextSize(label).x + ImGui::GetStyle().ItemSpacing.x + ImGui::GetStyle().FramePadding.x * 2; + if ((badgeXCur - xsz) < badgeXMin) { + continue; + } + + // ok, we have enough space -- commit to it! + badgeXCur -= xsz - ImGui::GetStyle().ItemSpacing.x; + ImGui::SameLine(badgeXCur); + ImGui::PushStyleColor(ImGuiCol_Button, color); + ImGui::SmallButton(label); + ImGui::PopStyleColor(); + break; + } + }; + auto renderDownloadProgress = [this, &badgeXMin, &badgeXCur](std::shared_ptr inst, InstrumentChannel *chan, bool isLast) + { + static const char* const download[] = {"DOWNLOADING", "DOWNLOAD" ,"DL","D", NULL}; + + /* prefer language "PENDING" to "WAITING": "PENDING" implies + * that we are going to do it when we get through a list of + * other things, "WAITING" could mean that the channel is + * waiting for something else (trigger?) + */ + static const char* const pend[] = {"PENDING" , "PEND" ,"PE","P", NULL}; + + /* prefer language "COMPLETE" to "READY": "READY" implies + * that the channel might be ready to capture or something, + * but "COMPLETE" at least is not to be confused with that. + * ("DOWNLOADED" is more specific but is easy to confuse + * with "DOWNLOADING". If you can come up with a better + * mid-length abbreviation for "COMPLETE" than "DL OK" / + * "OK", give it a go, I guess.) + */ + static const char* const ready[] = {"COMPLETE" , "DL OK" ,"OK","C", NULL}; + + /* Let's use active for fast download channels to display when data is available + */ + static const char* const active[] = {"ACTIVE" , "ACTV" ,"ACT","A", NULL}; + + static const char* const* labels; + + ImVec4 color; + bool shouldRender = true; + bool hasProgress = false; + double elapsed = GetTime() - chan->GetDownloadStartTime(); + + // determine what label we should apply, and while we are at + // it, determine if this channel appears to be slow enough + // to need a progress bar + +/// @brief hysteresis threshold for a channel finishing a download faster than this to be declared fast +#define CHANNEL_DOWNLOAD_THRESHOLD_FAST_SECONDS ((double)0.2) + +/// @brief hysteresis threshold for a channel finishing a still being in progress for longer than this to be declared slow +#define CHANNEL_DOWNLOAD_THRESHOLD_SLOW_SECONDS ((double)0.4) + + switch(chan->GetDownloadState()) + { + case InstrumentChannel::DownloadState::DOWNLOAD_NONE: + if (isLast && (elapsed < CHANNEL_DOWNLOAD_THRESHOLD_FAST_SECONDS)) + m_instrumentDownloadIsSlow[inst] = false; + /* FALLTHRU */ + case InstrumentChannel::DownloadState::DOWNLOAD_UNKNOWN: + /* There is nothing to say about this -- + * either there is nothing pending at all on + * the system, or this scope doesn't know + * how to report it, and in either case, we + * don't need to render a badge about it. + */ + shouldRender = false; + break; + case InstrumentChannel::DownloadState::DOWNLOAD_WAITING: + labels = pend; + if (elapsed > CHANNEL_DOWNLOAD_THRESHOLD_SLOW_SECONDS) + m_instrumentDownloadIsSlow[inst] = true; + hasProgress = m_instrumentDownloadIsSlow[inst]; + color.x = 0.8 ; color.y=0.3 ; color.z=0.3; color.w=1.0; + break; + case InstrumentChannel::DownloadState::DOWNLOAD_IN_PROGRESS: + labels = download; + if (elapsed > CHANNEL_DOWNLOAD_THRESHOLD_SLOW_SECONDS) + m_instrumentDownloadIsSlow[inst] = true; + hasProgress = m_instrumentDownloadIsSlow[inst]; + color.x = 0.7 ; color.y=0.7 ; color.z=0.3; color.w=1.0; + break; + case InstrumentChannel::DownloadState::DOWNLOAD_FINISHED: + labels = ready; + if (isLast && (elapsed < CHANNEL_DOWNLOAD_THRESHOLD_FAST_SECONDS)) + m_instrumentDownloadIsSlow[inst] = false; + color.x = 0.3 ; color.y=0.8 ; color.z=0.3; color.w=1.0; + break; + default: + shouldRender = false; + break; + } + + // For fast channels, show a constant green badge when a + // download has started "recently" -- even if we're not + // downloading at this moment. This could be slightly + // misleading (i.e., after a channel goes into STOP mode, we + // will remain ACTIVE for up to THRESHOLD_SLOW time) but the + // period of time for which it is misleading is short! + if(!m_instrumentDownloadIsSlow[inst] && elapsed < CHANNEL_DOWNLOAD_THRESHOLD_SLOW_SECONDS) + { + labels = active; + color.x = 0.3 ; color.y=0.8 ; color.z=0.3; color.w=1.0; + shouldRender = true; + hasProgress = false; + } + + if (!shouldRender) + return; + +/// @brief Width used to display progress bars (e.g. download progress bar) +#define PROGRESS_BAR_WIDTH 80 + + // try first adding a bar, and if there isn't enough room + // for a bar, skip it and try just putting a label + for (int withoutBar = 0; withoutBar < 2; withoutBar++) + { + if (withoutBar) + hasProgress = false; + + for (int i = 0; labels[i]; i++) + { + const char *label = labels[i]; + + float xsz = ImGui::CalcTextSize(label).x + (hasProgress ? (ImGui::GetStyle().ItemSpacing.x + PROGRESS_BAR_WIDTH) : 0) + ImGui::GetStyle().FramePadding.x * 2 + ImGui::GetStyle().ItemSpacing.x; + if ((badgeXCur - xsz) < badgeXMin) + continue; + + // ok, we have enough space -- commit to it! + badgeXCur -= xsz - ImGui::GetStyle().ItemSpacing.x; + ImGui::SameLine(badgeXCur); + ImGui::PushStyleColor(ImGuiCol_Button, color); + ImGui::SmallButton(label); + ImGui::PopStyleColor(); + if(hasProgress) + { + ImGui::SameLine(); + ImGui::ProgressBar(chan->GetDownloadProgress(), ImVec2(PROGRESS_BAR_WIDTH, ImGui::GetFontSize())); + } + + return; + } + } + + // well, shoot -- I guess there wasn't enough room to do *anything* useful! + }; + //Add all instruments auto insts = m_session.GetInstruments(); for(auto inst : insts) { - if(ImGui::TreeNodeEx(inst->m_nickname.c_str(), ImGuiTreeNodeFlags_DefaultOpen)) + bool instIsOpen = ImGui::TreeNodeEx(inst->m_nickname.c_str(), ImGuiTreeNodeFlags_DefaultOpen); + startBadgeLine(); + + auto state = m_session.GetInstrumentConnectionState(inst); + + // Render ornaments for this instrument: offline, trigger status, ... + auto scope = std::dynamic_pointer_cast(inst); + if (scope) { + if (scope->IsOffline()) + renderBadge(ImVec4(0.8, 0.3, 0.3, 1.0) /* XXX: pull color from prefs */, "OFFLINE", "OFFL", NULL); + else + { + Oscilloscope::TriggerMode mode = state ? state->m_lastTriggerState : Oscilloscope::TRIGGER_MODE_STOP; + switch (mode) { + case Oscilloscope::TRIGGER_MODE_RUN: + /* prefer language "ARMED" to "RUN": + * "RUN" could mean either "waiting + * for trigger" or "currently + * capturing samples post-trigger", + * "ARMED" is unambiguous */ + renderBadge(ImVec4(0.3, 0.8, 0.3, 1.0), "ARMED", "A", NULL); + break; + case Oscilloscope::TRIGGER_MODE_STOP: + renderBadge(ImVec4(0.8, 0.3, 0.3, 1.0), "STOPPED", "STOP", "S", NULL); + break; + case Oscilloscope::TRIGGER_MODE_TRIGGERED: + renderBadge(ImVec4(0.7, 0.7, 0.3, 1.0), "TRIGGERED", "TRIG'D", "T'D", "T", NULL); + break; + case Oscilloscope::TRIGGER_MODE_WAIT: + /* prefer language "BUSY" to "WAIT": + * "WAIT" could mean "waiting for + * trigger", "BUSY" means "I am + * doing something internally and am + * not ready for some reason" */ + renderBadge(ImVec4(0.8, 0.3, 0.3, 1.0), "BUSY", "B", NULL); + break; + case Oscilloscope::TRIGGER_MODE_AUTO: + renderBadge(ImVec4(0.3, 0.8, 0.3, 1.0), "AUTO", "A", NULL); + break; + default: + break; + } + } + } + + if(instIsOpen) { - for(size_t i=0; iGetChannelCount(); i++) + size_t lastEnabledChannelIndex = 0; + size_t channelCount = inst->GetChannelCount(); + if (scope) + { + ImGui::BeginChild("sample_params", ImVec2(0, 0), ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_Border); + + auto srate_txt = Unit(Unit::UNIT_SAMPLERATE).PrettyPrint(scope->GetSampleRate()); + auto sdepth_txt = Unit(Unit::UNIT_SAMPLEDEPTH).PrettyPrint(scope->GetSampleDepth()); + + bool clicked = false; + bool hovered = false; + renderInfoLink("Sample rate", srate_txt.c_str(), clicked, hovered); + renderInfoLink("Sample depth", sdepth_txt.c_str(), clicked, hovered); + if (clicked) + m_parent->ShowTimebaseProperties(); + if (hovered) + m_parent->AddStatusHelp("mouse_lmb", "Open timebase properties"); + for(size_t i = 0; iIsChannelEnabled(i)) + { + lastEnabledChannelIndex = i; + } + } + + ImGui::EndChild(); + } + + for(size_t i=0; iGetChannel(i); - bool open = ImGui::TreeNodeEx(chan->GetDisplayName().c_str(), ImGuiTreeNodeFlags_DefaultOpen); + bool singleStream = chan->GetStreamCount() == 1; + auto scopechan = dynamic_cast(chan); + bool renderScopeProps = false; + bool isDigital = false; + if (scopechan) + { + renderScopeProps = scopechan->IsEnabled(); + isDigital = scopechan->GetType(0) == Stream::STREAM_TYPE_DIGITAL; + } + + bool hasChildren = !singleStream || renderScopeProps; + + if (chan->m_displaycolor != "") + ImGui::PushStyleColor(ImGuiCol_Text, ColorFromString(chan->m_displaycolor)); + bool open = ImGui::TreeNodeEx(chan->GetDisplayName().c_str(), isDigital ? 0 : ImGuiTreeNodeFlags_DefaultOpen | (!hasChildren ? ImGuiTreeNodeFlags_Leaf : 0)); + if (chan->m_displaycolor != "") + ImGui::PopStyleColor(); //Single stream: drag the stream not the channel - bool singleStream = chan->GetStreamCount() == 1; if(singleStream) { StreamDescriptor s(chan, 0); @@ -105,25 +372,84 @@ bool StreamBrowserDialog::DoRender() ImGui::EndDragDropSource(); } - if(open && !singleStream) + // Channel decoration + startBadgeLine(); + if (scopechan && !scopechan->IsEnabled()) + { + renderBadge(ImVec4(0.4, 0.4, 0.4, 1.0) /* XXX: pull color from prefs */, "disabled", "disa", NULL); + } + else + { + renderDownloadProgress(inst, chan, (i == lastEnabledChannelIndex)); + } + + if(open) { for(size_t j=0; jGetStreamCount(); j++) { - ImGui::Selectable(chan->GetStreamName(j).c_str()); + ImGui::PushID(j); - StreamDescriptor s(chan, j); - if(ImGui::BeginDragDropSource()) + if (!singleStream) { - if(s.GetType() == Stream::STREAM_TYPE_ANALOG_SCALAR) - ImGui::SetDragDropPayload("Scalar", &s, sizeof(s)); + ImGui::Selectable(chan->GetStreamName(j).c_str()); + + StreamDescriptor s(chan, j); + if(ImGui::BeginDragDropSource()) + { + if(s.GetType() == Stream::STREAM_TYPE_ANALOG_SCALAR) + ImGui::SetDragDropPayload("Scalar", &s, sizeof(s)); + else + ImGui::SetDragDropPayload("Stream", &s, sizeof(s)); + + ImGui::TextUnformatted(s.GetName().c_str()); + ImGui::EndDragDropSource(); + } + else + DoItemHelp(); + } + // Channel/stram properties + if (renderScopeProps && scopechan) + { + ImGui::BeginChild("scope_params", ImVec2(0, 0), + ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_Border); + + if(isDigital) + { + auto threshold_txt = Unit(Unit::UNIT_VOLTS).PrettyPrint(scope->GetDigitalThreshold(i)); + + bool clicked = false; + bool hovered = false; + renderInfoLink("Threshold", threshold_txt.c_str(), clicked, hovered); + if (clicked) + { + /* XXX: refactor to be more like FilterGraphEditor::HandleNodeProperties? */ + m_parent->ShowChannelProperties(scopechan); + } + if (hovered) + m_parent->AddStatusHelp("mouse_lmb", "Open channel properties"); + } else - ImGui::SetDragDropPayload("Stream", &s, sizeof(s)); + { + auto offset_txt = Unit(Unit::UNIT_VOLTS).PrettyPrint(scopechan->GetOffset(j)); + auto range_txt = Unit(Unit::UNIT_VOLTS).PrettyPrint(scopechan->GetVoltageRange(j)); + + bool clicked = false; + bool hovered = false; + renderInfoLink("Offset", offset_txt.c_str(), clicked, hovered); + renderInfoLink("Voltage range", range_txt.c_str(), clicked, hovered); + if (clicked) + { + /* XXX: refactor to be more like FilterGraphEditor::HandleNodeProperties? */ + m_parent->ShowChannelProperties(scopechan); + } + if (hovered) + m_parent->AddStatusHelp("mouse_lmb", "Open channel properties"); + } - ImGui::TextUnformatted(s.GetName().c_str()); - ImGui::EndDragDropSource(); + ImGui::EndChild(); } - else - DoItemHelp(); + + ImGui::PopID(); } } diff --git a/src/ngscopeclient/StreamBrowserDialog.h b/src/ngscopeclient/StreamBrowserDialog.h index 6af3897e..dc497518 100644 --- a/src/ngscopeclient/StreamBrowserDialog.h +++ b/src/ngscopeclient/StreamBrowserDialog.h @@ -35,6 +35,8 @@ #ifndef StreamBrowserDialog_h #define StreamBrowserDialog_h +#include + #include "Dialog.h" #include "Session.h" @@ -53,6 +55,8 @@ class StreamBrowserDialog : public Dialog Session& m_session; MainWindow* m_parent; + + std::map, bool> m_instrumentDownloadIsSlow; }; #endif diff --git a/src/ngscopeclient/icons/contrib/blender/24x24/time.png b/src/ngscopeclient/icons/contrib/blender/24x24/time.png new file mode 100644 index 00000000..0192afda Binary files /dev/null and b/src/ngscopeclient/icons/contrib/blender/24x24/time.png differ diff --git a/src/ngscopeclient/icons/contrib/blender/scalable/time.svg b/src/ngscopeclient/icons/contrib/blender/scalable/time.svg new file mode 100644 index 00000000..7c2f62f6 --- /dev/null +++ b/src/ngscopeclient/icons/contrib/blender/scalable/time.svg @@ -0,0 +1,5 @@ + + + + +