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

930 lines
32 KiB
C++

#include "tools/cabana/ui/mainwin.h"
#include <algorithm>
#include <cassert>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <string>
#include <vector>
#include "imgui.h"
#include "imgui_internal.h"
#include <GLFW/glfw3.h>
#include "json11/json11.hpp"
#include "tools/cabana/commands.h"
#include "tools/cabana/settings.h"
#include "tools/cabana/ui/app.h"
#include "tools/cabana/ui/dialogs/filedialog.h"
#include "tools/cabana/ui/dialogs/messagebox.h"
#include "tools/cabana/ui/inistate.h"
#include "tools/cabana/ui/threadpool.h"
#include "tools/cabana/ui/tools/findsignal.h"
#include "tools/cabana/ui/tools/findsimilarbits.h"
#include "tools/cabana/ui/util.h"
#include "tools/cabana/utils/export.h"
#include "tools/cabana/utils/util.h"
#include "tools/replay/util.h"
namespace {
constexpr const char *VIDEO_PANEL = "###VideoPanel";
constexpr const char *CENTER_PANEL = "###CenterWidget";
constexpr const char *CHARTS_WINDOW = "Charts###ChartsWindow";
}
MainWindow::MainWindow(GLFWwindow *window, std::unique_ptr<AbstractStream> stream, StreamLoader stream_loader,
const std::string &dbc_file) : window_(window) {
can = &dummy_;
video_splitter_ratio_ = inistate::main_window.video_splitter_ratio;
messages_visible_ = inistate::main_window.messages_visible;
video_visible_ = inistate::main_window.video_visible;
loadFingerprints();
std::error_code ec;
for (const auto &entry : std::filesystem::directory_iterator(OPENDBC_FILE_PATH, ec)) {
if (entry.is_regular_file() && entry.path().extension() == ".dbc") {
opendbc_names_.push_back(entry.path().filename().string());
}
}
std::sort(opendbc_names_.begin(), opendbc_names_.end());
installDownloadProgressHandler([this](uint64_t cur, uint64_t total, bool success) {
utils::runOnMainThread([this, cur, total, success]() { updateDownloadProgress(cur, total, success); });
});
installMessageHandler([this](ReplyMsgType type, const std::string &msg) {
fprintf(stderr, "%s\n", msg.c_str());
utils::runOnMainThread([this, msg]() { showStatusMessage(msg, 2000); });
});
connections_.push_back(dbc()->fileChanged.connect([this]() { dbcFileChanged(); }));
connections_.push_back(UndoStack::instance()->cleanChanged.connect([this](bool clean) {
window_modified_ = !clean;
updateWindowTitle();
}));
startup_stream_ = std::move(stream);
startup_loader_ = std::move(stream_loader);
nextFrame([this, dbc_file]() {
if (startup_loader_) {
loadStartupStream(dbc_file);
} else {
startup_stream_ ? openStream(std::move(startup_stream_), dbc_file) : selectAndOpenStream();
}
});
}
void MainWindow::loadFingerprints() {
std::ifstream json_file((executableDir() / "dbc/car_fingerprint_to_dbc.json"));
if (!json_file) return;
const std::string contents{std::istreambuf_iterator<char>(json_file), std::istreambuf_iterator<char>()};
std::string err;
auto doc = json11::Json::parse(contents, err);
if (!err.empty() || !doc.is_object()) return;
for (const auto &kv : doc.object_items()) {
if (kv.second.is_string()) {
fingerprint_to_dbc_.emplace(kv.first, kv.second.string_value());
}
}
}
void MainWindow::drawFileMenu() {
const bool has_stream = hasStream();
if (ImGui::MenuItem("Open Stream...")) selectAndOpenStream();
if (ImGui::MenuItem("Close stream", nullptr, false, has_stream)) closeStream();
if (ImGui::MenuItem("Export to CSV...", nullptr, false, has_stream)) exportToCSV();
ImGui::Separator();
if (ImGui::MenuItem("New DBC File", "Ctrl+N")) newFile();
if (ImGui::MenuItem("Open DBC File...", "Ctrl+O")) openFile();
if (ImGui::BeginMenu("Manage DBC Files", has_stream)) {
drawManageDBCsMenu();
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Open Recent")) {
drawRecentFilesMenu();
ImGui::EndMenu();
}
ImGui::Separator();
if (ImGui::BeginMenu("Load DBC from IQ.Pilot iqdbc")) {
for (const auto &name : opendbc_names_) {
if (ImGui::MenuItem(name.c_str())) loadDBCFromOpendbc(name);
}
ImGui::EndMenu();
}
if (ImGui::MenuItem("Load DBC From Clipboard")) loadFromClipboard();
ImGui::Separator();
const int cnt = dbc()->nonEmptyDBCCount();
const std::string save_text = cnt > 1 ? "Save " + std::to_string(cnt) + " DBCs..." : "Save DBC...";
if (ImGui::MenuItem(save_text.c_str(), "Ctrl+S", false, cnt > 0)) save();
if (ImGui::MenuItem("Save DBC As...", "Ctrl+Shift+S", false, cnt == 1)) saveAs();
if (ImGui::MenuItem("Copy DBC To Clipboard", nullptr, false, cnt == 1)) saveToClipboard();
ImGui::Separator();
if (ImGui::MenuItem("Settings...")) openSettings();
ImGui::Separator();
if (ImGui::MenuItem("Exit", "Ctrl+Q")) close();
}
void MainWindow::drawMenuBar() {
if (!ImGui::BeginMainMenuBar()) return;
if (ImGui::BeginMenu("File")) {
drawFileMenu();
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Edit")) {
auto stack = UndoStack::instance();
const std::string undo_text = stack->canUndo() ? "Undo " + stack->undoText() : "Undo";
const std::string redo_text = stack->canRedo() ? "Redo " + stack->redoText() : "Redo";
if (ImGui::MenuItem(undo_text.c_str(), "Ctrl+Z", false, stack->canUndo())) stack->undo();
if (ImGui::MenuItem(redo_text.c_str(), "Ctrl+Shift+Z", false, stack->canRedo())) stack->redo();
ImGui::EndMenu();
}
if (ImGui::BeginMenu("View")) {
if (ImGui::MenuItem("Full Screen", "Ctrl+F11")) toggleFullScreen();
ImGui::Separator();
ImGui::MenuItem(messages_widget_ ? messages_widget_->title().c_str() : "MESSAGES", nullptr, &messages_visible_);
ImGui::MenuItem(video_dock_title_.empty() ? "##video_dock" : video_dock_title_.c_str(), nullptr, &video_visible_);
ImGui::Separator();
if (ImGui::MenuItem("Reset Window Layout")) {
messages_visible_ = video_visible_ = true;
reset_layout_ = true;
}
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Tools", hasStream())) {
if (ImGui::MenuItem("Find Similar Bits")) findSimilarBits();
if (ImGui::MenuItem("Find Signal")) findSignal();
ImGui::EndMenu();
}
if (ImGui::BeginMenu("Help")) {
if (ImGui::MenuItem("Help", "F1")) toggleHelp();
ImGui::EndMenu();
}
ImGui::EndMainMenuBar();
}
void MainWindow::createDockWidgets() {
widget_connections_.clear();
messages_widget_ = std::make_unique<MessagesWidget>();
widget_connections_.push_back(messages_widget_->msgSelectionChanged.connect([this](const MessageId &id) { center_widget_.setMessage(id); }));
charts_widget_ = std::make_unique<ChartsWidget>();
center_widget_.setChartsWidget(charts_widget_.get());
video_widget_ = std::make_unique<VideoWidget>();
widget_connections_.push_back(charts_widget_->toggleChartsDocking.connect([this]() { toggleChartsDocking(); }));
widget_connections_.push_back(charts_widget_->showTip.connect([this](double sec) { video_widget_->showThumbnail(sec); }));
}
void MainWindow::showStatusMessage(const std::string &msg, int timeout_ms) {
status_bar_.message = msg;
status_bar_.message_until = timeout_ms > 0 ? ImGui::GetTime() + timeout_ms / 1000.0 : 0;
}
void MainWindow::updateWindowTitle() {
std::string title;
for (auto f : dbc()->allDBCFiles()) {
if (!title.empty()) title += " | ";
title += "(" + toString(dbc()->sources(f)) + ") " + f->name();
}
if (window_modified_) title += "*";
if (!title.empty()) title += " \xe2\x80\x94 ";
title += "Cabana";
glfwSetWindowTitle(window_, title.c_str());
}
void MainWindow::dbcFileChanged() {
UndoStack::instance()->clear();
updateWindowTitle();
nextFrame([this]() { restoreSessionState(); });
}
void MainWindow::selectAndOpenStream() {
stream_selector_.open([this](std::unique_ptr<AbstractStream> stream, const std::string &dbc_file) {
if (stream) {
openStream(std::move(stream), dbc_file);
} else if (!stream_) {
openStream(std::make_unique<DummyStream>());
}
});
}
void MainWindow::loadStartupStream(const std::string &dbc_file) {
wait_dlg_.text = "Loading route...";
wait_dlg_.value = 0;
wait_dlg_.open = true;
wait_dlg_.show_at = ImGui::GetTime() + 4.0;
ThreadPool::instance().run([this, dbc_file, loader = std::move(startup_loader_)]() {
AbstractStream *loaded = nullptr;
std::string error;
try {
loaded = loader().release();
} catch (const std::exception &e) {
error = e.what();
}
utils::runOnMainThread([this, dbc_file, loaded, error]() {
wait_dlg_.open = false;
std::unique_ptr<AbstractStream> stream(loaded);
if (!error.empty()) {
fprintf(stderr, "%s\n", error.c_str());
MessageBox::warning("Failed to load route", error);
}
stream ? openStream(std::move(stream), dbc_file) : openStream(std::make_unique<DummyStream>());
});
});
}
void MainWindow::closeStream() {
openStream(std::make_unique<DummyStream>());
if (dbc()->nonEmptyDBCCount() > 0) {
dbc()->fileChanged();
}
showStatusMessage("stream closed");
}
void MainWindow::exportToCSV() {
std::string dir = settings.last_dir + "/" + can->routeName() + ".csv";
FileDialog::getSaveFileName("Export stream to CSV file", dir, ".csv", [](const std::string &fn) {
if (!fn.empty()) {
utils::exportToCSV(fn);
}
});
}
void MainWindow::newFile(SourceSet s) {
closeFile(s, [s]() { dbc()->open(s, std::string(""), std::string("")); });
}
void MainWindow::openFile(SourceSet s) {
remindSaveChanges([this, s]() {
FileDialog::getOpenFileName("Open File", settings.last_dir, ".dbc", [this, s](const std::string &fn) {
if (!fn.empty()) {
loadFile(fn, s);
}
});
});
}
void MainWindow::loadFile(const std::string &fn, SourceSet s, std::function<void()> then) {
if (!fn.empty()) {
closeFile(s, [this, fn, s, then]() {
std::string error;
if (dbc()->open(s, fn, &error)) {
updateRecentFiles(fn);
showStatusMessage("DBC File " + fn + " loaded", 2000);
if (then) then();
} else {
MessageBox::warning("Failed to load DBC file", "Failed to parse DBC file " + fn, error, then);
}
});
} else if (then) {
then();
}
}
void MainWindow::loadDBCFromOpendbc(const std::string &name) {
loadFile(std::string(OPENDBC_FILE_PATH) + "/" + name);
}
void MainWindow::loadFromClipboard(SourceSet s, bool close_all) {
std::string text;
if (!utils::getClipboardText(&text)) {
MessageBox::warning("Load From Clipboard", "No clipboard tool found. Install xclip (X11) or wl-clipboard (Wayland).");
return;
}
if (text.empty()) {
MessageBox::warning("Load From Clipboard", "Clipboard is empty.");
return;
}
closeFile(s, [s, text]() {
std::string error;
bool ret = dbc()->open(s, std::string(""), text, &error);
if (ret && dbc()->nonEmptyDBCCount() > 0) {
MessageBox::information("Load From Clipboard", "DBC Successfully Loaded!");
} else {
MessageBox::warning("Failed to load DBC from clipboard", "Make sure that you paste the text with correct format.", error);
}
});
}
MainWindow::~MainWindow() {
installDownloadProgressHandler(nullptr);
installMessageHandler(nullptr);
releaseStream();
can = nullptr;
}
void MainWindow::releaseStream() {
tool_dialogs_.clear();
wait_dlg_.connection.disconnect();
wait_dlg_.open = false;
widget_connections_.clear();
charts_widget_.reset();
video_widget_.reset();
center_widget_.clear();
messages_widget_.reset();
stream_connections_.clear();
stream_.reset();
can = &dummy_;
}
void MainWindow::openStream(std::unique_ptr<AbstractStream> stream, const std::string &dbc_file) {
releaseStream();
startStream(std::move(stream), dbc_file);
}
void MainWindow::startStream(std::unique_ptr<AbstractStream> stream, const std::string &dbc_file) {
stream_ = std::move(stream);
can = stream_.get();
stream_connections_.push_back(can->error.connect([](const std::string &msg) {
MessageBox::warning("Error", msg);
}));
can->start();
loadFile(dbc_file, SOURCE_ALL, [this]() {
showStatusMessage("Stream [" + can->routeName() + "] started", 2000);
createDockWidgets();
video_dock_title_ = can->routeName();
if (!dbc()->nonEmptyDBCCount()) {
newFile();
}
stream_connections_.push_back(can->eventsMerged.connect([this](const MessageEventsMap &) { eventsMerged(); }));
if (hasStream()) {
wait_dlg_.text = can->liveStreaming() ? "Waiting for the live stream to start..." : "Loading segment data...";
wait_dlg_.value = 0;
wait_dlg_.open = true;
wait_dlg_.show_at = ImGui::GetTime() + 4.0;
wait_dlg_.connection = can->eventsMerged.connect([this](const MessageEventsMap &) {
wait_dlg_.open = false;
wait_dlg_.connection.disconnect();
});
}
});
}
void MainWindow::eventsMerged() {
const std::string fingerprint = can->carFingerprint();
if (!can->liveStreaming() && std::exchange(car_fingerprint_, fingerprint) != fingerprint) {
video_dock_title_ = "ROUTE: " + can->routeName() + " FINGERPRINT: " + (car_fingerprint_.empty() ? "Unknown Car" : car_fingerprint_);
auto it = fingerprint_to_dbc_.find(car_fingerprint_);
if (!dbc()->nonEmptyDBCCount() && it != fingerprint_to_dbc_.end()) {
nextFrame([this, dbc_name = it->second]() { loadDBCFromOpendbc(dbc_name + ".dbc"); });
}
}
}
void MainWindow::saveFiles(bool as, std::function<void()> then) {
const std::vector<DBCFile *> files = dbc()->nonEmptyDBCFiles();
auto next = std::make_shared<std::function<void(size_t)>>();
*next = [this, as, files, next, then](size_t i) {
if (i >= files.size()) {
if (then) then();
return;
}
auto cb = [next, i]() { (*next)(i + 1); };
as ? saveFileAs(files[i], cb) : saveFile(files[i], cb);
};
(*next)(0);
}
void MainWindow::save(std::function<void()> then) {
saveFiles(false, std::move(then));
}
void MainWindow::saveAs(std::function<void()> then) {
saveFiles(true, std::move(then));
}
void MainWindow::closeFile(SourceSet s, std::function<void()> then) {
remindSaveChanges([s, then]() {
if (s == SOURCE_ALL) {
dbc()->closeAll();
} else {
dbc()->close(s);
}
if (then) then();
});
}
void MainWindow::closeFile(DBCFile *dbc_file) {
assert(dbc_file != nullptr);
remindSaveChanges([this, dbc_file]() {
dbc()->close(dbc_file);
if (dbc()->dbcCount() == 0) {
newFile();
}
});
}
void MainWindow::saveFile(DBCFile *dbc_file, std::function<void()> then) {
assert(dbc_file != nullptr);
if (!dbc_file->filename.empty()) {
dbc_file->save();
UndoStack::instance()->setClean();
showStatusMessage("File saved", 2000);
if (then) then();
} else if (!dbc_file->isEmpty()) {
saveFileAs(dbc_file, then);
} else if (then) {
then();
}
}
void MainWindow::saveFileAs(DBCFile *dbc_file, std::function<void()> then) {
std::string title = "Save File (bus: " + toString(dbc()->sources(dbc_file)) + ")";
std::string default_path = (std::filesystem::path(settings.last_dir) / "untitled.dbc").string();
FileDialog::getSaveFileName(title, default_path, ".dbc", [this, dbc_file, then](const std::string &fn) {
if (!fn.empty()) {
dbc_file->saveAs(fn);
UndoStack::instance()->setClean();
showStatusMessage("File saved as " + fn, 2000);
updateRecentFiles(fn);
}
if (then) then();
});
}
void MainWindow::saveToClipboard() {
for (auto dbc_file : dbc()->nonEmptyDBCFiles()) {
saveFileToClipboard(dbc_file);
}
}
void MainWindow::saveFileToClipboard(DBCFile *dbc_file) {
assert(dbc_file != nullptr);
copyToClipboard(dbc_file->generateDBC());
}
void MainWindow::copyToClipboard(const std::string &text) {
if (utils::setClipboardText(text)) {
MessageBox::information("Copy To Clipboard", "DBC Successfully copied!");
} else {
MessageBox::warning("Copy To Clipboard", "Failed to copy DBC to clipboard. Install xclip (X11) or wl-clipboard (Wayland).");
}
}
void MainWindow::drawManageDBCsMenu() {
for (int source : can->sources) {
if (source >= 64) continue;
SourceSet ss = {source, uint8_t(source + 128), uint8_t(source + 192)};
auto dbc_file = dbc()->findDBCFile(source);
const std::string title = "Bus " + std::to_string(source) + " (" + (dbc_file ? dbc_file->name() : "No DBCs loaded") + ")";
ImGui::PushID(source);
if (ImGui::BeginMenu(title.c_str())) {
if (ImGui::MenuItem("New DBC File...")) newFile(ss);
if (ImGui::MenuItem("Open DBC File...")) openFile(ss);
if (ImGui::MenuItem("Load DBC From Clipboard...")) loadFromClipboard(ss, false);
if (dbc_file) {
ImGui::Separator();
ImGui::MenuItem((dbc_file->name() + " (" + toString(dbc()->sources(dbc_file)) + ")").c_str(), nullptr, false, false);
if (ImGui::MenuItem("Save...")) saveFile(dbc_file);
if (ImGui::MenuItem("Save As...")) saveFileAs(dbc_file);
if (ImGui::MenuItem("Copy to Clipboard...")) saveFileToClipboard(dbc_file);
if (ImGui::MenuItem("Remove from this bus...")) closeFile(ss, {});
if (ImGui::MenuItem("Remove from all buses...")) closeFile(dbc_file);
}
ImGui::EndMenu();
}
ImGui::PopID();
}
}
void MainWindow::updateRecentFiles(const std::string &fn) {
settings.recent_files.erase(std::remove(settings.recent_files.begin(), settings.recent_files.end(), fn), settings.recent_files.end());
settings.recent_files.insert(settings.recent_files.begin(), fn);
while (settings.recent_files.size() > MAX_RECENT_FILES) {
settings.recent_files.pop_back();
}
settings.last_dir = std::filesystem::absolute(fn).parent_path().string();
}
void MainWindow::drawRecentFilesMenu() {
int num_recent_files = std::min<int>(settings.recent_files.size(), MAX_RECENT_FILES);
if (!num_recent_files) {
ImGui::MenuItem("No Recent Files", nullptr, false, false);
return;
}
for (int i = 0; i < num_recent_files; ++i) {
std::string text = std::to_string(i + 1) + " " + std::filesystem::path(settings.recent_files[i]).filename().string();
ImGui::PushID(i);
if (ImGui::MenuItem(text.c_str())) loadFile(settings.recent_files[i]);
ImGui::PopID();
}
}
void MainWindow::remindSaveChanges(std::function<void()> then) {
if (UndoStack::instance()->isClean()) {
UndoStack::instance()->clear();
if (then) then();
return;
}
std::string text = "You have unsaved changes. Press ok to save them, cancel to discard.";
MessageBox::question("Unsaved Changes", text, [this, then](bool ok) {
if (ok) {
save([this, then]() { remindSaveChanges(then); });
} else {
UndoStack::instance()->clear();
if (then) then();
}
});
}
void MainWindow::updateDownloadProgress(uint64_t cur, uint64_t total, bool success) {
const double fraction = total > 0 ? cur / (double)total : 0.0;
if (wait_dlg_.open) wait_dlg_.value = (int)(fraction * 100);
if (success && cur < total) {
status_bar_.progress_value = fraction;
status_bar_.progress_text = "Downloading " + std::to_string((int)(fraction * 100)) + "% (" + formattedDataSize(total) + ")";
status_bar_.progress_visible = true;
} else {
status_bar_.progress_visible = false;
}
}
void MainWindow::toggleChartsDocking() {
charts_floating_ = !charts_floating_;
charts_widget_->setIsDocked(!charts_floating_);
}
void MainWindow::close() {
if (closing_) return;
closing_ = true;
remindSaveChanges([this]() { finishClose(); });
}
void MainWindow::finishClose() {
auto &state = inistate::main_window;
state.maximized = glfwGetWindowAttrib(window_, GLFW_MAXIMIZED);
if (full_screen_) {
#ifndef __APPLE__
state.pos[0] = windowed_rect_[0]; state.pos[1] = windowed_rect_[1];
state.size[0] = windowed_rect_[2]; state.size[1] = windowed_rect_[3];
#endif
} else if (!state.maximized) {
glfwGetWindowPos(window_, &state.pos[0], &state.pos[1]);
glfwGetWindowSize(window_, &state.size[0], &state.size[1]);
}
state.has_geometry = state.size[0] > 0 && state.size[1] > 0;
state.video_splitter_ratio = video_splitter_ratio_;
state.messages_visible = messages_visible_;
state.video_visible = video_visible_;
settings.ui_state = inistate::save();
saveSessionState();
settings.save();
exited_ = true;
}
void MainWindow::openSettings() {
settings_dialog_.open();
}
void MainWindow::findSimilarBits() {
auto dlg = std::make_unique<FindSimilarBitsDlg>();
dlg->connections_.push_back(dlg->openMessage.connect([this](const MessageId &id) { messages_widget_->selectMessage(id); }));
tool_dialogs_.push_back(std::move(dlg));
}
void MainWindow::findSignal() {
auto dlg = std::make_unique<FindSignalDlg>();
dlg->connections_.push_back(dlg->openMessage.connect([this](const MessageId &id) { messages_widget_->selectMessage(id); }));
tool_dialogs_.push_back(std::move(dlg));
}
void MainWindow::toggleHelp() {
help_overlay_.toggle();
}
void MainWindow::toggleFullScreen() {
#ifdef __APPLE__
toggleNativeFullScreen(window_);
#else
full_screen_ = !full_screen_;
if (full_screen_) {
glfwGetWindowPos(window_, &windowed_rect_[0], &windowed_rect_[1]);
glfwGetWindowSize(window_, &windowed_rect_[2], &windowed_rect_[3]);
GLFWmonitor *monitor = glfwGetPrimaryMonitor();
const GLFWvidmode *mode = glfwGetVideoMode(monitor);
glfwSetWindowMonitor(window_, monitor, 0, 0, mode->width, mode->height, mode->refreshRate);
} else {
glfwSetWindowMonitor(window_, nullptr, windowed_rect_[0], windowed_rect_[1], windowed_rect_[2], windowed_rect_[3], 0);
glfwMaximizeWindow(window_);
}
#endif
}
void MainWindow::saveSessionState() {
settings.recent_dbc_file = "";
settings.active_msg_id = "";
settings.selected_msg_ids.clear();
settings.active_charts.clear();
const auto files = dbc()->nonEmptyDBCFiles();
if (!files.empty()) settings.recent_dbc_file = files.front()->filename;
if (auto *detail = center_widget_.getDetailWidget()) {
auto [active_id, ids] = detail->serializeMessageIds();
settings.active_msg_id = active_id;
settings.selected_msg_ids = ids;
}
if (charts_widget_) {
settings.active_charts = charts_widget_->serializeChartIds();
}
}
void MainWindow::restoreSessionState() {
if (settings.recent_dbc_file.empty() || dbc()->nonEmptyDBCCount() == 0) return;
if (dbc()->nonEmptyDBCFiles().front()->filename != settings.recent_dbc_file) return;
if (!settings.selected_msg_ids.empty()) {
center_widget_.ensureDetailWidget()->restoreTabs(settings.active_msg_id, settings.selected_msg_ids);
}
if (charts_widget_ != nullptr && !settings.active_charts.empty()) {
charts_widget_->restoreChartsFromIds(settings.active_charts);
}
}
void MainWindow::handleShortcuts() {
const ImGuiIO &io = ImGui::GetIO();
for (const KeyEvent &e : takeKeyEvents()) {
const bool ctrl = e.mods & (GLFW_MOD_CONTROL | GLFW_MOD_SUPER);
const bool shift = e.mods & GLFW_MOD_SHIFT;
if (e.key == GLFW_KEY_SPACE && !ctrl && can && !io.WantTextInput) can->pause(!can->isPaused());
if (e.key == GLFW_KEY_F1) toggleHelp();
if (e.key == GLFW_KEY_F11 && ctrl) toggleFullScreen();
if (e.key == GLFW_KEY_ESCAPE && full_screen_ && !io.WantTextInput &&
!ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopupId | ImGuiPopupFlags_AnyPopupLevel)) {
toggleFullScreen();
}
if (!ctrl) continue;
if (e.key == GLFW_KEY_N) newFile();
if (e.key == GLFW_KEY_O) openFile();
if (e.key == GLFW_KEY_S) {
if (shift) {
if (dbc()->nonEmptyDBCCount() == 1) saveAs();
} else if (dbc()->nonEmptyDBCCount() > 0) {
save();
}
}
if (e.key == GLFW_KEY_Z && !io.WantTextInput) shift ? UndoStack::instance()->redo() : UndoStack::instance()->undo();
if (e.key == GLFW_KEY_Q) close();
}
}
void MainWindow::drawStatusBar() {
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::GetStyle().Colors[ImGuiCol_MenuBarBg]);
ImGui::BeginChild("status_bar", ImVec2(0, ImGui::GetFrameHeight()), ImGuiChildFlags_None, ImGuiWindowFlags_NoScrollbar);
const float width = ImGui::GetContentRegionAvail().x;
const float pad = ImGui::GetStyle().WindowPadding.x;
ImGui::SetCursorPosX(pad);
ImGui::AlignTextToFramePadding();
auto &bar = status_bar_;
if (!bar.message.empty() && (bar.message_until == 0 || ImGui::GetTime() < bar.message_until)) {
ImGui::TextUnformatted(bar.message.c_str());
} else {
bar.message.clear();
ImGui::TextUnformatted("For Help, Press F1");
}
if (bar.progress_visible) {
ImGui::SameLine(width - pad - 300.0f);
ImGui::ProgressBar(bar.progress_value, ImVec2(300.0f, 16.0f), bar.progress_text.c_str());
}
ImGui::EndChild();
ImGui::PopStyleColor();
}
void MainWindow::drawWaitDialog() {
const char *id = "###WaitDialog";
if (wait_dlg_.open && !ImGui::IsPopupOpen(id) && ImGui::GetTime() >= wait_dlg_.show_at) ImGui::OpenPopup(id);
if (!ImGui::IsPopupOpen(id)) return;
ImGui::SetNextWindowSize(ImVec2(400.0f, 0.0f), ImGuiCond_Always);
setNextDialogWindow(ImVec2(0.0f, 0.0f));
if (ImGui::BeginPopupModal(id, nullptr, ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_AlwaysAutoResize)) {
ImGui::TextUnformatted(wait_dlg_.text.c_str());
ImGui::ProgressBar(wait_dlg_.value / 100.0f, ImVec2(-1.0f, 0.0f), wait_dlg_.value == 0 ? "" : (const char *)nullptr);
bool abort = false, rejected = false;
dialogButtons("Abort", &abort, &rejected, true, nullptr);
if (abort || rejected) {
wait_dlg_.open = false;
close();
}
if (!wait_dlg_.open) ImGui::CloseCurrentPopup();
ImGui::EndPopup();
}
}
void MainWindow::drawDockspace() {
const ImGuiViewport *viewport = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(viewport->WorkPos);
ImGui::SetNextWindowSize(viewport->WorkSize);
ImGui::SetNextWindowViewport(viewport->ID);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f));
const ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoNavFocus |
ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoBackground |
ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse;
ImGui::Begin("##host", nullptr, flags);
ImGui::PopStyleVar(3);
const float status_height = full_screen_ ? 0.0f : ImGui::GetFrameHeight() + ImGui::GetStyle().ItemSpacing.y;
const ImVec2 dock_size(ImGui::GetContentRegionAvail().x, ImGui::GetContentRegionAvail().y - status_height);
const ImGuiID dock_id = ImGui::GetID("cabana_dockspace");
if (reset_layout_ || ImGui::DockBuilderGetNode(dock_id) == nullptr) {
ImGui::DockBuilderRemoveNode(dock_id);
ImGui::DockBuilderAddNode(dock_id, ImGuiDockNodeFlags_DockSpace);
ImGui::DockBuilderSetNodeSize(dock_id, dock_size);
ImGuiID center = dock_id, left = 0, right = 0;
ImGui::DockBuilderSplitNode(center, ImGuiDir_Left, 0.28f, &left, &center);
ImGui::DockBuilderSplitNode(center, ImGuiDir_Right, 0.4f, &right, &center);
ImGui::DockBuilderDockWindow(MESSAGES_PANEL_ID, left);
ImGui::DockBuilderDockWindow(VIDEO_PANEL, right);
ImGui::DockBuilderDockWindow(CENTER_PANEL, center);
ImGui::DockBuilderGetNode(center)->LocalFlags |= ImGuiDockNodeFlags_NoTabBar;
ImGui::DockBuilderFinish(dock_id);
reset_layout_ = false;
}
const float min_panel_width = SignalView::minimumWidth() + (ImGui::GetStyle().WindowPadding.x + ImGui::GetStyle().WindowBorderSize) * 2;
ImGui::PushStyleVar(ImGuiStyleVar_WindowMinSize, ImVec2(min_panel_width, ImGui::GetStyle().WindowMinSize.y));
ImGui::DockSpace(dock_id, dock_size);
ImGui::PopStyleVar();
if (!full_screen_) drawStatusBar();
ImGui::End();
}
namespace {
bool floatingOut() { return ImGui::GetWindowViewport() != ImGui::GetMainViewport(); }
void setNextPanelClass() {
ImGuiWindowClass window_class;
window_class.ViewportFlagsOverrideSet = ImGuiViewportFlags_NoAutoMerge;
window_class.DockNodeFlagsOverrideSet = ImGuiDockNodeFlags_NoWindowMenuButton;
ImGui::SetNextWindowClass(&window_class);
}
}
void MainWindow::drawMessagesPanel() {
const std::string name = messages_widget_->title() + MESSAGES_PANEL_ID;
setNextPanelClass();
if (ImGui::Begin(name.c_str(), &messages_visible_)) {
help_overlay_.add(messages_widget_->whatsThis(), ImGui::GetCurrentWindow()->Rect());
messages_widget_->draw();
}
const bool floating = floatingOut();
ImGui::End();
if (!messages_visible_ && floating) messages_visible_ = reset_layout_ = true;
}
void MainWindow::drawVideoPanel() {
const std::string name = video_dock_title_ + VIDEO_PANEL;
setNextPanelClass();
const bool video_open = ImGui::Begin(name.c_str(), &video_visible_);
const bool floating = floatingOut();
if (!video_open) {
video_widget_->setVisible(false);
} else {
const ImVec2 avail = ImGui::GetContentRegionAvail();
const bool live = can->liveStreaming();
const float video_padding = ImGui::GetStyle().WindowPadding.y * 2.0f;
const float default_h = video_widget_->defaultHeight(avail.x) + video_padding;
const float video_hint = video_splitter_ratio_ >= 0.0f ? avail.y * video_splitter_ratio_ : default_h;
float video_h = charts_floating_ ? avail.y : std::clamp(video_hint, 0.0f, avail.y - 1.0f);
if (live) video_h = default_h;
if (!charts_floating_ && !live) {
const float min_h = std::min(video_widget_->sizeHintHeight() + video_padding, avail.y - 1.0f);
video_h = video_h < min_h / 2 ? 0.0f : std::max(video_h, min_h);
}
if (video_h > 0.0f) {
ImGui::BeginChild("video", ImVec2(0, video_h), ImGuiChildFlags_Borders);
help_overlay_.add(video_widget_->whatsThis(), ImGui::GetCurrentWindow()->Rect());
video_widget_->draw();
ImGui::EndChild();
} else {
video_widget_->setVisible(false);
}
if (!charts_floating_) {
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(ImGui::GetStyle().ItemSpacing.x, 0.0f));
ImGui::InvisibleButton("##splitter", ImVec2(-1.0f, ImGui::GetStyle().WindowPadding.x));
if (ImGui::IsItemActive() && !live) {
const float top = ImGui::GetWindowPos().y + ImGui::GetCursorStartPos().y;
video_splitter_ratio_ = std::clamp((ImGui::GetMousePos().y - top) / avail.y, 0.0f, 1.0f);
}
if (ImGui::IsItemHovered() && !live) ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeNS);
ImGui::BeginChild("charts", ImVec2(0, 0), ImGuiChildFlags_Borders, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
ImGui::PopStyleVar();
help_overlay_.add(charts_widget_->whatsThis(), ImGui::GetCurrentWindow()->Rect());
charts_widget_->draw();
ImGui::EndChild();
}
}
ImGui::End();
if (!video_visible_ && floating) video_visible_ = reset_layout_ = true;
}
void MainWindow::draw() {
#ifdef __APPLE__
full_screen_ = isNativeFullScreen(window_);
#endif
auto pending = std::move(next_frame_);
next_frame_.clear();
for (auto &fn : pending) fn();
if (ImGui::GetTopMostPopupModal() == nullptr) {
handleShortcuts();
} else {
takeKeyEvents();
}
if (!full_screen_) drawMenuBar();
drawDockspace();
if (ImGui::Begin(CENTER_PANEL, nullptr, ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) {
center_widget_.draw();
if (auto *detail = center_widget_.getDetailWidget(); detail && help_overlay_.visible()) {
for (const auto &[text, rect] : detail->helpRects()) help_overlay_.add(text, rect);
}
}
ImGui::End();
if (messages_widget_ && messages_visible_) drawMessagesPanel();
if (video_widget_ && !video_visible_) video_widget_->setVisible(false);
if (video_widget_ && video_visible_) drawVideoPanel();
if (charts_widget_ && charts_floating_) {
bool open = true;
ImGui::SetNextWindowSize(ImGui::GetMainViewport()->WorkSize, ImGuiCond_Appearing);
setNextWindowFloatsOut();
if (ImGui::Begin(CHARTS_WINDOW, &open, ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse)) charts_widget_->draw();
ImGui::End();
if (!open) toggleChartsDocking();
}
for (auto it = tool_dialogs_.begin(); it != tool_dialogs_.end();) {
it = (*it)->draw() ? it + 1 : tool_dialogs_.erase(it);
}
stream_selector_.draw();
settings_dialog_.draw();
drawWaitDialog();
FileDialog::draw();
MessageBox::draw();
help_overlay_.draw();
if (ImGui::IsKeyPressed(ImGuiKey_Escape, false)) {
ImGuiWindow *top = topPopupWindow();
if (top != nullptr && !(top->Flags & ImGuiWindowFlags_Modal)) ImGui::ClosePopupToLevel(GImGui->OpenPopupStack.Size - 1, true);
}
}