diff --git a/lib b/lib index 9ca18eb3..d8178048 160000 --- a/lib +++ b/lib @@ -1 +1 @@ -Subproject commit 9ca18eb322ab5859134b4875ecc8857d608527c8 +Subproject commit d817804827a4db2843f25d3489d44ffa7afb28cf diff --git a/src/ngscopeclient/Dialog.cpp b/src/ngscopeclient/Dialog.cpp index 4c51d510..89bd83dc 100644 --- a/src/ngscopeclient/Dialog.cpp +++ b/src/ngscopeclient/Dialog.cpp @@ -424,3 +424,280 @@ bool Dialog::UnitInputWithExplicitApply( ImGui::EndGroup(); return changed; } + +/** + @brief Segment on/off state for each of the 10 digits + "L" (needed for OL / Overload) + 0b01000000 : Top h segment + 0b00100000 : Top right v seglent + 0b00010000 : Bottom right v segment + 0b00001000 : Bottom h segment + 0b00000100 : Bottom left v segment + 0b00000010 : Top left v segment + 0b00000001 : Center h segment + */ +static char SEGMENTS[] = +{ + 0x7E, // 0 + 0x30, // 1 + 0x6D, // 2 + 0x79, // 3 + 0x33, // 4 + 0x5B, // 5 + 0x5F, // 6 + 0x70, // 7 + 0x7F, // 8 + 0x7B, // 9 + 0x0E, // L +}; + +/** + @brief Render a single digit in 7 segment display style + + @param drawList the drawList used for rendering + @param digit the digit to render + @param size the size of the digit + @param position the position of the digit + @param thickness the thickness of a segment + @param colorOn the color for an "on" segment + @param colorOff the color for an "off" segment + */ +void Dialog::Render7SegmentDigit(ImDrawList* drawList, uint8_t digit, ImVec2 size, ImVec2 position, float thickness, ImU32 colorOn, ImU32 colorOff) +{ + // Inspired by https://github.com/ocornut/imgui/issues/3606#issuecomment-736855952 + if(digit > 10) + digit = 10; // 10 is for L of OL (Overload) + size.y += thickness; + ImVec2 halfSize(size.x/2,size.y/2); + ImVec2 centerPosition(position.x+halfSize.x,position.y+halfSize.y); + float w = thickness; + float h = thickness/2; + float segmentSpec[7][4] = + { + {-1, -1, h, h}, // Top h segment + { 1, -1, -h, h}, // Top right v seglent + { 1, 0, -h, -h}, // Bottom right v segment + {-1, 1, h, -w * 1.5f},// Bottom h segment + {-1, 0, h, -h}, // Bottom left v segment + {-1, -1, h, h}, // Top left v segment + {-1, 0, h, -h}, // Center h segment + }; + for(int i = 0; i < 7; i++) + { + ImVec2 topLeft, bottomRight; + if(i % 3 == 0) + { + // Horizontal segment + topLeft = ImVec2(centerPosition.x + segmentSpec[i][0] * halfSize.x + segmentSpec[i][2], centerPosition.y + segmentSpec[i][1] * halfSize.y + segmentSpec[i][3] - h); + bottomRight = ImVec2(topLeft.x + size.x - w, topLeft.y + w); + } + else + { + // Vertical segment + topLeft = ImVec2(centerPosition.x + segmentSpec[i][0] * halfSize.x + segmentSpec[i][2] - h, centerPosition.y + segmentSpec[i][1] * halfSize.y + segmentSpec[i][3]); + bottomRight = ImVec2(topLeft.x + w, topLeft.y + halfSize.y - w); + } + ImVec2 segmentSize = bottomRight - topLeft; + float space = w * 0.6; + float u = space - h; + if(segmentSize.x > segmentSize.y) + { + // Horizontal segment + ImVec2 points[] = + { + {topLeft.x + u, topLeft.y + segmentSize.y * .5f}, + {topLeft.x + space, topLeft.y}, + {bottomRight.x - space, topLeft.y}, + {bottomRight.x - u, topLeft.y + segmentSize.y * .5f}, + {bottomRight.x - space, bottomRight.y}, + {topLeft.x + space, bottomRight.y} + }; + drawList->AddConvexPolyFilled(points, 6, (SEGMENTS[digit] >> (6 - i)) & 1 ? colorOn : colorOff); + } + else + { + // Vertical segment + ImVec2 points[] = { + {topLeft.x + segmentSize.x * .5f, topLeft.y + u}, + {bottomRight.x, topLeft.y + space}, + {bottomRight.x, bottomRight.y - space}, + {bottomRight.x - segmentSize.x * .5f, bottomRight.y - u}, + {topLeft.x, bottomRight.y - space}, + {topLeft.x, topLeft.y + space}}; + drawList->AddConvexPolyFilled(points, 6, (SEGMENTS[digit] >> (6 - i)) & 1 ? colorOn : colorOff); + } + } +} + +// @brief ratio between unit font size and digit size +#define UNIT_SCALE 0.80f + +// @brief ratio between digit width and height +#define DIGIT_WIDTH_RATIO 0.50f + +/** + @brief Render a numeric value with a 7 segment display style + + @param value the string representation of the value to display (may include the unit) + @param color the color to use + @param digitHeight the height of a digit + */ +void Dialog::Render7SegmentValue(const std::string& value, ImVec4 color, float digitHeight) +{ + bool ignoredClicked, ignoredHovered; + Render7SegmentValue(value,color,digitHeight,ignoredClicked,ignoredHovered,false); +} + +/** + @brief Render a numeric value with a 7 segment display style + + @param value the string representation of the value to display (may include the unit) + @param color the color to use + @param digitHeight the height of a digit + @param clicked output value for clicked state + @param hovered output value for hovered state + @param clickable true (default) if the displayed value should be clickable + */ +void Dialog::Render7SegmentValue(const std::string& value, ImVec4 color, float digitHeight, bool &clicked, bool &hovered, bool clickable) +{ + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + + // Compute digit width according th height + float digitWidth = digitHeight*DIGIT_WIDTH_RATIO; + + // Compute front and back color + float bgmul = 0.15; + auto bcolor = ImGui::ColorConvertFloat4ToU32(ImVec4(color.x*bgmul, color.y*bgmul, color.z*bgmul, color.w)); + auto fcolor = ImGui::ColorConvertFloat4ToU32(color); + + + // Parse value string to get integer and fractional part + unit + bool inIntPart = true; + bool inFractPart = false; + vector intPart; + vector fractPart; + string unit; + + if(value == UNIT_OVERLOAD_LABEL) + { + // Overload + intPart.push_back(0); + intPart.push_back(10); // 10 is for L + unit = "Inf."; + } + else + { + // Iterate on each char of the value string + for(const char c : value) + { + if(c >= '0' && c <='9') + { + // This is a numeric digit + if(inIntPart) + intPart.push_back((uint8_t)(c-'0')); + else if(inFractPart) + fractPart.push_back((uint8_t)(c-'0')); + else + unit += c; + } + else if(c == '.' || c == std::use_facet >(std::locale()).decimal_point() || c == ',') + { + // This is the decimal separator + if(inIntPart) + { + inFractPart = true; + inIntPart = false; + } + else + LogWarning("Unexpected decimal separator '%c' in value '%s'.\n",c,value.c_str()); + } + else if(isspace(c) || c == std::use_facet< std::numpunct >(std::locale()).thousands_sep()) + { + // We ingore spaces (except in unit part) + if(inIntPart || inFractPart) {} // Ignore + else + unit += c; + } + else // Anything else + { + // This is the unit + inFractPart = false; + inIntPart = false; + unit += c; + } + } + // Trim the unit string + unit = Trim(unit); + + // Fill fractional part with 2 zeros if it's empty + if(fractPart.empty()) + { + fractPart.push_back(0); + fractPart.push_back(0); + } + } + + // Segment thickness + float thickness = digitHeight/10; + + // Space between digits + float spacing = 0.08 * digitWidth; + + // Size of decimal separator + float dotSize = 2*thickness; + + // Size of unit font and unit text + float unitSize = digitHeight*UNIT_SCALE; + float unitTextWidth = ImGui::GetFont()->CalcTextSizeA(unitSize,FLT_MAX, 0.0f,unit.c_str()).x; + + ImVec2 size(digitWidth*(intPart.size()+fractPart.size())+dotSize+2*spacing+unitTextWidth+thickness, digitHeight); + + if(clickable) + { + bgmul = 0.0f; + ImVec4 buttonColor = ImVec4(color.x*bgmul, color.y*bgmul, color.z*bgmul, 0); + bgmul = 0.2f; + ImVec4 buttonColorHovered = ImVec4(color.x*bgmul, color.y*bgmul, color.z*bgmul, color.w); + bgmul = 0.3f; + ImVec4 buttonColorActive = ImVec4(color.x*bgmul, color.y*bgmul, color.z*bgmul, color.w); + ImGui::PushStyleColor(ImGuiCol_Button, buttonColor); + ImGui::PushStyleColor(ImGuiCol_ButtonHovered, buttonColorHovered); + ImGui::PushStyleColor(ImGuiCol_ButtonActive, buttonColorActive); + clicked |= ImGui::Button(" ",size); + hovered |= ImGui::IsItemHovered(); + ImGui::PopStyleColor(3); + if(hovered) + ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + } + else + ImGui::InvisibleButton("seven", size, ImGuiButtonFlags_EnableNav); + + ImVec2 position = ImGui::GetItemRectMin(); + + // Actual digit width (without space) + float digitActualWidth = digitWidth - spacing; + // Current x position + float x = 0; + + // Integer part + for(size_t i = 0; i < intPart.size(); i++) + { + Render7SegmentDigit(draw_list, intPart[i], ImVec2(digitActualWidth, digitHeight), ImVec2(position.x + x, position.y),thickness,fcolor,bcolor); + x += digitWidth; + } + // Decimal separator + x+= spacing; + draw_list->AddCircleFilled(ImVec2(position.x+x+dotSize/2-spacing/2,position.y+digitHeight-dotSize/2),dotSize/2,fcolor); + x+= dotSize; + x+= spacing; + // Factional part + for(size_t i = 0; i < fractPart.size(); i++) + { + Render7SegmentDigit(draw_list, fractPart[i], ImVec2(digitActualWidth, digitHeight), ImVec2(position.x + x, position.y),thickness,fcolor,bcolor); + x += digitWidth; + } + // Unit + draw_list->AddText(NULL,unitSize, + ImVec2(position.x + x + thickness, position.y), + fcolor, + unit.c_str()); +} \ No newline at end of file diff --git a/src/ngscopeclient/Dialog.h b/src/ngscopeclient/Dialog.h index 43bd4957..5590448d 100644 --- a/src/ngscopeclient/Dialog.h +++ b/src/ngscopeclient/Dialog.h @@ -96,6 +96,11 @@ class Dialog void RenderErrorPopup(); void ShowErrorPopup(const std::string& title, const std::string& msg); + void Render7SegmentDigit(ImDrawList* drawList, uint8_t digit, ImVec2 size, ImVec2 position, float thikness, ImU32 colorOn, ImU32 colorOff); + void Render7SegmentValue(const std::string& value, ImVec4 color, float digitHeight); + void Render7SegmentValue(const std::string& value, ImVec4 color, float digitHeight, bool &clicked, bool &hovered, bool clickable = true); + + bool m_open; std::string m_id; std::string m_title; diff --git a/src/ngscopeclient/PreferenceSchema.cpp b/src/ngscopeclient/PreferenceSchema.cpp index ea4ef9f2..5cd22095 100644 --- a/src/ngscopeclient/PreferenceSchema.cpp +++ b/src/ngscopeclient/PreferenceSchema.cpp @@ -165,6 +165,10 @@ void PreferenceManager::InitializeDefaults() .Description("Color for icon captions")); auto& stream = appearance.AddCategory("Stream Browser"); + stream.AddPreference( + Preference::Bool("use_7_segment_display", true) + .Label("Use 7 segment style display") + .Description("Use 7 segment style display for DMM and PSU values")); stream.AddPreference( Preference::Real("instrument_badge_latch_duration", 0.4) .Label("Intrument badge latch duration (seconds)") @@ -241,6 +245,10 @@ void PreferenceManager::InitializeDefaults() Preference::Color("psu_meas_label_color", ColorFromString("#00C100")) .Label("PSU measured label color") .Description("Color for PSU 'meas.' label")); + stream.AddPreference( + Preference::Color("psu_7_segment_color", ColorFromString("#B2FFFF")) + .Label("PSU 7 segment display color") + .Description("Color for PSU 7 segment style display")); stream.AddPreference( Preference::Color("awg_hiz_badge_color", ColorFromString("#666600")) .Label("Function Generator HI-Z badge color") diff --git a/src/ngscopeclient/StreamBrowserDialog.cpp b/src/ngscopeclient/StreamBrowserDialog.cpp index 8110eafa..a4abaeb7 100644 --- a/src/ngscopeclient/StreamBrowserDialog.cpp +++ b/src/ngscopeclient/StreamBrowserDialog.cpp @@ -279,12 +279,18 @@ bool StreamBrowserDialog::renderCombo(ImVec4 color,int *selected, ... /* values, @param color the color of the toggle button @param curValue the value of the toggle button + @param valueOff label for value off (optionnal, defaults to "OFF") + @param valueOn label for value on (optionnal, defaults to "ON") + @param cropTextTo if >0 crop the combo text up to this number of characters to have it fit the available space (optionnal, defaults to 0) @return the selected value for the toggle button */ -bool StreamBrowserDialog::renderToggle(ImVec4 color, bool curValue) +bool StreamBrowserDialog::renderToggle(ImVec4 color, bool curValue, const char* valueOff, const char* valueOn, uint8_t cropTextTo) { int selection = (int)curValue; - renderCombo(color, &selection, "OFF", "ON", NULL); + std::vector values; + values.push_back(string(valueOff)); + values.push_back(string(valueOn)); + renderCombo(color, selection, values, false, cropTextTo); return (selection == 1); } @@ -292,13 +298,16 @@ bool StreamBrowserDialog::renderToggle(ImVec4 color, bool curValue) @brief Render an on/off toggle button combo @param curValue the value of the toggle button + @param valueOff label for value off (optionnal, defaults to "OFF") + @param valueOn label for value on (optionnal, defaults to "ON") + @param cropTextTo if >0 crop the combo text up to this number of characters to have it fit the available space (optionnal, defaults to 0) @return the selected value for the toggle button */ -bool StreamBrowserDialog::renderOnOffToggle(bool curValue) +bool StreamBrowserDialog::renderOnOffToggle(bool curValue, const char* valueOff, const char* valueOn, uint8_t cropTextTo) { auto& prefs = m_session.GetPreferences(); ImVec4 color = ImGui::ColorConvertU32ToFloat4((curValue ? prefs.GetColor("Appearance.Stream Browser.instrument_on_badge_color") : prefs.GetColor("Appearance.Stream Browser.instrument_off_badge_color"))); - return renderToggle(color, curValue); + return renderToggle(color, curValue, valueOff, valueOn, cropTextTo); } /** @@ -481,8 +490,17 @@ void StreamBrowserDialog::renderPsuRows(bool isVoltage, bool cc, PowerSupplyChan ImGui::PopID(); ImGui::TableSetColumnIndex(2); ImGui::PushID(isVoltage ? "sV" : "sC"); - clicked |= ImGui::TextLink(setValue); - hovered |= ImGui::IsItemHovered(); + + float height = ImGui::GetFontSize(); + ImVec4 color = ImGui::ColorConvertU32ToFloat4(prefs.GetColor("Appearance.Stream Browser.psu_7_segment_color")); + + if(prefs.GetBool("Appearance.Stream Browser.use_7_segment_display")) + Render7SegmentValue(setValue,color,height,clicked,hovered); + else + { + clicked |= ImGui::TextLink(setValue); + hovered |= ImGui::IsItemHovered(); + } ImGui::PopID(); // Row 2 ImGui::TableNextRow(); @@ -511,8 +529,14 @@ void StreamBrowserDialog::renderPsuRows(bool isVoltage, bool cc, PowerSupplyChan ImGui::PopID(); ImGui::TableSetColumnIndex(2); ImGui::PushID(isVoltage ? "mV" : "mC"); - clicked |= ImGui::TextLink(measuredValue); - hovered |= ImGui::IsItemHovered(); + + if(prefs.GetBool("Appearance.Stream Browser.use_7_segment_display")) + Render7SegmentValue(measuredValue, color,height,clicked,hovered); + else + { + clicked |= ImGui::TextLink(measuredValue); + hovered |= ImGui::IsItemHovered(); + } ImGui::PopID(); } @@ -629,6 +653,114 @@ void StreamBrowserDialog::renderAwgProperties(std::shared_ptr ImGui::PopID(); } +/** + @brief Render DMM channel properties + + @param dmm the DMM to render channel properties for + @param dmmchan the DMM channel to render properties for + @param clicked output param for clicked state + @param hovered output param for hovered state + */ +void StreamBrowserDialog::renderDmmProperties(std::shared_ptr dmm, MultimeterChannel* dmmchan, bool isMain, bool &clicked, bool &hovered) +{ + auto& prefs = m_session.GetPreferences(); + size_t streamIndex = isMain ? 0 : 1; + Unit unit = dmmchan->GetYAxisUnits(streamIndex); + float mainValue = dmmchan->GetScalarValue(streamIndex); + string valueText = unit.PrettyPrint(mainValue,dmm->GetMeterDigits()); + ImVec4 color = ImGui::ColorConvertU32ToFloat4(ColorFromString(dmmchan->m_displaycolor)); + string streamName = isMain ? "Main" : "Secondary"; + + ImGui::PushID(streamName.c_str()); + + // Get available operating and current modes + auto modemask = isMain ? dmm->GetMeasurementTypes() : dmm->GetSecondaryMeasurementTypes(); + auto curMode = isMain ? dmm->GetMeterMode() : dmm->GetSecondaryMeterMode(); + + // Stream name + bool open = ImGui::TreeNodeEx(streamName.c_str(), (curMode > 0) ? ImGuiTreeNodeFlags_DefaultOpen : 0); + + // Mode combo + startBadgeLine(); + ImGui::PushID(streamName.c_str()); + vector modeNames; + vector modes; + if(!isMain) + { + // Add None for secondary measurement to be able to disable it + modeNames.push_back("None"); + modes.push_back(Multimeter::MeasurementTypes::NONE); + } + int modeSelector = 0; + for(unsigned int i=0; i<32; i++) + { + auto mode = static_cast(1 << i); + if(modemask & mode) + { + modes.push_back(mode); + modeNames.push_back(dmm->ModeToText(mode)); + if(curMode == mode) + modeSelector = modes.size() - 1; + } + } + + if(renderCombo(color, modeSelector, modeNames,true,3)) + { + curMode = modes[modeSelector]; + if(isMain) + dmm->SetMeterMode(curMode); + else + { + dmm->SetSecondaryMeterMode(curMode); + // Open or close tree node according the secondary measure mode + ImGuiContext& g = *GImGui; + ImGui::TreeNodeSetOpen(g.LastItemData.ID,(curMode > 0)); + } + } + ImGui::PopID(); + + StreamDescriptor s(dmmchan, streamIndex); + 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(); + + if(open) + ImGui::TreePop(); + + if(open) + { + if(prefs.GetBool("Appearance.Stream Browser.use_7_segment_display")) + Render7SegmentValue(valueText, color,ImGui::GetFontSize()*2,clicked,hovered); + else + { + clicked |= ImGui::TextLink(valueText.c_str()); + hovered |= ImGui::IsItemHovered(); + } + if(isMain) + { + ImGui::PushID("autorange"); + // For main, also show the autorange combo + startBadgeLine(); + bool autorange = dmm->GetMeterAutoRange(); + bool result = renderOnOffToggle(autorange,"Manual Range","Autorange",3); + if(result != autorange) + dmm->SetMeterAutoRange(result); + ImGui::PopID(); + } + } + + ImGui::PopID(); +} + /** @brief Rendering of an instrument node @@ -811,11 +943,13 @@ void StreamBrowserDialog::renderChannelNode(shared_ptr instrument, s auto psu = std::dynamic_pointer_cast(instrument); auto scope = std::dynamic_pointer_cast(instrument); auto awg = std::dynamic_pointer_cast(instrument); + auto dmm = std::dynamic_pointer_cast(instrument); bool singleStream = channel->GetStreamCount() == 1; auto scopechan = dynamic_cast(channel); auto psuchan = dynamic_cast(channel); auto awgchan = dynamic_cast(channel); + auto dmmchan = dynamic_cast(channel); bool renderProps = false; bool isDigital = false; if (scopechan) @@ -980,6 +1114,26 @@ void StreamBrowserDialog::renderChannelNode(shared_ptr instrument, s m_parent->AddStatusHelp("mouse_lmb", "Open Function Generator properties"); ImGui::EndChild(); } + else if(dmm && dmmchan) + { + ImGui::BeginChild("dmm_params", ImVec2(0, 0), + ImGuiChildFlags_AutoResizeY | ImGuiChildFlags_Border); + // Always 2 streams for dmm channel => render properties on channel node + bool clicked = false; + bool hovered = false; + // Main measurement + renderDmmProperties(dmm,dmmchan,true,clicked,hovered); + // Secondary measurement + renderDmmProperties(dmm,dmmchan,false,clicked,hovered); + if (clicked) + { + m_parent->ShowInstrumentProperties(dmm); + } + if (hovered) + m_parent->AddStatusHelp("mouse_lmb", "Open Multimeter properties"); + + ImGui::EndChild(); + } else { size_t streamCount = channel->GetStreamCount(); @@ -1196,7 +1350,6 @@ bool StreamBrowserDialog::DoRender() } ImGui::TreePop(); } - return true; } diff --git a/src/ngscopeclient/StreamBrowserDialog.h b/src/ngscopeclient/StreamBrowserDialog.h index 9d112751..44a79da2 100644 --- a/src/ngscopeclient/StreamBrowserDialog.h +++ b/src/ngscopeclient/StreamBrowserDialog.h @@ -74,11 +74,12 @@ class StreamBrowserDialog : public Dialog bool renderInstrumentBadge(std::shared_ptr inst, bool latched, InstrumentBadge badge); bool renderCombo(ImVec4 color,int &selected, const std::vector &values, bool useColorForText = false, uint8_t cropTextTo = 0); bool renderCombo(ImVec4 color,int* selected, ... /* values, ending in NULL */); - bool renderToggle(ImVec4 color, bool curValue); - bool renderOnOffToggle(bool curValue); + bool renderToggle(ImVec4 color, bool curValue, const char* valueOff = "OFF", const char* valueOn = "ON", uint8_t cropTextTo = 0); + bool renderOnOffToggle(bool curValue, const char* valueOff = "OFF", const char* valueOn = "ON", uint8_t cropTextTo = 0); void renderDownloadProgress(std::shared_ptr inst, InstrumentChannel *chan, bool isLast); void renderPsuRows(bool isVoltage, bool cc, PowerSupplyChannel* chan,const char *setValue, const char *measuredValue, bool &clicked, bool &hovered); void renderAwgProperties(std::shared_ptr awg, FunctionGeneratorChannel* awgchan, bool &clicked, bool &hovered); + void renderDmmProperties(std::shared_ptr dmm, MultimeterChannel* dmmchan, bool isMain, bool &clicked, bool &hovered); // Rendering of an instrument node void renderInstrumentNode(shared_ptr instrument);