Files
IQ.Pilot/iqpilot/tools/cabana/ui/widgets/signalview.cc
2026-09-02 16:46:45 -05:00

976 lines
37 KiB
C++

#include "tools/cabana/ui/widgets/signalview.h"
#include <algorithm>
#include <cfloat>
#include <cmath>
#include <cstdio>
#include <future>
#include "tools/cabana/commands.h"
#include "tools/cabana/settings.h"
#include "tools/cabana/ui/dialogs/messagebox.h"
#include "tools/cabana/ui/util.h"
#include "tools/cabana/ui/threadpool.h"
#include "tools/cabana/utils/strings.h"
#include "tools/cabana/utils/util.h"
#include "tools/cabana/ui/icons.h"
namespace {
constexpr float INDENTATION = 20.0f;
constexpr float H_MARGIN = 3.0f;
constexpr float V_MARGIN = 2.0f;
constexpr float SIGNAL_ROW_EXTRA = 5.0f;
constexpr float SIGNAL_ROW_SCALE = 1.25f;
constexpr float FILTER_WIDTH = 160.0f;
constexpr float SPARKLINE_SLIDER_WIDTH = 120.0f;
constexpr float COLLAPSE_ICON_SIZE = 12.0f;
constexpr int SPARKLINE_RANGE_MAX = 30;
constexpr float LABEL_FONT = 12.0f;
constexpr float MINMAX_FONT = 10.0f;
constexpr int COLOR_LABEL_WIDTH = 18;
std::string signalTypeToString(cabana::Signal::Type type) {
if (type == cabana::Signal::Type::Multiplexor) return "Multiplexor Signal";
else if (type == cabana::Signal::Type::Multiplexed) return "Multiplexed Signal";
else return "Normal Signal";
}
std::string multiplexIndicator(const cabana::Signal *sig) {
return sig->type == cabana::Signal::Type::Multiplexor ? std::string(" M ") : " m" + std::to_string(sig->multiplex_value) + " ";
}
std::string nameText(const SignalModel::Item *item) {
return item->type == SignalModel::Item::Sig ? item->sig->name : item->title;
}
float rowHeight() {
return ImGui::GetFrameHeight();
}
float signalRowHeight() {
return std::floor((ImGui::GetFrameHeight() + SIGNAL_ROW_EXTRA) * SIGNAL_ROW_SCALE);
}
bool valueDescriptionEditor(int column, std::string *text) {
ImGui::PushID(column);
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f);
validatedInput("##edit", text, column == 0 ? doubleValidator : nullptr);
ImGui::PopStyleVar();
const bool clicked = ImGui::IsItemActivated() || ImGui::IsItemClicked();
ImGui::PopID();
return clicked;
}
}
SignalModel::SignalModel() : root_(new Item) {
connections_.push_back(dbc()->fileChanged.connect([this]() { refresh(); }));
connections_.push_back(dbc()->msgUpdated.connect([this](MessageId id) { handleMsgChanged(id); }));
connections_.push_back(dbc()->msgRemoved.connect([this](MessageId id) { handleMsgChanged(id); }));
connections_.push_back(dbc()->signalAdded.connect([this](MessageId id, const cabana::Signal *sig) { handleSignalAdded(id, sig); }));
connections_.push_back(dbc()->signalUpdated.connect([this](const cabana::Signal *sig) { handleSignalUpdated(sig); }));
connections_.push_back(dbc()->signalRemoved.connect([this](const cabana::Signal *sig) { handleSignalRemoved(sig); }));
}
void SignalModel::insertItem(SignalModel::Item *root_item, int pos, const cabana::Signal *sig) {
Item *parent_item = new Item{.type = Item::Sig, .parent = root_item, .sig = sig, .title = sig->name};
root_item->children.insert(root_item->children.begin() + pos, parent_item);
std::string titles[]{"Name", "Size", "Receiver Nodes", "Little Endian", "Signed", "Offset", "Factor", "Type",
"Multiplex Value", "Extra Info", "Unit", "Comment", "Minimum Value", "Maximum Value", "Value Table"};
for (int i = 0; i < std::size(titles); ++i) {
auto item = new Item{.type = (Item::Type)(i + Item::Name), .parent = parent_item, .sig = sig, .title = titles[i]};
parent_item->children.push_back(item);
if (item->type == Item::ExtraInfo) {
parent_item = item;
}
}
}
void SignalModel::setMessage(const MessageId &id) {
msg_id_ = id;
filter_str_ = "";
refresh();
}
void SignalModel::setFilter(const std::string &txt) {
filter_str_ = txt;
refresh();
}
void SignalModel::refresh() {
root_.reset(new SignalModel::Item);
if (auto msg = dbc()->msg(msg_id_)) {
for (auto s : msg->getSignals()) {
if (filter_str_.empty() || utils::containsCI(s->name, filter_str_)) {
insertItem(root_.get(), root_->children.size(), s);
}
}
}
modelReset();
rowsChanged();
}
bool SignalModel::isEnabled(const Item *item) {
return !(item->type == Item::MultiplexValue && item->sig->type != cabana::Signal::Type::Multiplexed);
}
bool SignalModel::isCheckable(const Item *item) {
return item->type == Item::Endian || item->type == Item::Signed;
}
bool SignalModel::isEditable(const Item *item) {
return item->children.empty() && !isCheckable(item);
}
int SignalModel::signalRow(const cabana::Signal *sig) const {
for (int i = 0; i < root_->children.size(); ++i) {
if (root_->children[i]->sig == sig) return i;
}
return -1;
}
std::string SignalModel::valueText(const Item *item) const {
switch (item->type) {
case Item::Sig: return item->sig_val;
case Item::Name: return item->sig->name;
case Item::Size: return std::to_string(item->sig->size);
case Item::Node: return item->sig->receiver_name;
case Item::SignalType: return signalTypeToString(item->sig->type);
case Item::MultiplexValue: return std::to_string(item->sig->multiplex_value);
case Item::Offset: return doubleToString(item->sig->offset);
case Item::Factor: return doubleToString(item->sig->factor);
case Item::Unit: return item->sig->unit;
case Item::Comment: return item->sig->comment;
case Item::Min: return doubleToString(item->sig->min);
case Item::Max: return doubleToString(item->sig->max);
case Item::Desc: {
std::string val_desc;
for (auto &[val, desc] : item->sig->val_desc) {
if (!val_desc.empty()) val_desc += " ";
val_desc += utils::toString(val) + " \"" + desc + "\"";
}
return val_desc;
}
default: return {};
}
}
bool SignalModel::setData(Item *item, const ItemValue &value) {
cabana::Signal s = *item->sig;
switch (item->type) {
case Item::Name: s.name = value.toString(); break;
case Item::Size: s.size = value.toInt(); break;
case Item::Node: s.receiver_name = utils::trimmed(value.toString()); break;
case Item::SignalType: s.type = (cabana::Signal::Type)value.toInt(); break;
case Item::MultiplexValue: s.multiplex_value = value.toInt(); break;
case Item::Endian: s.is_little_endian = value.toBool(); break;
case Item::Signed: s.is_signed = value.toBool(); break;
case Item::Offset: s.offset = value.toDouble(); break;
case Item::Factor: s.factor = value.toDouble(); break;
case Item::Unit: s.unit = value.toString(); break;
case Item::Comment: s.comment = value.toString(); break;
case Item::Min: s.min = value.toDouble(); break;
case Item::Max: s.max = value.toDouble(); break;
case Item::Desc: s.val_desc = value.toValueDescription(); break;
default: return false;
}
return saveSignal(item->sig, s);
}
bool SignalModel::saveSignal(const cabana::Signal *origin_s, cabana::Signal &s) {
auto msg = dbc()->msg(msg_id_);
if (s.name != origin_s->name && msg->sig(s.name) != nullptr) {
std::string text = "There is already a signal with the same name '" + s.name + "'";
MessageBox::warning("Failed to save signal", text);
return false;
}
if (s.is_little_endian != origin_s->is_little_endian) {
s.start_bit = flipBitPos(s.start_bit);
}
UndoStack::instance()->push(new EditSignalCommand(msg_id_, origin_s, s));
return true;
}
void SignalModel::handleMsgChanged(MessageId id) {
if (id.address == msg_id_.address) {
refresh();
}
}
void SignalModel::handleSignalAdded(MessageId id, const cabana::Signal *sig) {
if (id == msg_id_) {
if (filter_str_.empty()) {
int i = dbc()->msg(msg_id_)->indexOf(sig);
insertItem(root_.get(), i, sig);
rowsChanged();
} else if (utils::containsCI(sig->name, filter_str_)) {
refresh();
}
}
}
void SignalModel::handleSignalUpdated(const cabana::Signal *sig) {
if (int row = signalRow(sig); row != -1) {
if (filter_str_.empty()) {
int to = dbc()->msg(msg_id_)->indexOf(sig);
if (to != row) {
auto item = root_->children[row];
root_->children.erase(root_->children.begin() + row);
root_->children.insert(root_->children.begin() + to, item);
}
}
}
}
void SignalModel::handleSignalRemoved(const cabana::Signal *sig) {
if (int row = signalRow(sig); row != -1) {
delete root_->children[row];
root_->children.erase(root_->children.begin() + row);
rowsChanged();
}
}
float SignalView::textWidth(const std::string &text, float font_size) {
ImFont *font = ImGui::GetFont();
if (!font || ImGui::GetFontSize() <= 0) return 0;
return font->CalcTextSizeA(font_size > 0 ? font_size : ImGui::GetFontSize(), FLT_MAX, 0.0f, text.c_str()).x;
}
float SignalView::nameColumnWidth(const SignalModel::Item *item, float widget_width, const std::string &text) const {
float spacing = INDENTATION + COLOR_LABEL_WIDTH + 8;
std::string txt = text;
if (item->type == SignalModel::Item::Sig && item->sig->type != cabana::Signal::Type::Normal) {
txt += multiplexIndicator(item->sig);
spacing += H_MARGIN * 2;
}
return std::min<float>(widget_width / 3.0, textWidth(txt) + spacing);
}
void SignalView::paintCell(ImDrawList *painter, const ImRect &option_rect, const SignalModel::Item *item, int column,
bool selected, const std::string &text, float viewport_x) const {
const float h_margin = H_MARGIN;
const float v_margin = V_MARGIN;
ImRect rect(option_rect.Min.x + h_margin, option_rect.Min.y + v_margin, option_rect.Max.x - h_margin, option_rect.Max.y - v_margin);
const ImU32 text_color = selected ? highlightedTextColor() : ImGui::GetColorU32(ImGuiCol_Text);
if (column == 0) {
if (item->type == SignalModel::Item::Sig) {
ImRect icon_rect(rect.Min.x, rect.Min.y, rect.Min.x + COLOR_LABEL_WIDTH, rect.Max.y);
painter->AddRectFilled(icon_rect.Min, icon_rect.Max, toImU32(item->sig->color.darker(item->highlight ? 125 : 0)), 3.0f);
drawText(painter, icon_rect, std::to_string(item->row() + 1).c_str(), item->highlight ? IM_COL32_WHITE : IM_COL32_BLACK,
nullptr, LABEL_FONT);
rect.Min.x = icon_rect.Max.x + h_margin * 2;
if (item->sig->type != cabana::Signal::Type::Normal) {
const std::string indicator = multiplexIndicator(item->sig);
ImRect indicator_rect(rect.Min.x, rect.Min.y, rect.Min.x + ImGui::CalcTextSize(indicator.c_str()).x, rect.Max.y);
painter->AddRectFilled(indicator_rect.Min, indicator_rect.Max, IM_COL32(160, 160, 164, 255), 3.0f);
drawElidedText(painter, indicator_rect, indicator, IM_COL32_WHITE, false);
rect.Min.x = indicator_rect.Max.x + h_margin * 2;
}
} else {
rect.Min.x = viewport_x + INDENTATION + COLOR_LABEL_WIDTH + h_margin * 3;
}
if (rect.GetWidth() > 0) drawElidedText(painter, rect, text, text_color, false);
} else if (column == 1) {
if (!item->sparkline.isEmpty()) {
const ImVec2 sparkline_size = item->sparkline.size;
item->sparkline.draw(painter, rect.Min);
rect.Min.x += sparkline_size.x + 1;
float value_adjust = 10;
if (item->highlight || selected) {
painter->AddLine(rect.Min, ImVec2(rect.Min.x, rect.Max.y), text_color);
rect.Min.x += 5;
rect.Min.y -= v_margin;
rect.Max.y += v_margin;
std::string min = utils::toString(item->sparkline.min_val);
std::string max = utils::toString(item->sparkline.max_val);
drawText(painter, rect, max.c_str(), text_color, nullptr, MINMAX_FONT, ImVec2(0.0f, 0.0f));
drawText(painter, rect, min.c_str(), text_color, nullptr, MINMAX_FONT, ImVec2(0.0f, 1.0f));
value_adjust = std::max(textWidth(min, MINMAX_FONT), textWidth(max, MINMAX_FONT)) + 5;
} else if (item->sig->type == cabana::Signal::Type::Multiplexed) {
char freq[64];
snprintf(freq, sizeof(freq), "%.2g hz", item->sparkline.freq());
ImRect freq_rect(rect.Min.x + 5, rect.Min.y, rect.Max.x, rect.Max.y);
drawText(painter, freq_rect, freq, text_color, nullptr, LABEL_FONT, ImVec2(0.0f, 0.5f));
value_adjust = textWidth(freq, LABEL_FONT) + 10;
}
rect.Min.x += value_adjust;
rect.Max.x -= button_size_.x;
if (rect.GetWidth() > 0) drawElidedText(painter, rect, text, text_color, true);
} else {
rect.Max.x -= button_size_.x;
if (rect.GetWidth() > 0) drawElidedText(painter, rect, text, text_color, true);
}
}
}
void SignalView::drawEditor(SignalModel::Item *item) {
const bool take_focus = focus_item_ == item;
if (take_focus) focus_item_ = nullptr;
if (item->type == SignalModel::Item::Name || item->type == SignalModel::Item::Node || item->type == SignalModel::Item::Offset ||
item->type == SignalModel::Item::Factor || item->type == SignalModel::Item::MultiplexValue ||
item->type == SignalModel::Item::Min || item->type == SignalModel::Item::Max) {
ImGuiInputTextCallback validator = nullptr;
if (item->type == SignalModel::Item::Name) validator = nameValidator;
else if (item->type == SignalModel::Item::Node) validator = nodeValidator;
else validator = doubleValidator;
drawLineEditor(item, validator, take_focus);
} else if (item->type == SignalModel::Item::Size) {
int v = item->sig->size;
if (take_focus) ImGui::SetKeyboardFocusHere();
bool changed = ImGui::InputInt("##editor", &v, 1, 100, ImGuiInputTextFlags_AutoSelectAll);
if (ImGui::IsItemDeactivated() && ImGui::IsKeyPressed(ImGuiKey_Escape, false)) {
open_item_ = nullptr;
return;
}
if (ImGui::IsItemDeactivatedAfterEdit() || (changed && !ImGui::IsItemActive())) {
queueCommit(item, std::clamp(v, 1, CAN_MAX_DATA_BYTES));
}
if (ImGui::IsItemDeactivated() && (!ImGui::IsItemHovered() || ImGui::IsKeyPressed(ImGuiKey_Enter, false) ||
ImGui::IsKeyPressed(ImGuiKey_KeypadEnter, false) || ImGui::IsKeyPressed(ImGuiKey_Escape, false))) {
open_item_ = nullptr;
}
} else if (item->type == SignalModel::Item::SignalType) {
if (combo_focused_ && (ImGui::IsKeyPressed(ImGuiKey_Escape, false) || ImGui::IsKeyPressed(ImGuiKey_Enter, false) ||
ImGui::IsKeyPressed(ImGuiKey_KeypadEnter, false))) {
open_item_ = nullptr;
combo_focused_ = false;
return;
}
std::vector<std::pair<std::string, int>> items;
items.emplace_back(signalTypeToString(cabana::Signal::Type::Normal), (int)cabana::Signal::Type::Normal);
if (!dbc()->msg(model_.msgId())->multiplexor) {
items.emplace_back(signalTypeToString(cabana::Signal::Type::Multiplexor), (int)cabana::Signal::Type::Multiplexor);
} else if (item->sig->type != cabana::Signal::Type::Multiplexor) {
items.emplace_back(signalTypeToString(cabana::Signal::Type::Multiplexed), (int)cabana::Signal::Type::Multiplexed);
}
std::vector<const char *> names;
int current = -1;
for (int i = 0; i < items.size(); ++i) {
names.push_back(items[i].first.c_str());
if (items[i].second == (int)item->sig->type) current = i;
}
const ImGuiID popup_id = ImHashStr("##ComboPopup", 0, ImGui::GetID("##editor"));
if (take_focus) ImGui::SetKeyboardFocusHere();
if (ImGui::Combo("##editor", &current, names.data(), names.size())) {
queueCommit(item, items[current].second);
open_item_ = nullptr;
}
combo_focused_ = ImGui::IsItemFocused() || ImGui::IsPopupOpen(popup_id, ImGuiPopupFlags_None);
if (!take_focus && !combo_focused_) open_item_ = nullptr;
} else if (item->type == SignalModel::Item::Desc) {
ImGui::PushStyleColor(ImGuiCol_Header, (ImU32)0);
const bool clicked = ImGui::Selectable("##editor", false, 0, ImVec2(0, rowHeight()));
ImGui::PopStyleColor();
drawElidedText(ImGui::GetWindowDrawList(), ImRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax()), model_.valueText(item),
highlightedTextColor(), false);
if (clicked || take_focus) {
desc_dlg_ = std::make_unique<ValueDescriptionDlg>(item->sig->val_desc);
desc_dlg_->title = item->sig->name;
desc_sig_ = item->sig;
}
} else {
drawLineEditor(item, nullptr, take_focus);
}
}
void SignalView::commitEditor() {
SignalModel::Item *item = editing_item_;
std::string text = edit_text_;
closeEditor();
if (item && validateEditor(item, text) == ValidState::Acceptable) {
queueCommit(item, text);
}
}
void SignalView::closeEditor() {
editing_item_ = open_item_ = focus_item_ = nullptr;
editor_active_ = refocus_editor_ = enter_pressed_ = combo_focused_ = false;
pending_commit_ = nullptr;
}
ValidState SignalView::validateEditor(const SignalModel::Item *item, std::string &text) {
if (item->type == SignalModel::Item::Name) return validateName(text);
if (item->type == SignalModel::Item::Node) return validateNodes(text);
if (item->type == SignalModel::Item::Offset || item->type == SignalModel::Item::Factor ||
item->type == SignalModel::Item::MultiplexValue || item->type == SignalModel::Item::Min ||
item->type == SignalModel::Item::Max) {
return validateDouble(text);
}
return ValidState::Acceptable;
}
void SignalView::drawLineEditor(SignalModel::Item *item, ImGuiInputTextCallback validator, bool take_focus) {
const bool editing = editing_item_ == item;
const bool was_active = editing && editor_active_;
std::string text = editing ? edit_text_ : model_.valueText(item);
if (take_focus) ImGui::SetKeyboardFocusHere();
if (editing && refocus_editor_) {
ImGui::SetKeyboardFocusHere();
refocus_editor_ = false;
}
validatedInput("##editor", &text, validator, "", ImGuiInputTextFlags_AutoSelectAll);
if (ImGui::IsItemActivated()) editing_item_ = item;
if (editing_item_ != item) return;
edit_text_ = text;
editor_active_ = ImGui::IsItemActive();
if (was_active) {
if (ImGui::IsKeyPressed(ImGuiKey_Escape, false)) {
editing_item_ = open_item_ = nullptr;
enter_pressed_ = false;
return;
}
if (ImGui::IsKeyPressed(ImGuiKey_Enter, false) || ImGui::IsKeyPressed(ImGuiKey_KeypadEnter, false)) {
enter_pressed_ = true;
}
}
if (ImGui::IsItemDeactivated()) {
const bool by_enter = std::exchange(enter_pressed_, false);
if (!ImGui::IsItemDeactivatedAfterEdit()) {
editing_item_ = open_item_ = nullptr;
} else if (validateEditor(item, edit_text_) == ValidState::Acceptable) {
queueCommit(item, edit_text_);
editing_item_ = open_item_ = nullptr;
} else if (by_enter) {
refocus_editor_ = true;
} else {
editing_item_ = open_item_ = nullptr;
}
}
}
void SignalView::queueCommit(SignalModel::Item *item, const ItemValue &value) {
pending_commit_ = [this, item, value]() { model_.setData(item, value); };
}
void SignalView::drawValueDescriptionDlg() {
if (!desc_dlg_) return;
if (desc_dlg_->draw()) return;
if (desc_dlg_->accepted) {
for (auto sig_item : model_.root()->children) {
if (sig_item->sig != desc_sig_) continue;
for (auto child : sig_item->children) {
if (child->type != SignalModel::Item::ExtraInfo) continue;
for (auto extra : child->children) {
if (extra->type == SignalModel::Item::Desc) queueCommit(extra, desc_dlg_->val_desc);
}
}
}
}
desc_dlg_.reset();
desc_sig_ = nullptr;
}
SignalView::SignalView(ChartsWidget *charts) : charts_(charts) {
settings.sparkline_range = std::clamp(settings.sparkline_range, 1, SPARKLINE_RANGE_MAX);
button_size_ = ImVec2(22 * 2 + TOOLBAR_ITEM_SPACING, 22);
updateToolBar();
connections_.push_back(model_.rowsChanged.connect([this]() { rowsChanged(); }));
connections_.push_back(model_.modelReset.connect([this]() {
closeEditor();
if (first_visible_row_ != -1) {
last_visible_row_ = std::min(model_.rowCount() - 1, last_visible_row_ - first_visible_row_);
first_visible_row_ = 0;
}
}));
connections_.push_back(dbc()->signalAdded.connect([this](MessageId id, const cabana::Signal *sig) { handleSignalAdded(id, sig); }));
connections_.push_back(dbc()->signalUpdated.connect([this](const cabana::Signal *sig) { handleSignalUpdated(sig); }));
connections_.push_back(dbc()->signalRemoved.connect([this](const cabana::Signal *sig) {
if (desc_sig_ == sig) desc_sig_ = nullptr;
if ((editing_item_ && editing_item_->sig == sig) || (open_item_ && open_item_->sig == sig) ||
(focus_item_ && focus_item_->sig == sig)) closeEditor();
handleSignalRemoved(sig);
}));
connections_.push_back(dbc()->fileChanged.connect([this]() {
desc_sig_ = nullptr;
closeEditor();
handleSignalRemoved(nullptr);
}));
connections_.push_back(can->msgsReceived.connect([this](const std::set<MessageId> *msgs, bool) { updateState(msgs); }));
}
std::string SignalView::whatsThis() const {
return R"(
<b>Signal view</b><br />
)";
}
void SignalView::setMessage(const MessageId &id) {
filter_edit_.clear();
model_.setMessage(id);
}
void SignalView::rowsChanged() {
updateToolBar();
updateChartState();
updateState();
}
void SignalView::rowClicked(SignalModel::Item *item) {
if (item->type == SignalModel::Item::Sig || item->type == SignalModel::Item::ExtraInfo) {
item->expanded = !item->expanded;
}
}
void SignalView::selectSignal(const cabana::Signal *sig, bool expand) {
if (int row = model_.signalRow(sig); row != -1) {
auto item = model_.root()->children[row];
if (expand) {
item->expanded = !item->expanded;
}
scroll_to_sig_ = sig;
current_sig_ = sig;
current_type_ = SignalModel::Item::Sig;
}
}
void SignalView::updateChartState() {
for (auto item : model_.root()->children) {
item->chart_opened = charts_->hasSignal(model_.msgId(), item->sig);
}
}
void SignalView::signalHovered(const cabana::Signal *sig) {
auto &children = model_.root()->children;
for (int i = 0; i < children.size(); ++i) {
children[i]->highlight = children[i]->sig == sig;
}
}
void SignalView::updateToolBar() {
signal_count_lb_ = "Signals: " + std::to_string(model_.rowCount());
sparkline_label_ = utils::formatSeconds(settings.sparkline_range);
}
void SignalView::setSparklineRange(int value) {
settings.sparkline_range = value;
updateToolBar();
updateState();
}
void SignalView::handleSignalAdded(MessageId id, const cabana::Signal *sig) {
if (id.address == model_.msgId().address) {
selectSignal(sig);
}
}
void SignalView::handleSignalUpdated(const cabana::Signal *sig) {
if (int row = model_.signalRow(sig); row != -1)
updateState();
}
void SignalView::handleSignalRemoved(const cabana::Signal *sig) {
if (!sig || current_sig_ == sig) {
current_sig_ = nullptr;
auto &children = model_.root()->children;
if (sig && !children.empty() && current_row_ >= 0) {
current_sig_ = children[std::min<int>(current_row_, children.size() - 1)]->sig;
current_type_ = SignalModel::Item::Sig;
}
}
if (!sig || scroll_to_sig_ == sig) scroll_to_sig_ = nullptr;
if (!sig || hovered_sig_ == sig) hovered_sig_ = nullptr;
}
float SignalView::widestValueWidth(const cabana::Signal *sig) {
const double raw_max = sig->is_signed ? std::ldexp(1.0, sig->size - 1) - 1 : std::ldexp(1.0, sig->size) - 1;
const double raw_min = sig->is_signed ? -std::ldexp(1.0, sig->size - 1) : 0.0;
float width = 0;
for (double raw : {raw_min, raw_max}) {
width = std::max(width, textWidth(sig->formatValue(raw * sig->factor + sig->offset)));
}
for (const auto &[_, desc] : sig->val_desc) {
width = std::max(width, textWidth(desc));
}
return width;
}
void SignalView::updateState(const std::set<MessageId> *msgs) {
const auto &last_msg = can->lastMessage(model_.msgId());
if (model_.rowCount() == 0 || (msgs && !msgs->count(model_.msgId())) || last_msg.dat.size() == 0) return;
float max_value_width = 0;
for (auto item : model_.root()->children) {
double value = 0;
if (item->sig->getValue(last_msg.dat.data(), last_msg.dat.size(), &value)) {
item->sig_val = item->sig->formatValue(value);
}
max_value_width = std::max(max_value_width, widestValueWidth(item->sig));
}
if (first_visible_row_ != -1 && last_visible_row_ != -1 && last_visible_row_ < model_.rowCount()) {
const float min_max_width = textWidth("-000.00", MINMAX_FONT) + 5;
float available_width = value_column_width_ - button_size_.x;
float value_width = std::min<float>(max_value_width + min_max_width, available_width / 2);
ImVec2 size(std::floor(available_width - value_width),
std::floor(signalRowHeight() - V_MARGIN * 2));
const double window_end = can->currentSec();
const double lead_in = settings.sparkline_range * 0.05;
const auto range = can->eventsInRange(model_.msgId(), std::make_pair(window_end - settings.sparkline_range - lead_in, window_end));
const CanEventIter first = range.first, last = range.second;
std::vector<std::future<void>> futures;
for (int i = first_visible_row_; i <= last_visible_row_; ++i) {
auto item = model_.root()->children[i];
futures.push_back(ThreadPool::instance().run([item, first, last, size, window_end]() {
item->sparkline.update(item->sig, first, last, settings.sparkline_range, size, window_end);
}));
}
for (auto &f : futures) f.get();
}
}
float SignalView::toolBarRightWidth(const std::string &range_label) {
const ImGuiStyle &style = ImGui::GetStyle();
return ImGui::CalcTextSize(range_label.c_str()).x + style.ItemSpacing.x + SPARKLINE_SLIDER_WIDTH + style.ItemSpacing.x +
ImGui::GetFont()->CalcTextSizeA(COLLAPSE_ICON_SIZE, FLT_MAX, 0.0f, icon::DASH_SQUARE).x + style.FramePadding.x * 2;
}
float SignalView::minimumWidth() {
const ImGuiStyle &style = ImGui::GetStyle();
const float left_width = ImGui::CalcTextSize("Signals: 000").x + style.ItemSpacing.x + FILTER_WIDTH;
return left_width + style.ItemSpacing.x + toolBarRightWidth("00:00") + (style.WindowPadding.x + style.ChildBorderSize) * 2;
}
void SignalView::draw() {
if (!ImGui::BeginChild("SignalView", ImVec2(0, 0), ImGuiChildFlags_Borders)) {
ImGui::EndChild();
return;
}
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(signal_count_lb_.c_str());
ImGui::SameLine();
ImGui::SetNextItemWidth(FILTER_WIDTH);
if (clearableInput("##filter_edit", &filter_edit_, "Filter Signal", nonWhitespaceValidator)) {
model_.setFilter(filter_edit_);
}
alignRight(toolBarRightWidth(sparkline_label_));
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(sparkline_label_.c_str());
ImGui::SameLine();
int range = settings.sparkline_range;
if (fusionSliderInt("##sparkline_range_slider", &range, 1, SPARKLINE_RANGE_MAX, SPARKLINE_SLIDER_WIDTH)) {
setSparklineRange(range);
}
ImGui::SetItemTooltip("Sparkline time range");
ImGui::SameLine();
ImGui::PushFont(ImGui::GetFont(), COLLAPSE_ICON_SIZE);
const bool collapse = toolButton("collapse_all", icon::DASH_SQUARE, "Collapse All");
ImGui::PopFont();
if (collapse) collapseAll();
drawTree();
drawValueDescriptionDlg();
if (pending_commit_) std::exchange(pending_commit_, nullptr)();
if (pending_action_) std::exchange(pending_action_, nullptr)();
current_row_ = model_.signalRow(current_sig_);
ImGui::EndChild();
}
void SignalView::collapseAll() {
commitEditor();
for (auto item : model_.root()->children) {
item->expanded = false;
for (auto child : item->children) child->expanded = false;
}
}
void SignalView::drawTree() {
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(ImGui::GetStyle().ItemSpacing.x, 0.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
const float min_height = std::max(ImGui::GetContentRegionAvail().y, 300.0f);
const bool visible = ImGui::BeginChild("tree", ImVec2(0, min_height), ImGuiChildFlags_None);
ImGui::PopStyleVar();
if (visible) {
DrawContext ctx{ImGui::GetWindowDrawList(), ImGui::GetCursorScreenPos().x, ImGui::GetContentRegionAvail().x, rowHeight()};
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) editor_open_on_press_ = open_item_ != nullptr;
int first_visible = -1, last_visible = -1;
auto &children = model_.root()->children;
for (int i = 0; i < children.size(); ++i) {
ctx.any_visible = false;
const bool header_visible = drawItem(children[i], 0, ctx);
if (header_visible && first_visible == -1) first_visible = i;
if (ctx.any_visible) last_visible = i;
}
if (first_visible == -1 && last_visible != -1) last_visible = -1;
bool changed = first_visible != first_visible_row_ || last_visible != last_visible_row_;
first_visible_row_ = first_visible;
last_visible_row_ = last_visible;
scroll_to_sig_ = nullptr;
if (ctx.name_width > 0) name_column_width_ = ctx.name_width;
if (ctx.value_column_width > 0 && ctx.value_column_width != value_column_width_) {
value_column_width_ = ctx.value_column_width;
changed = true;
}
if (changed) updateState();
if (!ctx.mouse_on_row && ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
current_sig_ = nullptr;
current_type_ = SignalModel::Item::Root;
}
if (ctx.hovered_sig != hovered_sig_) {
hovered_sig_ = ctx.hovered_sig;
highlight(hovered_sig_);
}
}
ImGui::EndChild();
ImGui::PopStyleVar();
}
bool SignalView::drawItem(SignalModel::Item *item, int depth, DrawContext &ctx) {
const bool selected = item->sig == current_sig_ && item->type == current_type_;
const float row_height = item->type == SignalModel::Item::Sig ? signalRowHeight() : ctx.row_height;
const ImVec2 row_min = ImGui::GetCursorScreenPos();
const ImVec2 row_max(row_min.x + ctx.width, row_min.y + row_height);
const bool row_visible = ImGui::IsRectVisible(row_min, row_max);
ctx.any_visible |= row_visible;
ImGui::PushID(item);
ImGui::BeginDisabled(!SignalModel::isEnabled(item));
const bool row_clicked = viewSelectable("##row", selected, ImGuiSelectableFlags_AllowOverlap, ImVec2(0, row_height));
const float branch_x = row_min.x + depth * INDENTATION;
const bool on_branch = !item->children.empty() && ImGui::GetMousePos().x >= branch_x &&
ImGui::GetMousePos().x < branch_x + INDENTATION;
if (row_clicked && on_branch) {
item->expanded = !item->expanded;
} else if (row_clicked) {
current_sig_ = item->sig;
current_type_ = item->type;
closeEditor();
if (SignalModel::isEditable(item) && ImGui::GetMousePos().x >= row_min.x + name_column_width_) {
focus_item_ = open_item_ = item;
}
rowClicked(item);
}
if (item->type == SignalModel::Item::Sig && item->sig == scroll_to_sig_) {
ImGui::SetScrollHereY(0.0f);
scroll_to_sig_ = nullptr;
}
if (ImGui::IsMouseHoveringRect(row_min, row_max)) {
ctx.mouse_on_row = true;
if (ImGui::IsWindowHovered()) ctx.hovered_sig = item->sig;
}
if (!item->children.empty()) {
const float arrow_size = ImGui::GetFontSize() * 0.7f;
ImGui::RenderArrow(ctx.draw_list, ImVec2(row_min.x + depth * INDENTATION + 4.0f, row_min.y + (row_height - arrow_size) * 0.5f),
ImGui::GetColorU32(ImGuiCol_Text), item->expanded ? ImGuiDir_Down : ImGuiDir_Right, 0.7f);
}
const std::string text0 = nameText(item);
ctx.name_width = std::max(ctx.name_width, nameColumnWidth(item, ctx.width, text0));
const ImRect rect1(ImVec2(row_min.x + name_column_width_, row_min.y), row_max);
ctx.value_column_width = rect1.GetWidth();
const bool editor_open = selected && open_item_ == item;
if (row_visible || editor_open) {
const ImRect rect0(ImVec2(row_min.x + (depth + 1) * INDENTATION, row_min.y), ImVec2(row_min.x + name_column_width_, row_max.y));
paintCell(ctx.draw_list, rect0, item, 0, selected, text0, ctx.viewport_x);
if (item->type == SignalModel::Item::Sig && ImGui::IsMouseHoveringRect(ImVec2(row_min.x, row_min.y), rect0.Max) &&
ImGui::IsItemHovered(ImGuiHoveredFlags_ForTooltip) && ImGui::BeginTooltip()) {
ImGui::TextUnformatted(utils::stripHtml(utils::signalToolTip(item->sig)).c_str());
ImGui::EndTooltip();
}
if (item->type == SignalModel::Item::Sig) {
paintCell(ctx.draw_list, rect1, item, 1, selected, item->sig_val, ctx.viewport_x);
drawIndexWidget(item, rect1);
} else if (SignalModel::isCheckable(item)) {
bool checked = item->type == SignalModel::Item::Endian ? item->sig->is_little_endian : item->sig->is_signed;
ImGui::SetCursorScreenPos(ImVec2(rect1.Min.x + H_MARGIN, rect1.Min.y));
if (checkBox("##check", &checked)) queueCommit(item, checked);
} else if (SignalModel::isEditable(item) && editor_open) {
ImGui::SetCursorScreenPos(rect1.Min);
ImGui::SetNextItemWidth(rect1.GetWidth());
drawEditor(item);
} else {
paintCell(ctx.draw_list, rect1, item, 1, selected, model_.valueText(item), ctx.viewport_x);
}
}
ImGui::EndDisabled();
ImGui::PopID();
ImGui::SetCursorScreenPos(ImVec2(row_min.x, row_max.y));
if (item->expanded) {
for (auto child : item->children) drawItem(child, depth + 1, ctx);
}
return row_visible;
}
void SignalView::drawIndexWidget(SignalModel::Item *item, const ImRect &rect) {
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(3.0f, 2.0f));
const ImVec2 btn_size(ImGui::CalcTextSize(icon::GRAPH_UP).x + 6.0f, ImGui::GetFrameHeight());
const ImVec2 size(btn_size.x * 2 + TOOLBAR_ITEM_SPACING, btn_size.y);
ImGui::SetCursorScreenPos(ImVec2(rect.Max.x - size.x, rect.Min.y + (rect.GetHeight() - size.y) * 0.5f));
const auto sig = item->sig;
const bool checked = item->chart_opened;
if (checked) ImGui::PushStyleColor(ImGuiCol_Button, ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive));
if (ImGui::Button((std::string(icon::GRAPH_UP) + "##plot").c_str(), btn_size) && !editor_open_on_press_) {
item->chart_opened = !checked;
showChart(model_.msgId(), sig, item->chart_opened, ImGui::GetIO().KeyShift);
}
if (checked) ImGui::PopStyleColor();
ImGui::SetItemTooltip("%s", checked ? "Close Plot" : "Show Plot\nSHIFT click to add to previous opened plot");
ImGui::SameLine(0.0f, TOOLBAR_ITEM_SPACING);
if (ImGui::Button((std::string(icon::X) + "##remove").c_str(), btn_size) && !editor_open_on_press_) {
pending_action_ = [this, sig]() { UndoStack::instance()->push(new RemoveSigCommand(model_.msgId(), sig)); };
}
ImGui::SetItemTooltip("Remove signal");
ImGui::PopStyleVar();
button_size_ = size;
}
ValueDescriptionDlg::ValueDescriptionDlg(const ValueDescription &descriptions) {
for (auto &[val, desc] : descriptions) {
table_.emplace_back(utils::toString(val), desc);
}
}
bool ValueDescriptionDlg::draw() {
const std::string popup_id = title + "###ValueDescriptionDlg";
if (!opened_) {
ImGui::OpenPopup(popup_id.c_str());
opened_ = true;
}
setNextDialogWindow(ImVec2(500.0f, 0.0f));
bool open = true;
if (!ImGui::BeginPopupModal(popup_id.c_str(), &open, ImGuiWindowFlags_NoSavedSettings)) return ImGui::IsPopupOpen(popup_id.c_str());
bool closing = false;
if (ImGui::Button(icon::PLUS)) {
table_.emplace_back("", "");
}
ImGui::SameLine();
ImGui::BeginDisabled(current_row_ == -1);
if (ImGui::Button(icon::DASH) && current_row_ < table_.size()) {
table_.erase(table_.begin() + current_row_);
current_row_ = -1;
}
ImGui::EndDisabled();
const ImGuiTableFlags flags = ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY;
if (ImGui::BeginTable("table", 3, flags, ImVec2(0.0f, 300.0f))) {
ImGui::TableSetupScrollFreeze(1, 1);
ImGui::TableSetupColumn("##row_number", ImGuiTableColumnFlags_WidthFixed | ImGuiTableColumnFlags_NoHeaderLabel,
ImGui::CalcTextSize("000").x + ImGui::GetStyle().CellPadding.x * 2);
ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthFixed, 120.0f);
ImGui::TableSetupColumn("Description", ImGuiTableColumnFlags_WidthStretch);
ImGui::TableHeadersRow();
for (int row = 0; row < table_.size(); ++row) {
ImGui::PushID(row);
ImGui::TableNextRow();
if (row == current_row_) ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg1, ImGui::GetColorU32(ImGuiCol_Header));
ImGui::TableSetColumnIndex(0);
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(std::to_string(row + 1).c_str());
ImGui::TableSetColumnIndex(1);
ImGui::SetNextItemWidth(-FLT_MIN);
if (valueDescriptionEditor(0, &table_[row].first)) current_row_ = row;
ImGui::TableSetColumnIndex(2);
ImGui::SetNextItemWidth(-FLT_MIN);
if (valueDescriptionEditor(1, &table_[row].second)) current_row_ = row;
ImGui::PopID();
}
ImGui::EndTable();
}
bool accept = false, reject = false;
if (dialogButtons("OK", &accept, &reject)) {
if (accept) save();
closing = true;
}
if (!open) closing = true;
if (closing) ImGui::CloseCurrentPopup();
ImGui::EndPopup();
return !closing;
}
void ValueDescriptionDlg::save() {
for (int i = 0; i < table_.size(); ++i) {
std::string val = utils::trimmed(table_[i].first);
std::string desc = utils::trimmed(table_[i].second);
if (!val.empty() && !desc.empty()) {
val_desc.push_back({utils::toDouble(val), desc});
}
}
accepted = true;
}