<NME> mainwindow.cpp
<BEF> /*
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
 */

#include <functional>
#include <stdio.h>
#include <numeric>

#include <QApplication>
#include <QActionGroup>
#include <QCheckBox>
#include <QCommandLineParser>
#include <QDebug>
#include <QDesktopServices>
#include <QDomDocument>
#include <QDoubleSpinBox>
#include <QElapsedTimer>
#include <QFileDialog>
#include <QInputDialog>
#include <QMenu>
#include <QGroupBox>
#include <QMessageBox>
#include <QMimeData>
#include <QMouseEvent>
#include <QPluginLoader>
#include <QPushButton>
#include <QKeySequence>
#include <QScrollBar>
#include <QSettings>
#include <QStringListModel>
#include <QStringRef>
#include <QThread>
#include <QTextStream>
#include <QWindow>
#include <QHeaderView>
#include <QStandardPaths>
#include <QXmlStreamReader>

#include "mainwindow.h"
#include "curvelist_panel.h"
#include "tabbedplotwidget.h"
#include "PlotJuggler/plotdata.h"
#include "qwt_plot_canvas.h"
#include "transforms/function_editor.h"
#include "transforms/lua_custom_function.h"
#include "utils.h"
#include "stylesheet.h"
#include "dummy_data.h"
#include "PlotJuggler/svg_util.h"
#include "PlotJuggler/reactive_function.h"
#include "multifile_prefix.h"

#include "ui_aboutdialog.h"
#include "ui_support_dialog.h"
#include "preferences_dialog.h"
#include "nlohmann_parsers.h"
#include "cheatsheet/cheatsheet_dialog.h"
#include "colormap_editor.h"

#ifdef COMPILED_WITH_CATKIN

#endif
#ifdef COMPILED_WITH_AMENT
#include <ament_index_cpp/get_package_prefix.hpp>
#include <ament_index_cpp/get_package_share_directory.hpp>
#endif

MainWindow::MainWindow(const QCommandLineParser& commandline_parser, QWidget* parent)
  : QMainWindow(parent)
  , ui(new Ui::MainWindow)
  , _undo_shortcut(QKeySequence(Qt::CTRL + Qt::Key_Z), this)
  , _redo_shortcut(QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_Z), this)
  , _fullscreen_shortcut(Qt::Key_F10, this)
  , _streaming_shortcut(QKeySequence(Qt::CTRL + Qt::Key_Space), this)
  , _playback_shotcut(Qt::Key_Space, this)
  , _minimized(false)
  , _active_streamer_plugin(nullptr)
  , _disable_undo_logging(false)
  , _tracker_time(0)
  , _tracker_param(CurveTracker::VALUE)
  , _labels_status(LabelStatus::RIGHT)
  , _recent_data_files(new QMenu())
  , _recent_layout_files(new QMenu())
{
  QLocale::setDefault(QLocale::c());  // set as default
  setAcceptDrops(true);

  _test_option = commandline_parser.isSet("test");
  _autostart_publishers = commandline_parser.isSet("publish");

  if (commandline_parser.isSet("enabled_plugins"))
  {
    _enabled_plugins =
        commandline_parser.value("enabled_plugins").split(";", QString::SkipEmptyParts);
    // Treat the command-line parameter  '--enabled_plugins *' to mean all plugings are
    // enabled
    if ((_enabled_plugins.size() == 1) && (_enabled_plugins.contains("*")))
    {
      _enabled_plugins.clear();
    }
  }
  if (commandline_parser.isSet("disabled_plugins"))
  {
    _disabled_plugins =
        commandline_parser.value("disabled_plugins").split(";", QString::SkipEmptyParts);
  }

  _curvelist_widget = new CurveListPanel(_mapped_plot_data, _transform_functions, this);

  ui->setupUi(this);
  }
  QFile fileTitle( _skin_path + "/mainwindow_title.txt");
  if(fileTitle.open(QIODevice::ReadOnly)) {
    QString title = fileTitle.readAll();
    setWindowTitle(title);
  }

    {
      _skin_path = path.absolutePath();
    }
  }
  if (commandline_parser.isSet("window_title"))
  {
    setWindowTitle(commandline_parser.value("window_title"));
  }
  else
  {
    QFile fileTitle(_skin_path + "/mainwindow_title.txt");
    if (fileTitle.open(QIODevice::ReadOnly))
    {
      QString title = fileTitle.readAll().trimmed();
      setWindowTitle(title);
    }
  }

  QSettings settings;

  ui->playbackLoop->setText("");
  ui->pushButtonZoomOut->setText("");
  ui->pushButtonPlay->setText("");
  ui->pushButtonUseDateTime->setText("");
  ui->pushButtonActivateGrid->setText("");
  ui->pushButtonRatio->setText("");
  ui->pushButtonLink->setText("");
  ui->pushButtonTimeTracker->setText("");
  ui->pushButtonLoadDatafile->setText("");
  ui->pushButtonRemoveTimeOffset->setText("");
  ui->pushButtonLegend->setText("");

  if (commandline_parser.isSet("buffer_size"))
  {
    int buffer_size = std::max(10, commandline_parser.value("buffer_size").toInt());
    ui->streamingSpinBox->setMaximum(buffer_size);
  }

  _animated_streaming_movie = new QMovie(":/resources/animated_radio.gif");
  _animated_streaming_movie->setScaledSize(ui->labelStreamingAnimation->size());
  _animated_streaming_movie->jumpToFrame(0);

  _animated_streaming_timer = new QTimer();
  _animated_streaming_timer->setSingleShot(true);

  connect(_animated_streaming_timer, &QTimer::timeout, this, [this]() {
    _animated_streaming_movie->stop();
    _animated_streaming_movie->jumpToFrame(0);
  });

  _tracker_delay.connectCallback([this]() {
    updatedDisplayTime();
    onUpdateLeftTableValues();
  });

  ui->labelStreamingAnimation->setMovie(_animated_streaming_movie);
  ui->labelStreamingAnimation->setHidden(true);

  connect(this, &MainWindow::stylesheetChanged, this, &MainWindow::on_stylesheetChanged);

  connect(this, &MainWindow::stylesheetChanged, _curvelist_widget,
          &CurveListPanel::on_stylesheetChanged);

  connect(_curvelist_widget, &CurveListPanel::hiddenItemsChanged, this,
          &MainWindow::onUpdateLeftTableValues);

  connect(_curvelist_widget, &CurveListPanel::deleteCurves, this,
          &MainWindow::onDeleteMultipleCurves);

  connect(_curvelist_widget, &CurveListPanel::createMathPlot, this,
          &MainWindow::onAddCustomPlot);

  connect(_curvelist_widget, &CurveListPanel::editMathPlot, this,
          &MainWindow::onEditCustomPlot);

  connect(_curvelist_widget, &CurveListPanel::refreshMathPlot, this,
          &MainWindow::onRefreshCustomPlot);

  connect(ui->timeSlider, &RealSlider::realValueChanged, this,
          &MainWindow::onTimeSlider_valueChanged);

  connect(ui->playbackRate, &QDoubleSpinBox::editingFinished, this,
          [this]() { ui->playbackRate->clearFocus(); });

  connect(ui->playbackStep, &QDoubleSpinBox::editingFinished, this,
          [this]() { ui->playbackStep->clearFocus(); });

  connect(_curvelist_widget, &CurveListPanel::requestDeleteAll, this, [this](int option) {
    if (option == 1)
    {
      deleteAllData();
    }
    else if (option == 2)
    {
      on_actionClearBuffer_triggered();
    }
  });

  _main_tabbed_widget =
      new TabbedPlotWidget("Main Window", this, _mapped_plot_data, this);

  connect(this, &MainWindow::stylesheetChanged, _main_tabbed_widget,
          &TabbedPlotWidget::on_stylesheetChanged);

  ui->plottingLayout->insertWidget(0, _main_tabbed_widget, 1);
  ui->leftLayout->addWidget(_curvelist_widget, 1);

  ui->mainSplitter->setCollapsible(0, true);
  ui->mainSplitter->setStretchFactor(0, 2);
  ui->mainSplitter->setStretchFactor(1, 6);

  ui->layoutTimescale->removeWidget(ui->widgetButtons);
  _main_tabbed_widget->tabWidget()->setCornerWidget(ui->widgetButtons);

  connect(ui->mainSplitter, SIGNAL(splitterMoved(int, int)),
          SLOT(on_splitterMoved(int, int)));

  initializeActions();

  LoadColorMapFromSettings();

  //------------ Load plugins -------------
  auto plugin_extra_folders =
      commandline_parser.value("plugin_folders").split(";", QString::SkipEmptyParts);

  _default_streamer = commandline_parser.value("start_streamer");

  loadAllPlugins(plugin_extra_folders);

  //------------------------------------

  _undo_timer.start();

  // save initial state
  onUndoableChange();

  _replot_timer = new QTimer(this);
  connect(_replot_timer, &QTimer::timeout, this,
          [this]() { updateDataAndReplot(false); });

  _publish_timer = new QTimer(this);
  _publish_timer->setInterval(20);
  connect(_publish_timer, &QTimer::timeout, this, &MainWindow::onPlaybackLoop);

  ui->menuFile->setToolTipsVisible(true);

  this->setMenuBar(ui->menuBar);
  ui->menuBar->setNativeMenuBar(false);

  if (_test_option)
  {
    buildDummyData();
  }

  bool file_loaded = false;
  if (commandline_parser.isSet("datafile"))
  {
    QStringList datafiles = commandline_parser.values("datafile");
    file_loaded = loadDataFromFiles(datafiles);
  }
  if (commandline_parser.isSet("layout"))
  {
    loadLayoutFromFile(commandline_parser.value("layout"));
  }

  restoreGeometry(settings.value("MainWindow.geometry").toByteArray());
  restoreState(settings.value("MainWindow.state").toByteArray());

  // qDebug() << "restoreGeometry";

  bool activate_grid = settings.value("MainWindow.activateGrid", false).toBool();
  ui->pushButtonActivateGrid->setChecked(activate_grid);

  bool zoom_link_active = settings.value("MainWindow.buttonLink", true).toBool();
  ui->pushButtonLink->setChecked(zoom_link_active);

  bool ration_active = settings.value("MainWindow.buttonRatio", true).toBool();
  ui->pushButtonRatio->setChecked(ration_active);

  int streaming_buffer_value =
      settings.value("MainWindow.streamingBufferValue", 5).toInt();
  ui->streamingSpinBox->setValue(streaming_buffer_value);

  bool datetime_display = settings.value("MainWindow.dateTimeDisplay", false).toBool();
  ui->pushButtonUseDateTime->setChecked(datetime_display);

  bool remove_time_offset = settings.value("MainWindow.removeTimeOffset", true).toBool();
  ui->pushButtonRemoveTimeOffset->setChecked(remove_time_offset);

  //  ui->widgetOptions->setVisible(ui->pushButtonOptions->isChecked());

  if (settings.value("MainWindow.hiddenFileFrame", false).toBool())
  {
    ui->buttonHideFileFrame->setText("+");
    ui->frameFile->setHidden(true);
  }
  if (settings.value("MainWindow.hiddenStreamingFrame", false).toBool())
  {
    ui->buttonHideStreamingFrame->setText("+");
    ui->frameStreaming->setHidden(true);
  }
  if (settings.value("MainWindow.hiddenPublishersFrame", false).toBool())
  {
    ui->buttonHidePublishersFrame->setText("+");
    ui->framePublishers->setHidden(true);
  }

  //----------------------------------------------------------
  QIcon trackerIconA, trackerIconB, trackerIconC;

  trackerIconA.addFile(QStringLiteral(":/style_light/line_tracker.png"), QSize(36, 36));
  trackerIconB.addFile(QStringLiteral(":/style_light/line_tracker_1.png"), QSize(36, 36));
  trackerIconC.addFile(QStringLiteral(":/style_light/line_tracker_a.png"), QSize(36, 36));

  _tracker_button_icons[CurveTracker::LINE_ONLY] = trackerIconA;
  _tracker_button_icons[CurveTracker::VALUE] = trackerIconB;
  _tracker_button_icons[CurveTracker::VALUE_NAME] = trackerIconC;

  int tracker_setting =
      settings.value("MainWindow.timeTrackerSetting", (int)CurveTracker::VALUE).toInt();
  _tracker_param = static_cast<CurveTracker::Parameter>(tracker_setting);

  ui->pushButtonTimeTracker->setIcon(_tracker_button_icons[_tracker_param]);

  forEachWidget([&](PlotWidget* plot) { plot->configureTracker(_tracker_param); });

  auto editor_layout = new QVBoxLayout();
  editor_layout->setMargin(0);
  ui->formulaPage->setLayout(editor_layout);
  _function_editor =
      new FunctionEditorWidget(_mapped_plot_data, _transform_functions, this);
  editor_layout->addWidget(_function_editor);

  connect(_function_editor, &FunctionEditorWidget::closed, this,
          [this]() { ui->widgetStack->setCurrentIndex(0); });

  connect(this, &MainWindow::stylesheetChanged, _function_editor,
          &FunctionEditorWidget::on_stylesheetChanged);

  connect(_function_editor, &FunctionEditorWidget::accept, this,
          &MainWindow::onCustomPlotCreated);

  QString theme = settings.value("Preferences::theme", "light").toString();
  if (theme != "dark")
  {
    theme = "light";
  }
  loadStyleSheet(tr(":/resources/stylesheet_%1.qss").arg(theme));

  // builtin messageParsers
  auto json_parser = std::make_shared<JSON_ParserFactory>();
  _parser_factories.insert({ json_parser->encoding(), json_parser });

  auto cbor_parser = std::make_shared<CBOR_ParserFactory>();
  _parser_factories.insert({ cbor_parser->encoding(), cbor_parser });

  auto bson_parser = std::make_shared<BSON_ParserFactory>();
  _parser_factories.insert({ bson_parser->encoding(), bson_parser });

  auto msgpack = std::make_shared<MessagePack_ParserFactory>();
  _parser_factories.insert({ msgpack->encoding(), msgpack });

  if (!_default_streamer.isEmpty())
  {
    auto index = ui->comboStreaming->findText(_default_streamer);
    if (index != -1)
    {
      ui->comboStreaming->setCurrentIndex(index);
      settings.setValue("MainWindow.previousStreamingPlugin", _default_streamer);
    }
  }
}

MainWindow::~MainWindow()
{
  // important: avoid problems with plugins
  _mapped_plot_data.user_defined.clear();

  delete ui;
}

void MainWindow::onUndoableChange()
{
  if (_disable_undo_logging)
    return;

  int elapsed_ms = _undo_timer.restart();

  // overwrite the previous
  if (elapsed_ms < 100)
  {
    if (_undo_states.empty() == false)
      _undo_states.pop_back();
  }

  while (_undo_states.size() >= 100)
    _undo_states.pop_front();
  _undo_states.push_back(xmlSaveState());
  _redo_states.clear();
  // qDebug() << "undo " << _undo_states.size();
}

void MainWindow::onRedoInvoked()
{
  _disable_undo_logging = true;
  if (_redo_states.size() > 0)
  {
    QDomDocument state_document = _redo_states.back();
    while (_undo_states.size() >= 100)
      _undo_states.pop_front();
    _undo_states.push_back(state_document);
    _redo_states.pop_back();

    xmlLoadState(state_document);
  }
  // qDebug() << "undo " << _undo_states.size();
  _disable_undo_logging = false;
}

void MainWindow::onUndoInvoked()
{
  _disable_undo_logging = true;
  if (_undo_states.size() > 1)
  {
    QDomDocument state_document = _undo_states.back();
    while (_redo_states.size() >= 100)
      _redo_states.pop_front();
    _redo_states.push_back(state_document);
    _undo_states.pop_back();
    state_document = _undo_states.back();

    xmlLoadState(state_document);
  }
  // qDebug() << "undo " << _undo_states.size();
  _disable_undo_logging = false;
}

void MainWindow::onUpdateLeftTableValues()
{
  _curvelist_widget->update2ndColumnValues(_tracker_time);
}

void MainWindow::onTrackerMovedFromWidget(QPointF relative_pos)
{
  _tracker_time = relative_pos.x() + _time_offset.get();

  auto prev = ui->timeSlider->blockSignals(true);
  ui->timeSlider->setRealValue(_tracker_time);
  ui->timeSlider->blockSignals(prev);

  onTrackerTimeUpdated(_tracker_time, true);
}

void MainWindow::onTimeSlider_valueChanged(double abs_time)
{
  _tracker_time = abs_time;
  onTrackerTimeUpdated(_tracker_time, true);
}

void MainWindow::onTrackerTimeUpdated(double absolute_time, bool do_replot)
{
  _tracker_delay.triggerSignal(100);

  for (auto& it : _state_publisher)
  {
    it.second->updateState(absolute_time);
  }

  updateReactivePlots();

  forEachWidget([&](PlotWidget* plot) {
    plot->setTrackerPosition(_tracker_time);
    if (do_replot)
    {
      plot->replot();
    }
  });
}

void MainWindow::initializeActions()
{
  _undo_shortcut.setContext(Qt::ApplicationShortcut);
  _redo_shortcut.setContext(Qt::ApplicationShortcut);
  _fullscreen_shortcut.setContext(Qt::ApplicationShortcut);

  connect(&_undo_shortcut, &QShortcut::activated, this, &MainWindow::onUndoInvoked);
  connect(&_redo_shortcut, &QShortcut::activated, this, &MainWindow::onRedoInvoked);
  connect(&_streaming_shortcut, &QShortcut::activated, this,
          &MainWindow::on_streamingToggled);
  connect(&_playback_shotcut, &QShortcut::activated, ui->pushButtonPlay,
          &QPushButton::toggle);
  connect(&_fullscreen_shortcut, &QShortcut::activated, this,
          &MainWindow::onActionFullscreenTriggered);

  QShortcut* open_menu_shortcut = new QShortcut(QKeySequence(Qt::ALT + Qt::Key_F), this);
  connect(open_menu_shortcut, &QShortcut::activated,
          [this]() { ui->menuFile->exec(ui->menuBar->mapToGlobal(QPoint(0, 25))); });

  QShortcut* open_help_shortcut = new QShortcut(QKeySequence(Qt::ALT + Qt::Key_H), this);
  connect(open_help_shortcut, &QShortcut::activated,
          [this]() { ui->menuHelp->exec(ui->menuBar->mapToGlobal(QPoint(230, 25))); });

  //---------------------------------------------

  QSettings settings;
  updateRecentDataMenu(
      settings.value("MainWindow.recentlyLoadedDatafile").toStringList());
  updateRecentLayoutMenu(
      settings.value("MainWindow.recentlyLoadedLayout").toStringList());
}

void MainWindow::loadAllPlugins(QStringList command_line_plugin_folders)
{
  QSettings settings;
  QStringList loaded;
  QStringList plugin_folders;
  QStringList builtin_folders;

  plugin_folders += command_line_plugin_folders;
  plugin_folders +=
      settings.value("Preferences::plugin_folders", QStringList()).toStringList();

  builtin_folders += QCoreApplication::applicationDirPath();

  try
  {
#ifdef COMPILED_WITH_CATKIN
    builtin_folders += QCoreApplication::applicationDirPath() + "_ros";

    const char* env = std::getenv("CMAKE_PREFIX_PATH");
    if (env)
    {
      QString env_catkin_paths = QString::fromStdString(env);
      env_catkin_paths.replace(";", ":");  // for windows
      auto catkin_paths = env_catkin_paths.split(":");

      for (const auto& path : catkin_paths)
      {
        builtin_folders += path + "/lib/plotjuggler_ros";
      }
    }
#endif
#ifdef COMPILED_WITH_AMENT
    auto ros2_path = QString::fromStdString(ament_index_cpp::get_package_prefix("plotjugg"
                                                                                "ler_"
                                                                                "ros"));
    ros2_path += "/lib/plotjuggler_ros";
    loaded += initializePlugins(ros2_path);
#endif
  }
  catch (...)
  {
    QMessageBox::warning(nullptr, "Missing package [plotjuggler-ros]",
                         "If you just upgraded from PlotJuggler 2.x to 3.x , try "
                         "installing this package:\n\n"
                         "sudo apt install ros-${ROS_DISTRO}-plotjuggler-ros",
                         QMessageBox::Cancel, QMessageBox::Cancel);
  }

  builtin_folders +=
      QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + "/PlotJuggl"
                                                                              "er";
  builtin_folders.removeDuplicates();

  plugin_folders += builtin_folders;
  plugin_folders.removeDuplicates();

  for (const auto& folder : plugin_folders)
  {
    loaded += initializePlugins(folder);
  }

  settings.setValue("Preferences::builtin_plugin_folders", builtin_folders);
}

QStringList MainWindow::initializePlugins(QString directory_name)
{
  static std::set<QString> loaded_plugins;
  QStringList loaded_out;

  qDebug() << "Loading compatible plugins from directory: " << directory_name;
  int loaded_count = 0;

  QDir pluginsDir(directory_name);

  for (const QString& filename : pluginsDir.entryList(QDir::Files))
  {
    QFileInfo fileinfo(filename);
    if (fileinfo.suffix() != "so" && fileinfo.suffix() != "dll" &&
        fileinfo.suffix() != "dylib")
    {
      continue;
    }

    if (loaded_plugins.find(filename) != loaded_plugins.end())
    {
      continue;
    }

    QPluginLoader pluginLoader(pluginsDir.absoluteFilePath(filename), this);

    QObject* plugin = pluginLoader.instance();
    if (plugin && dynamic_cast<PlotJugglerPlugin*>(plugin))
    {
      auto class_name = pluginLoader.metaData().value("className").toString();
      loaded_out.push_back(class_name);

      DataLoader* loader = qobject_cast<DataLoader*>(plugin);
      StatePublisher* publisher = qobject_cast<StatePublisher*>(plugin);
      DataStreamer* streamer = qobject_cast<DataStreamer*>(plugin);
      ParserFactoryPlugin* message_parser = qobject_cast<ParserFactoryPlugin*>(plugin);
      ToolboxPlugin* toolbox = qobject_cast<ToolboxPlugin*>(plugin);

      QString plugin_name;
      QString plugin_type;
      bool is_debug_plugin = dynamic_cast<PlotJugglerPlugin*>(plugin)->isDebugPlugin();

      if (loader)
      {
        plugin_name = loader->name();
        plugin_type = "DataLoader";
      }
      else if (publisher)
      {
        plugin_name = publisher->name();
        plugin_type = "StatePublisher";
      }
      else if (streamer)
      {
        plugin_name = streamer->name();
        plugin_type = "DataStreamer";
      }
      else if (message_parser)
      {
        plugin_name = message_parser->name();
        plugin_type = "MessageParser";
      }
      else if (toolbox)
      {
        plugin_name = toolbox->name();
        plugin_type = "Toolbox";
      }

      QString message = QString("%1 is a %2 plugin").arg(filename).arg(plugin_type);

      if ((_enabled_plugins.size() > 0) &&
          (_enabled_plugins.contains(fileinfo.baseName()) == false))
      {
        qDebug() << message << " ...skipping, because it is not explicitly enabled";
        continue;
      }
      if ((_disabled_plugins.size() > 0) &&
          (_disabled_plugins.contains(fileinfo.baseName()) == true))
      {
        qDebug() << message << " ...skipping, because it is explicitly disabled";
        continue;
      }
      if (!_test_option && is_debug_plugin)
      {
        qDebug() << message << " ...disabled, unless option -t is used";
        continue;
      }
      if (loaded_plugins.find(plugin_name) != loaded_plugins.end())
      {
        qDebug() << message << " ...skipping, because already loaded";
        continue;
      }

      qDebug() << message;

      loaded_plugins.insert(plugin_name);
      loaded_count++;

      if (loader)
      {
        _data_loader.insert(std::make_pair(plugin_name, loader));
      }
      else if (publisher)
      {
        publisher->setDataMap(&_mapped_plot_data);
        _state_publisher.insert(std::make_pair(plugin_name, publisher));

        ui->layoutPublishers->setColumnStretch(0, 1.0);

        int row = _state_publisher.size() - 1;
        auto label = new QLabel(plugin_name, ui->framePublishers);
        ui->layoutPublishers->addWidget(label, row, 0);

        auto start_checkbox = new QCheckBox(ui->framePublishers);
        ui->layoutPublishers->addWidget(start_checkbox, row, 1);
        start_checkbox->setFocusPolicy(Qt::FocusPolicy::NoFocus);

        connect(start_checkbox, &QCheckBox::toggled, this,
                [=](bool enable) { publisher->setEnabled(enable); });

        connect(publisher, &StatePublisher::closed, start_checkbox,
                [=]() { start_checkbox->setChecked(false); });

        if (publisher->availableActions().empty())
        {
          QFrame* empty = new QFrame(ui->framePublishers);
          empty->setFixedSize({ 22, 22 });
          ui->layoutPublishers->addWidget(empty, row, 2);
        }
        else
        {
          auto options_button = new QPushButton(ui->framePublishers);
          options_button->setFlat(true);
          options_button->setFixedSize({ 24, 24 });
          ui->layoutPublishers->addWidget(options_button, row, 2);

          options_button->setIcon(LoadSvg(":/resources/svg/settings_cog.svg", "light"));
          options_button->setIconSize({ 16, 16 });

          auto optionsMenu = [=]() {
            PopupMenu* menu = new PopupMenu(options_button, this);
            for (auto action : publisher->availableActions())
            {
              menu->addAction(action);
            }
            menu->exec();
          };

          connect(options_button, &QPushButton::clicked, options_button, optionsMenu);

          connect(this, &MainWindow::stylesheetChanged, options_button,
                  [=](QString style) {
                    options_button->setIcon(
                        LoadSvg(":/resources/svg/settings_cog.svg", style));
                  });
        }
      }
      else if (message_parser)
      {
        _parser_factories.insert(
            std::make_pair(message_parser->encoding(), message_parser));
      }
      else if (streamer)
      {
        if (_default_streamer == fileinfo.baseName())
        {
          _default_streamer = plugin_name;
        }
        _data_streamer.insert(std::make_pair(plugin_name, streamer));

        connect(streamer, &DataStreamer::closed, this,
                [this]() { this->stopStreamingPlugin(); });

        connect(streamer, &DataStreamer::clearBuffers, this,
                &MainWindow::on_actionClearBuffer_triggered);

        connect(streamer, &DataStreamer::dataReceived, _animated_streaming_movie,
                [this]() {
                  _animated_streaming_movie->start();
                  _animated_streaming_timer->start(500);
                });

        connect(streamer, &DataStreamer::removeGroup, this,
                &MainWindow::on_deleteSerieFromGroup);

        connect(streamer, &DataStreamer::dataReceived, this, [this]() {
          if (isStreamingActive() && !_replot_timer->isActive())
          {
            _replot_timer->setSingleShot(true);
            _replot_timer->start(40);
          }
        });

        connect(streamer, &DataStreamer::notificationsChanged, this,
                &MainWindow::on_streamingNotificationsChanged);
      }
      else if (toolbox)
      {
        toolbox->init(_mapped_plot_data, _transform_functions);

        _toolboxes.insert(std::make_pair(plugin_name, toolbox));

        auto action = ui->menuTools->addAction(toolbox->name());

        int new_index = ui->widgetStack->count();
        auto provided = toolbox->providedWidget();
        auto widget = provided.first;
        ui->widgetStack->addWidget(widget);

        connect(action, &QAction::triggered, toolbox, &ToolboxPlugin::onShowWidget);

        connect(action, &QAction::triggered, this,
                [=]() { ui->widgetStack->setCurrentIndex(new_index); });

        connect(toolbox, &ToolboxPlugin::closed, this,
                [=]() { ui->widgetStack->setCurrentIndex(0); });

        connect(toolbox, &ToolboxPlugin::plotCreated, this, [=](std::string name) {
          _curvelist_widget->addCustom(QString::fromStdString(name));
          _curvelist_widget->updateAppearance();
          _curvelist_widget->clearSelections();
        });
      }
    }
    else
    {
      if (pluginLoader.errorString().contains("is not an ELF object") == false)
      {
        qDebug() << filename << ": " << pluginLoader.errorString();
      }
    }
  }

  for (auto& [name, streamer] : _data_streamer)
  {
    streamer->setParserFactories(&_parser_factories);
  }

  for (auto& [name, loader] : _data_loader)
  {
    loader->setParserFactories(&_parser_factories);
  }

  if (!_data_streamer.empty())
  {
    QSignalBlocker block(ui->comboStreaming);
    ui->comboStreaming->setEnabled(true);
    ui->buttonStreamingStart->setEnabled(true);

    for (const auto& it : _data_streamer)
    {
      if (ui->comboStreaming->findText(it.first) == -1)
      {
        ui->comboStreaming->addItem(it.first);
      }
    }

    // remember the previous one
    QSettings settings;
    QString streaming_name =
        settings
            .value("MainWindow.previousStreamingPlugin", ui->comboStreaming->itemText(0))
            .toString();

    auto streamer_it = _data_streamer.find(streaming_name);
    if (streamer_it == _data_streamer.end())
    {
      streamer_it = _data_streamer.begin();
      streaming_name = streamer_it->first;
    }

    ui->comboStreaming->setCurrentText(streaming_name);

    bool contains_options = !streamer_it->second->availableActions().empty();
    ui->buttonStreamingOptions->setEnabled(contains_options);
  }
  qDebug() << "Number of plugins loaded: " << loaded_count << "\n";
  return loaded_out;
}

void MainWindow::buildDummyData()
{
  PlotDataMapRef datamap;
  BuildDummyData(datamap);
  importPlotDataMap(datamap, true);
}

void MainWindow::on_splitterMoved(int, int)
{
  QList<int> sizes = ui->mainSplitter->sizes();
  int max_left_size = _curvelist_widget->maximumWidth();
  int totalWidth = sizes[0] + sizes[1];

  // this is needed only once to restore the old size
  static bool first = true;
  if (sizes[0] != 0 && first)
  {
    first = false;
    QSettings settings;
    int splitter_width = settings.value("MainWindow.splitterWidth", 200).toInt();
    auto sizes = ui->mainSplitter->sizes();
    int tot_splitter_width = sizes[0] + sizes[1];
    sizes[0] = splitter_width;
    sizes[1] = tot_splitter_width - splitter_width;
    ui->mainSplitter->setSizes(sizes);
    return;
  }

  if (sizes[0] > max_left_size)
  {
    sizes[0] = max_left_size;
    sizes[1] = totalWidth - max_left_size;
    ui->mainSplitter->setSizes(sizes);
  }
}

void MainWindow::resizeEvent(QResizeEvent*)
{
  on_splitterMoved(0, 0);
}

void MainWindow::onPlotAdded(PlotWidget* plot)
{
  connect(plot, &PlotWidget::undoableChange, this, &MainWindow::onUndoableChange);

  connect(plot, &PlotWidget::trackerMoved, this, &MainWindow::onTrackerMovedFromWidget);

  connect(this, &MainWindow::dataSourceRemoved, plot, &PlotWidget::onDataSourceRemoved);

  connect(plot, &PlotWidget::curveListChanged, this, [this]() {
    updateTimeOffset();
    updateTimeSlider();
  });

  connect(&_time_offset, SIGNAL(valueChanged(double)), plot,
          SLOT(on_changeTimeOffset(double)));

  connect(ui->pushButtonUseDateTime, &QPushButton::toggled, plot,
          &PlotWidget::on_changeDateTimeScale);

  connect(plot, &PlotWidget::curvesDropped, _curvelist_widget,
          &CurveListPanel::clearSelections);

  connect(plot, &PlotWidget::legendSizeChanged, this, [=](int point_size) {
    auto visitor = [=](PlotWidget* p) {
      if (plot != p)
        p->setLegendSize(point_size);
    };
    this->forEachWidget(visitor);
  });

  connect(plot, &PlotWidget::rectChanged, this, &MainWindow::onPlotZoomChanged);

  plot->on_changeTimeOffset(_time_offset.get());
  plot->on_changeDateTimeScale(ui->pushButtonUseDateTime->isChecked());
  plot->activateGrid(ui->pushButtonActivateGrid->isChecked());
  plot->enableTracker(!isStreamingActive());
  plot->setKeepRatioXY(ui->pushButtonRatio->isChecked());
  plot->configureTracker(_tracker_param);
}

void MainWindow::onPlotZoomChanged(PlotWidget* modified_plot, QRectF new_range)
{
  if (ui->pushButtonLink->isChecked())
  {
    auto visitor = [=](PlotWidget* plot) {
      if (plot != modified_plot && !plot->isEmpty() && !plot->isXYPlot() &&
          plot->isZoomLinkEnabled())
      {
        QRectF bound_act = plot->currentBoundingRect();
        bound_act.setLeft(new_range.left());
        bound_act.setRight(new_range.right());
        plot->setZoomRectangle(bound_act, false);
        plot->on_zoomOutVertical_triggered(false);
        plot->replot();
      }
    };
    this->forEachWidget(visitor);
  }

  onUndoableChange();
}

void MainWindow::onPlotTabAdded(PlotDocker* docker)
{
  connect(docker, &PlotDocker::plotWidgetAdded, this, &MainWindow::onPlotAdded);

  connect(this, &MainWindow::stylesheetChanged, docker,
          &PlotDocker::on_stylesheetChanged);

  // TODO  connect(matrix, &PlotMatrix::undoableChange, this,
  // &MainWindow::onUndoableChange);
}

QDomDocument MainWindow::xmlSaveState() const
{
  QDomDocument doc;
  QDomProcessingInstruction instr = doc.createProcessingInstruction("xml", "version='1.0'"
                                                                           " encoding='"
                                                                           "UTF-8'");

  doc.appendChild(instr);

  QDomElement root = doc.createElement("root");

  for (auto& it : TabbedPlotWidget::instances())
  {
    QDomElement tabbed_area = it.second->xmlSaveState(doc);
    root.appendChild(tabbed_area);
  }

  doc.appendChild(root);

  QDomElement relative_time = doc.createElement("use_relative_time_offset");
  relative_time.setAttribute("enabled", ui->pushButtonRemoveTimeOffset->isChecked());
  root.appendChild(relative_time);

  return doc;
}

void MainWindow::checkAllCurvesFromLayout(const QDomElement& root)
{
  std::set<std::string> curves;

  for (QDomElement tw = root.firstChildElement("tabbed_widget"); !tw.isNull();
       tw = tw.nextSiblingElement("tabbed_widget"))
  {
    for (QDomElement pm = tw.firstChildElement("plotmatrix"); !pm.isNull();
         pm = pm.nextSiblingElement("plotmatrix"))
    {
      for (QDomElement pl = pm.firstChildElement("plot"); !pl.isNull();
           pl = pl.nextSiblingElement("plot"))
      {
        QDomElement tran_elem = pl.firstChildElement("transform");
        std::string trans = tran_elem.attribute("value").toStdString();
        bool is_XY_plot = (trans == "XYPlot");

        for (QDomElement cv = pl.firstChildElement("curve"); !cv.isNull();
             cv = cv.nextSiblingElement("curve"))
        {
          if (is_XY_plot)
          {
            curves.insert(cv.attribute("curve_x").toStdString());
            curves.insert(cv.attribute("curve_y").toStdString());
          }
          else
          {
            curves.insert(cv.attribute("name").toStdString());
          }
        }
      }
    }
  }

  std::vector<std::string> missing_curves;

  for (auto& curve_name : curves)
  {
    if (_mapped_plot_data.numeric.count(curve_name) == 0)
    {
      missing_curves.push_back(curve_name);
    }
    if (_mapped_plot_data.strings.count(curve_name) == 0)
    {
      missing_curves.push_back(curve_name);
    }
  }
  if (missing_curves.size() > 0)
  {
    QMessageBox msgBox(this);
    msgBox.setWindowTitle("Warning");
    msgBox.setText(tr("One or more timeseries in the layout haven't been loaded yet\n"
                      "What do you want to do?"));

    QPushButton* buttonRemove =
        msgBox.addButton(tr("Remove curves from plots"), QMessageBox::RejectRole);
    QPushButton* buttonPlaceholder =
        msgBox.addButton(tr("Create empty placeholders"), QMessageBox::YesRole);
    msgBox.setDefaultButton(buttonPlaceholder);
    msgBox.exec();
    if (msgBox.clickedButton() == buttonPlaceholder)
    {
      for (auto& name : missing_curves)
      {
        auto plot_it = _mapped_plot_data.addNumeric(name);
        _curvelist_widget->addCurve(name);
      }
      _curvelist_widget->refreshColumns();
    }
  }
}

bool MainWindow::xmlLoadState(QDomDocument state_document)
{
  QDomElement root = state_document.namedItem("root").toElement();
  if (root.isNull())
  {
    qWarning() << "No <root> element found at the top-level of the XML file!";
    return false;
  }

  size_t num_floating = 0;
  std::map<QString, QDomElement> tabbed_widgets_with_name;

  for (QDomElement tw = root.firstChildElement("tabbed_widget"); tw.isNull() == false;
       tw = tw.nextSiblingElement("tabbed_widget"))
  {
    if (tw.attribute("parent") != ("main_window"))
    {
      num_floating++;
    }
    tabbed_widgets_with_name[tw.attribute("name")] = tw;
  }

  // add if missing
  for (const auto& it : tabbed_widgets_with_name)
  {
    if (TabbedPlotWidget::instance(it.first) == nullptr)
    {
      // TODO createTabbedDialog(it.first, nullptr);
    }
  }

  // remove those which don't share list of names
  for (const auto& it : TabbedPlotWidget::instances())
  {
    if (tabbed_widgets_with_name.count(it.first) == 0)
    {
      it.second->deleteLater();
    }
  }

  //-----------------------------------------------------
  checkAllCurvesFromLayout(root);
  //-----------------------------------------------------

  for (QDomElement tw = root.firstChildElement("tabbed_widget"); tw.isNull() == false;
       tw = tw.nextSiblingElement("tabbed_widget"))
  {
    TabbedPlotWidget* tabwidget = TabbedPlotWidget::instance(tw.attribute("name"));
    tabwidget->xmlLoadState(tw);
  }

  QDomElement relative_time = root.firstChildElement("use_relative_time_offset");
  if (!relative_time.isNull())
  {
    bool remove_offset = (relative_time.attribute("enabled") == QString("1"));
    ui->pushButtonRemoveTimeOffset->setChecked(remove_offset);
  }
  return true;
}

void MainWindow::onDeleteMultipleCurves(const std::vector<std::string>& curve_names)
{
  std::set<std::string> to_be_deleted;
  for (auto& name : curve_names)
  {
    to_be_deleted.insert(name);
  }
  // add to the list of curves to delete the derived transforms
  size_t prev_size = 0;
  while (prev_size < to_be_deleted.size())
  {
    prev_size = to_be_deleted.size();
    for (auto& [trans_name, transform] : _transform_functions)
    {
      for (const auto& source : transform->dataSources())
      {
        if (to_be_deleted.count(source->plotName()) > 0)
        {
          to_be_deleted.insert(trans_name);
        }
      }
    }
  }

  for (const auto& curve_name : to_be_deleted)
  {
    emit dataSourceRemoved(curve_name);
    _curvelist_widget->removeCurve(curve_name);
    _mapped_plot_data.erase(curve_name);
    _transform_functions.erase(curve_name);
  }
  updateTimeOffset();
  forEachWidget([](PlotWidget* plot) { plot->replot(); });
}

void MainWindow::updateRecentDataMenu(QStringList new_filenames)
{
  QMenu* menu = _recent_data_files;

  QAction* separator = nullptr;
  QStringList prev_filenames;
  for (QAction* action : menu->actions())
  {
    if (action->isSeparator())
    {
      separator = action;
      break;
    }
    if (new_filenames.contains(action->text()) == false)
    {
      prev_filenames.push_back(action->text());
    }
    menu->removeAction(action);
  }

  new_filenames.append(prev_filenames);
  while (new_filenames.size() > 10)
  {
    new_filenames.removeLast();
  }

  for (const auto& filename : new_filenames)
  {
    QAction* action = new QAction(filename, nullptr);
    connect(action, &QAction::triggered, this,
            [this, filename] { loadDataFromFiles({ filename }); });
    menu->insertAction(separator, action);
  }

  QSettings settings;
  settings.setValue("MainWindow.recentlyLoadedDatafile", new_filenames);
  menu->setEnabled(new_filenames.size() > 0);
}

void MainWindow::updateRecentLayoutMenu(QStringList new_filenames)
{
  QMenu* menu = _recent_layout_files;

  QAction* separator = nullptr;
  QStringList prev_filenames;
  for (QAction* action : menu->actions())
  {
    if (action->isSeparator())
    {
      separator = action;
      break;
    }
    if (new_filenames.contains(action->text()) == false)
    {
      prev_filenames.push_back(action->text());
    }
    menu->removeAction(action);
  }

  new_filenames.append(prev_filenames);
  while (new_filenames.size() > 10)
  {
    new_filenames.removeLast();
  }

  for (const auto& filename : new_filenames)
  {
    QAction* action = new QAction(filename, nullptr);
    connect(action, &QAction::triggered, this, [this, filename] {
      if (this->loadLayoutFromFile(filename))
      {
        updateRecentLayoutMenu({ filename });
      }
    });
    menu->insertAction(separator, action);
  }

  QSettings settings;
  settings.setValue("MainWindow.recentlyLoadedLayout", new_filenames);
  menu->setEnabled(new_filenames.size() > 0);
}

void MainWindow::deleteAllData()
{
  forEachWidget([](PlotWidget* plot) { plot->removeAllCurves(); });

  _mapped_plot_data.clear();
  _transform_functions.clear();
  _curvelist_widget->clear();
  _loaded_datafiles.clear();
  _undo_states.clear();
  _redo_states.clear();

  bool stopped = false;

  for (int idx = 0; idx < ui->layoutPublishers->count(); idx++)
  {
    QLayoutItem* item = ui->layoutPublishers->itemAt(idx);
    if (dynamic_cast<QWidgetItem*>(item))
    {
      if (auto checkbox = dynamic_cast<QCheckBox*>(item->widget()))
      {
        if (checkbox->isChecked())
        {
          checkbox->setChecked(false);
          stopped = true;
        }
      }
    }
  }

  if (stopped)
  {
    QMessageBox::warning(this, "State publishers stopped",
                         "All the state publishers have been stopped because old data "
                         "has been deleted.");
  }
}

void MainWindow::importPlotDataMap(PlotDataMapRef& new_data, bool remove_old)
{
  if (remove_old)
  {
    auto ClearOldSeries = [](auto& prev_plot_data, auto& new_plot_data) {
      for (auto& it : prev_plot_data)
      {
        // timeseries in both
        if (new_plot_data.count(it.first) != 0)
        {
          it.second.clear();
        }
      }
    };

    ClearOldSeries(_mapped_plot_data.scatter_xy, new_data.scatter_xy);
    ClearOldSeries(_mapped_plot_data.numeric, new_data.numeric);
    ClearOldSeries(_mapped_plot_data.strings, new_data.strings);
  }

  auto [added_curves, curve_updated, data_pushed] =
      MoveData(new_data, _mapped_plot_data, remove_old);

  for (const auto& added_curve : added_curves)
  {
    _curvelist_widget->addCurve(added_curve);
  }

  if (curve_updated)
  {
    _curvelist_widget->refreshColumns();
  }
}

bool MainWindow::isStreamingActive() const
{
  return !ui->buttonStreamingPause->isChecked() && _active_streamer_plugin;
}

bool MainWindow::loadDataFromFiles(QStringList filenames)
{
  filenames.sort();
  std::map<QString, QString> filename_prefix;

  if (filenames.size() > 1)
  {
    DialogMultifilePrefix dialog(filenames, this);
    int ret = dialog.exec();
    if (ret != QDialog::Accepted)
    {
      return false;
    }
    filename_prefix = dialog.getPrefixes();
  }

  std::unordered_set<std::string> previous_names = _mapped_plot_data.getAllNames();

  QStringList loaded_filenames;

  for (int i = 0; i < filenames.size(); i++)
  {
    FileLoadInfo info;
    info.filename = filenames[i];
    if (filename_prefix.count(info.filename) > 0)
    {
      info.prefix = filename_prefix[info.filename];
    }
    auto added_names = loadDataFromFile(info);
    if (!added_names.empty())
    {
      loaded_filenames.push_back(filenames[i]);
    }
    for (const auto& name : added_names)
    {
      previous_names.erase(name);
    }
  }

  bool data_replaced_entirely = false;

  if (previous_names.empty())
  {
    data_replaced_entirely = true;
  }
  else
  {
    QMessageBox::StandardButton reply;
    reply = QMessageBox::question(
        this, tr("Warning"), tr("Do you want to remove the previously loaded data?\n"),
        QMessageBox::Yes | QMessageBox::No, QMessageBox::NoButton);

    if (reply == QMessageBox::Yes)
    {
      std::vector<std::string> to_delete;
      for (const auto& name : previous_names)
      {
        to_delete.push_back(name);
      }
      onDeleteMultipleCurves(to_delete);
      data_replaced_entirely = true;
    }
  }

  // special case when only the last file should be remembered
  if (loaded_filenames.size() == 1 && data_replaced_entirely &&
      _loaded_datafiles.size() > 1)
  {
    std::swap(_loaded_datafiles.back(), _loaded_datafiles.front());
    _loaded_datafiles.resize(1);
  }

  if (loaded_filenames.size() > 0)
  {
    updateRecentDataMenu(loaded_filenames);
    linkedZoomOut();
    return true;
  }
  return false;
}

std::unordered_set<std::string> MainWindow::loadDataFromFile(const FileLoadInfo& info)
{
  ui->pushButtonPlay->setChecked(false);

  const QString extension = QFileInfo(info.filename).suffix().toLower();

  typedef std::map<QString, DataLoaderPtr>::iterator MapIterator;

  std::vector<MapIterator> compatible_loaders;

  for (auto it = _data_loader.begin(); it != _data_loader.end(); ++it)
  {
    DataLoaderPtr data_loader = it->second;
    std::vector<const char*> extensions = data_loader->compatibleFileExtensions();

    for (auto& ext : extensions)
    {
      if (extension == QString(ext).toLower())
      {
        compatible_loaders.push_back(it);
        break;
      }
    }
  }

  DataLoaderPtr dataloader;
  std::unordered_set<std::string> added_names;

  if (compatible_loaders.size() == 1)
  {
    dataloader = compatible_loaders.front()->second;
  }
  else
  {
    static QString last_plugin_name_used;

    QStringList names;
    for (auto& cl : compatible_loaders)
    {
      const auto& name = cl->first;

      if (name == last_plugin_name_used)
      {
        names.push_front(name);
      }
      else
      {
        names.push_back(name);
      }
    }

    bool ok;
    QString plugin_name =
        QInputDialog::getItem(this, tr("QInputDialog::getItem()"),
                              tr("Select the loader to use:"), names, 0, false, &ok);
    if (ok && !plugin_name.isEmpty() && (_enabled_plugins.size() == 0 || _enabled_plugins.contains(plugin_name)))
    {
      dataloader = _data_loader[plugin_name];
      last_plugin_name_used = plugin_name;
    }
  }

  if (dataloader)
  {
    QFile file(info.filename);

    if (!file.open(QFile::ReadOnly | QFile::Text))
    {
      QMessageBox::warning(
          this, tr("Datafile"),
          tr("Cannot read file %1:\n%2.").arg(info.filename).arg(file.errorString()));
      return {};
    }
    file.close();

    try
    {
      PlotDataMapRef mapped_data;
      FileLoadInfo new_info = info;

      if (dataloader->readDataFromFile(&new_info, mapped_data))
      {
        AddPrefixToPlotData(info.prefix.toStdString(), mapped_data.numeric);
        AddPrefixToPlotData(info.prefix.toStdString(), mapped_data.strings);

        added_names = mapped_data.getAllNames();
        importPlotDataMap(mapped_data, true);

        QDomElement plugin_elem = dataloader->xmlSaveState(new_info.plugin_config);
        new_info.plugin_config.appendChild(plugin_elem);

        bool duplicate = false;

        // substitute an old item of _loaded_datafiles or push_back another item.
        for (auto& prev_loaded : _loaded_datafiles)
        {
          if (prev_loaded.filename == new_info.filename &&
              prev_loaded.prefix == new_info.prefix)
          {
            prev_loaded = new_info;
            duplicate = true;
            break;
          }
        }

        if (!duplicate)
        {
          _loaded_datafiles.push_back(new_info);
        }
      }
    }
    catch (std::exception& ex)
    {
      QMessageBox::warning(this, tr("Exception from the plugin"),
                           tr("The plugin [%1] thrown the following exception: \n\n %3\n")
                               .arg(dataloader->name())
                               .arg(ex.what()));
      return {};
    }
  }
  else
  {
    QMessageBox::warning(this, tr("Error"),
                         tr("Cannot read files with extension %1.\n No plugin can handle "
                            "that!\n")
                             .arg(info.filename));
  }
  _curvelist_widget->updateFilter();

  // clean the custom plot. Function updateDataAndReplot will update them
  for (auto& custom_it : _transform_functions)
  {
    auto it = _mapped_plot_data.numeric.find(custom_it.first);
    if (it != _mapped_plot_data.numeric.end())
    {
      it->second.clear();
    }
    custom_it.second->reset();
  }
  forEachWidget([](PlotWidget* plot) { plot->updateCurves(true); });

  updateDataAndReplot(true);
  ui->timeSlider->setRealValue(ui->timeSlider->getMinimum());

  return added_names;
}

void MainWindow::on_buttonStreamingNotifications_clicked()
{
  if (_data_streamer.empty())
  {
    return;
  }
  auto streamer = _data_streamer.at(ui->comboStreaming->currentText());
  QAction* notification_button_action = streamer->notificationAction().first;
  if (notification_button_action != nullptr)
  {
    notification_button_action->trigger();
  }
}

void MainWindow::on_buttonStreamingPause_toggled(bool paused)
{
  if (!_active_streamer_plugin)
  {
    paused = true;
  }

  ui->pushButtonRemoveTimeOffset->setEnabled(paused);
  ui->widgetPlay->setEnabled(paused);

  if (!paused && ui->pushButtonPlay->isChecked())
  {
    ui->pushButtonPlay->setChecked(false);
  }

  forEachWidget([&](PlotWidget* plot) {
    plot->enableTracker(paused);
    plot->setZoomEnabled(paused);
  });

  if (!paused)
  {
    updateTimeOffset();
  }
  else
  {
    onUndoableChange();
  }
}

void MainWindow::on_streamingToggled()
{
  if (_active_streamer_plugin)
  {
    bool prev_state = ui->buttonStreamingPause->isChecked();
    ui->buttonStreamingPause->setChecked(!prev_state);
  }
}

void MainWindow::stopStreamingPlugin()
{
  ui->comboStreaming->setEnabled(true);
  ui->buttonStreamingStart->setText("Start");
  ui->buttonStreamingPause->setEnabled(false);
  ui->labelStreamingAnimation->setHidden(true);
  enableStreamingNotificationsButton(false);

  // force the cleanups typically done in on_buttonStreamingPause_toggled
  if (ui->buttonStreamingPause->isChecked())
  {
    // Will call on_buttonStreamingPause_toggled
    ui->buttonStreamingPause->setChecked(false);
  }
  else
  {
    // call it manually
    on_buttonStreamingPause_toggled(true);
  }

  if (_active_streamer_plugin)
  {
    _active_streamer_plugin->shutdown();
    _active_streamer_plugin = nullptr;
  }

  if (!_mapped_plot_data.numeric.empty())
  {
    ui->actionDeleteAllData->setToolTip("");
  }

  // reset max range.
  _mapped_plot_data.setMaximumRangeX(std::numeric_limits<double>::max());
}

void MainWindow::startStreamingPlugin(QString streamer_name)
{
  if (_active_streamer_plugin)
  {
    _active_streamer_plugin->shutdown();
    _active_streamer_plugin = nullptr;
  }

  if (_data_streamer.empty())
  {
    qDebug() << "Error, no streamer loaded";
    return;
  }

  auto it = _data_streamer.find(streamer_name);
  if (it != _data_streamer.end())
  {
    _active_streamer_plugin = it->second;
  }
  else
  {
    qDebug() << "Error. The streamer " << streamer_name << " can't be loaded";
    _active_streamer_plugin = nullptr;
    return;
  }

  bool started = false;
  try
  {
    // TODO data sources (argument to _active_streamer_plugin->start()
    started = _active_streamer_plugin && _active_streamer_plugin->start(nullptr);
  }
  catch (std::runtime_error& err)
  {
    QMessageBox::warning(
        this, tr("Exception from the plugin"),
        tr("The plugin thrown the following exception: \n\n %1\n").arg(err.what()));
    _active_streamer_plugin = nullptr;
    return;
  }

  // The attemp to start the plugin may have succeded or failed
  if (started)
  {
    {
      std::lock_guard<std::mutex> lock(_active_streamer_plugin->mutex());
      importPlotDataMap(_active_streamer_plugin->dataMap(), false);
    }

    ui->actionClearBuffer->setEnabled(true);
    ui->actionDeleteAllData->setToolTip("Stop streaming to be able to delete the data");

    ui->buttonStreamingStart->setText("Stop");
    ui->buttonStreamingPause->setEnabled(true);
    ui->buttonStreamingPause->setChecked(false);
    ui->comboStreaming->setEnabled(false);
    ui->labelStreamingAnimation->setHidden(false);

    // force start
    on_buttonStreamingPause_toggled(false);
    // this will force the update the max buffer size values
    on_streamingSpinBox_valueChanged(ui->streamingSpinBox->value());
  }
  else
  {
    QSignalBlocker block(ui->buttonStreamingStart);
    ui->buttonStreamingStart->setChecked(false);
    qDebug() << "Failed to launch the streamer";
    _active_streamer_plugin = nullptr;
  }
}

void MainWindow::enableStreamingNotificationsButton(bool enabled)
{
  ui->buttonStreamingNotifications->setEnabled(enabled);

  QSettings settings;
  QString theme = settings.value("Preferences::theme", "light").toString();

  if (enabled)
  {
    ui->buttonStreamingNotifications->setIcon(
        LoadSvg(":/resources/svg/alarm-bell-active.svg", theme));
  }
  else
  {
    ui->buttonStreamingNotifications->setIcon(
        LoadSvg(":/resources/svg/alarm-bell.svg", theme));
  }
}

void MainWindow::loadStyleSheet(QString file_path)
{
  QFile styleFile(file_path);
  styleFile.open(QFile::ReadOnly);
  try
  {
    QString theme = SetApplicationStyleSheet(styleFile.readAll());

    forEachWidget([&](PlotWidget* plot) { plot->replot(); });

    _curvelist_widget->updateAppearance();
    emit stylesheetChanged(theme);
  }
  catch (std::runtime_error& err)
  {
    QMessageBox::warning(this, tr("Error loading StyleSheet"), tr(err.what()));
  }
}

void MainWindow::updateDerivedSeries()
{
  for (auto& [id, series] : _transform_functions)
  {
  }
}

void MainWindow::updateReactivePlots()
{
  std::unordered_set<std::string> updated_curves;

  bool curve_added = false;
  for (auto& it : _transform_functions)
  {
    if (auto reactive_function =
            std::dynamic_pointer_cast<PJ::ReactiveLuaFunction>(it.second))
    {
      reactive_function->setTimeTracker(_tracker_time);
      reactive_function->calculate();

      for (auto& name : reactive_function->createdCurves())
      {
        curve_added |= _curvelist_widget->addCurve(name);
        updated_curves.insert(name);
      }
    }
  }
  if (curve_added)
  {
    _curvelist_widget->refreshColumns();
  }

  forEachWidget([&](PlotWidget* plot) {
    for (auto& curve : plot->curveList())
    {
      if (updated_curves.count(curve.src_name) != 0)
      {
        plot->replot();
      }
    }
  });
}

void MainWindow::dragEnterEvent(QDragEnterEvent* event)
{
  if (event->mimeData()->hasUrls())
  {
    event->acceptProposedAction();
  }
}

void MainWindow::dropEvent(QDropEvent* event)
{
  QStringList file_names;
  const auto urls = event->mimeData()->urls();

  for (const auto& url : urls)
  {
    file_names << QDir::toNativeSeparators(url.toLocalFile());
  }

  loadDataFromFiles(file_names);
}

void MainWindow::on_stylesheetChanged(QString theme)
{
  ui->pushButtonLoadDatafile->setIcon(LoadSvg(":/resources/svg/import.svg", theme));
  ui->buttonStreamingPause->setIcon(LoadSvg(":/resources/svg/pause.svg", theme));
  if (ui->buttonStreamingNotifications->isEnabled())
  {
    ui->buttonStreamingNotifications->setIcon(
        LoadSvg(":/resources/svg/alarm-bell-active.svg", theme));
  }
  else
  {
    ui->buttonStreamingNotifications->setIcon(
        LoadSvg(":/resources/svg/alarm-bell.svg", theme));
  }
  ui->buttonRecentData->setIcon(LoadSvg(":/resources/svg/right-arrow.svg", theme));
  ui->buttonRecentLayout->setIcon(LoadSvg(":/resources/svg/right-arrow.svg", theme));

  ui->pushButtonZoomOut->setIcon(LoadSvg(":/resources/svg/zoom_max.svg", theme));
  ui->playbackLoop->setIcon(LoadSvg(":/resources/svg/loop.svg", theme));
  ui->pushButtonPlay->setIcon(LoadSvg(":/resources/svg/play_arrow.svg", theme));
  ui->pushButtonUseDateTime->setIcon(LoadSvg(":/resources/svg/datetime.svg", theme));
  ui->pushButtonActivateGrid->setIcon(LoadSvg(":/resources/svg/grid.svg", theme));
  ui->pushButtonRatio->setIcon(LoadSvg(":/resources/svg/ratio.svg", theme));

  ui->pushButtonLoadLayout->setIcon(LoadSvg(":/resources/svg/import.svg", theme));
  ui->pushButtonSaveLayout->setIcon(LoadSvg(":/resources/svg/export.svg", theme));

  ui->pushButtonLink->setIcon(LoadSvg(":/resources/svg/link.svg", theme));
  ui->pushButtonRemoveTimeOffset->setIcon(LoadSvg(":/resources/svg/t0.svg", theme));
  ui->pushButtonLegend->setIcon(LoadSvg(":/resources/svg/legend.svg", theme));

  ui->buttonStreamingOptions->setIcon(LoadSvg(":/resources/svg/settings_cog.svg", theme));
}

void MainWindow::loadPluginState(const QDomElement& root)
{
  QDomElement plugins = root.firstChildElement("Plugins");

  for (QDomElement plugin_elem = plugins.firstChildElement();
       plugin_elem.isNull() == false; plugin_elem = plugin_elem.nextSiblingElement())
  {
    const QString plugin_name = plugin_elem.attribute("ID");

    if (plugin_elem.nodeName() != "plugin" || plugin_name.isEmpty())
    {
      QMessageBox::warning(this, tr("Error loading Plugin State from Layout"),
                           tr("The method xmlSaveState() must return a node like this "
                              "<plugin ID=\"PluginName\" "));
    }

    if (_data_loader.find(plugin_name) != _data_loader.end())
    {
      _data_loader[plugin_name]->xmlLoadState(plugin_elem);
    }
    if (_data_streamer.find(plugin_name) != _data_streamer.end())
    {
      _data_streamer[plugin_name]->xmlLoadState(plugin_elem);
    }
    if (_toolboxes.find(plugin_name) != _toolboxes.end())
    {
      _toolboxes[plugin_name]->xmlLoadState(plugin_elem);
    }
    if (_state_publisher.find(plugin_name) != _state_publisher.end())
    {
      StatePublisherPtr publisher = _state_publisher[plugin_name];
      publisher->xmlLoadState(plugin_elem);

      if (_autostart_publishers && plugin_elem.attribute("status") == "active")
      {
        publisher->setEnabled(true);
      }
    }
  }
}

QDomElement MainWindow::savePluginState(QDomDocument& doc)
{
  QDomElement list_plugins = doc.createElement("Plugins");

  auto AddPlugins = [&](auto& plugins) {
    for (auto& [name, plugin] : plugins)
    {
      QDomElement elem = plugin->xmlSaveState(doc);
      list_plugins.appendChild(elem);
    }
  };

  AddPlugins(_data_loader);
  AddPlugins(_data_streamer);
  AddPlugins(_toolboxes);
  AddPlugins(_state_publisher);

  for (auto& it : _state_publisher)
  {
    const auto& state_publisher = it.second;
    QDomElement plugin_elem = state_publisher->xmlSaveState(doc);
    plugin_elem.setAttribute("status", state_publisher->enabled() ? "active" : "idle");
  }

  return list_plugins;
}

std::tuple<double, double, int> MainWindow::calculateVisibleRangeX()
{
  // find min max time
  double min_time = std::numeric_limits<double>::max();
  double max_time = std::numeric_limits<double>::lowest();
  int max_steps = 0;

  forEachWidget([&](const PlotWidget* widget) {
    for (auto& it : widget->curveList())
    {
      const auto& curve_name = it.src_name;

      auto plot_it = _mapped_plot_data.numeric.find(curve_name);
      if (plot_it == _mapped_plot_data.numeric.end())
      {
        continue;  // FIXME?
      }
      const auto& data = plot_it->second;
      if (data.size() >= 1)
      {
        const double t0 = data.front().x;
        const double t1 = data.back().x;
        min_time = std::min(min_time, t0);
        max_time = std::max(max_time, t1);
        max_steps = std::max(max_steps, (int)data.size());
      }
    }
  });

  // needed if all the plots are empty
  if (max_steps == 0 || max_time < min_time)
  {
    for (const auto& it : _mapped_plot_data.numeric)
    {
      const PlotData& data = it.second;
      if (data.size() >= 1)
      {
        const double t0 = data.front().x;
        const double t1 = data.back().x;
        min_time = std::min(min_time, t0);
        max_time = std::max(max_time, t1);
        max_steps = std::max(max_steps, (int)data.size());
      }
    }
  }

  // last opportunity. Everything else failed
  if (max_steps == 0 || max_time < min_time)
  {
    min_time = 0.0;
    max_time = 1.0;
    max_steps = 1;
  }
  return std::tuple<double, double, int>(min_time, max_time, max_steps);
}

bool MainWindow::loadLayoutFromFile(QString filename)
{
  QSettings settings;

  QFile file(filename);
  if (!file.open(QFile::ReadOnly | QFile::Text))
  {
    QMessageBox::warning(
        this, tr("Layout"),
        tr("Cannot read file %1:\n%2.").arg(filename).arg(file.errorString()));
    return false;
  }

  QString errorStr;
  int errorLine, errorColumn;

  QDomDocument domDocument;

  if (!domDocument.setContent(&file, true, &errorStr, &errorLine, &errorColumn))
  {
    QMessageBox::information(
        window(), tr("XML Layout"),
        tr("Parse error at line %1:\n%2").arg(errorLine).arg(errorStr));
    return false;
  }

  //-------------------------------------------------
  // refresh plugins
  QDomElement root = domDocument.namedItem("root").toElement();

  loadPluginState(root);
  //-------------------------------------------------
  QDomElement previously_loaded_datafile = root.firstChildElement("previouslyLoaded_"
                                                                  "Datafiles");

  QDomElement datafile_elem = previously_loaded_datafile.firstChildElement("fileInfo");
  while (!datafile_elem.isNull())
  {
    QString datafile_path = datafile_elem.attribute("filename");
    if (QDir(datafile_path).isRelative())
    {
      QDir layout_directory = QFileInfo(filename).absoluteDir();
      QString new_path = layout_directory.filePath(datafile_path);
      datafile_path = QFileInfo(new_path).absoluteFilePath();
    }

    FileLoadInfo info;
    info.filename = datafile_path;
    info.prefix = datafile_elem.attribute("prefix");

    QDomElement datasources_elem = datafile_elem.firstChildElement("selected_"
                                                                   "datasources");
    QString topics_list = datasources_elem.attribute("value");
    info.selected_datasources = topics_list.split(";", QString::SkipEmptyParts);

    auto plugin_elem = datafile_elem.firstChildElement("plugin");
    info.plugin_config.appendChild(info.plugin_config.importNode(plugin_elem, true));

    loadDataFromFile(info);
    datafile_elem = datafile_elem.nextSiblingElement("fileInfo");
  }

  QDomElement previous_streamer = root.firstChildElement("previouslyLoaded_Streamer");
  if (!previous_streamer.isNull())
  {
    QString streamer_name = previous_streamer.attribute("name");

    QMessageBox msgBox(this);
    msgBox.setWindowTitle("Start Streaming?");
    msgBox.setText(
        tr("Start the previously used streaming plugin?\n\n %1 \n\n").arg(streamer_name));
    QPushButton* yes = msgBox.addButton(tr("Yes"), QMessageBox::YesRole);
    QPushButton* no = msgBox.addButton(tr("No"), QMessageBox::RejectRole);
    msgBox.setDefaultButton(yes);
    msgBox.exec();

    if (msgBox.clickedButton() == yes)
    {
      if (_data_streamer.count(streamer_name) != 0)
      {
        auto allCurves = readAllCurvesFromXML(root);

        // create placeholders, if necessary
        for (auto curve_name : allCurves)
        {
          std::string curve_str = curve_name.toStdString();
          if (_mapped_plot_data.numeric.count(curve_str) == 0)
          {
            _mapped_plot_data.addNumeric(curve_str);
          }
        }

        startStreamingPlugin(streamer_name);
      }
      else
      {
        QMessageBox::warning(
            this, tr("Error Loading Streamer"),
            tr("The streamer named %1 can not be loaded.").arg(streamer_name));
      }
    }
  }
  //-------------------------------------------------
  // autostart_publishers
  QDomElement plugins = root.firstChildElement("Plugins");

  if (!plugins.isNull() && _autostart_publishers)
  {
    for (QDomElement plugin_elem = plugins.firstChildElement();
         plugin_elem.isNull() == false; plugin_elem = plugin_elem.nextSiblingElement())
    {
      const QString plugin_name = plugin_elem.nodeName();
      if (_state_publisher.find(plugin_name) != _state_publisher.end())
      {
        StatePublisherPtr publisher = _state_publisher[plugin_name];

        if (plugin_elem.attribute("status") == "active")
        {
          publisher->setEnabled(true);
        }
      }
    }
  }
  //-------------------------------------------------
  auto custom_equations = root.firstChildElement("customMathEquations");

  if (!custom_equations.isNull())
  {
    using SnippetPair = std::pair<SnippetData, QDomElement>;
    std::vector<SnippetPair> snippets;

    for (QDomElement custom_eq = custom_equations.firstChildElement("snippet");
         custom_eq.isNull() == false; custom_eq = custom_eq.nextSiblingElement("snippet"))
    {
      snippets.push_back({ GetSnippetFromXML(custom_eq), custom_eq });
    }
    // A custom plot may depend on other custom plots.
    // Reorder them to respect the mutual depencency.
    auto DependOn = [](const SnippetPair& a, const SnippetPair& b) {
      if (b.first.linked_source == a.first.alias_name)
      {
        return true;
      }
      for (const auto& source : b.first.additional_sources)
      {
        if (source == a.first.alias_name)
        {
          return true;
        }
      }
      return false;
    };
    std::sort(snippets.begin(), snippets.end(), DependOn);

    for (const auto& [snippet, custom_eq] : snippets)
    {
      try
      {
        CustomPlotPtr new_custom_plot = std::make_shared<LuaCustomFunction>(snippet);
        new_custom_plot->xmlLoadState(custom_eq);

        new_custom_plot->calculateAndAdd(_mapped_plot_data);
        const auto& alias_name = new_custom_plot->aliasName();
        _curvelist_widget->addCustom(alias_name);

        _transform_functions.insert({ alias_name.toStdString(), new_custom_plot });
      }
      catch (std::runtime_error& err)
      {
        QMessageBox::warning(this, tr("Exception"),
                             tr("Failed to load customMathEquation [%1] \n\n %2\n")
                                 .arg(snippet.alias_name)
                                 .arg(err.what()));
      }
    }
    _curvelist_widget->refreshColumns();
  }

  auto colormaps = root.firstChildElement("colorMaps");

  if (!colormaps.isNull())
  {
    for (auto colormap = colormaps.firstChildElement("colorMap");
         colormap.isNull() == false; colormap = colormap.nextSiblingElement("colorMap"))
    {
      QString name = colormap.attribute("name");
      ColorMapLibrary()[name]->setScrip(colormap.text());
    }
  }

  QByteArray snippets_saved_xml =
      settings.value("AddCustomPlotDialog.savedXML", QByteArray()).toByteArray();

  auto snippets_element = root.firstChildElement("snippets");
  if (!snippets_element.isNull())
  {
    auto snippets_previous = GetSnippetsFromXML(snippets_saved_xml);
    auto snippets_layout = GetSnippetsFromXML(snippets_element);

    bool snippets_are_different = false;
    for (const auto& snippet_it : snippets_layout)
    {
      auto prev_it = snippets_previous.find(snippet_it.first);

      if (prev_it == snippets_previous.end() ||
          prev_it->second.function != snippet_it.second.function ||
          prev_it->second.global_vars != snippet_it.second.global_vars)
      {
        snippets_are_different = true;
        break;
      }
    }

    if (snippets_are_different)
    {
      QMessageBox msgBox(this);
      msgBox.setWindowTitle("Overwrite custom transforms?");
      msgBox.setText("Your layout file contains a set of custom transforms different "
                     "from "
                     "the last one you used.\nWant to load these transformations?");
      msgBox.addButton(QMessageBox::No);
      msgBox.addButton(QMessageBox::Yes);
      msgBox.setDefaultButton(QMessageBox::Yes);

      if (msgBox.exec() == QMessageBox::Yes)
      {
        for (const auto& snippet_it : snippets_layout)
        {
          snippets_previous[snippet_it.first] = snippet_it.second;
        }
        QDomDocument doc;
        auto snippets_root_element = ExportSnippets(snippets_previous, doc);
        doc.appendChild(snippets_root_element);
        settings.setValue("AddCustomPlotDialog.savedXML", doc.toByteArray(2));
      }
    }
  }

  ///--------------------------------------------------

  xmlLoadState(domDocument);

  linkedZoomOut();

  _undo_states.clear();
  _undo_states.push_back(domDocument);
  return true;
}

void MainWindow::linkedZoomOut()
{
  if (ui->pushButtonLink->isChecked())
  {
    for (const auto& it : TabbedPlotWidget::instances())
    {
      auto tabs = it.second->tabWidget();
      for (int t = 0; t < tabs->count(); t++)
      {
        if (PlotDocker* matrix = dynamic_cast<PlotDocker*>(tabs->widget(t)))
        {
          bool first = true;
          Range range;
          // find the ideal zoom
          for (int index = 0; index < matrix->plotCount(); index++)
          {
            PlotWidget* plot = matrix->plotAt(index);
            if (plot->isEmpty())
            {
              continue;
            }

            auto rect = plot->maxZoomRect();
            if (first)
            {
              range.min = rect.left();
              range.max = rect.right();
              first = false;
            }
            else
            {
              range.min = std::min(rect.left(), range.min);
              range.max = std::max(rect.right(), range.max);
            }
          }

          for (int index = 0; index < matrix->plotCount() && !first; index++)
          {
            PlotWidget* plot = matrix->plotAt(index);
            if (plot->isEmpty())
            {
              continue;
            }
            QRectF bound_act = plot->maxZoomRect();
            bound_act.setLeft(range.min);
            bound_act.setRight(range.max);
            plot->setZoomRectangle(bound_act, false);
            plot->replot();
          }
        }
      }
    }
  }
  else
  {
    this->forEachWidget([](PlotWidget* plot) { plot->zoomOut(false); });
  }
}

void MainWindow::on_tabbedAreaDestroyed(QObject* object)
{
  this->setFocus();
}

void MainWindow::forEachWidget(
    std::function<void(PlotWidget*, PlotDocker*, int)> operation)
{
  auto func = [&](QTabWidget* tabs) {
    for (int t = 0; t < tabs->count(); t++)
    {
      PlotDocker* matrix = dynamic_cast<PlotDocker*>(tabs->widget(t));
      if (!matrix)
      {
        continue;
      }

      for (int index = 0; index < matrix->plotCount(); index++)
      {
        PlotWidget* plot = matrix->plotAt(index);
        operation(plot, matrix, index);
      }
    }
  };

  for (const auto& it : TabbedPlotWidget::instances())
  {
    func(it.second->tabWidget());
  }
}

void MainWindow::forEachWidget(std::function<void(PlotWidget*)> op)
{
  forEachWidget([&](PlotWidget* plot, PlotDocker*, int) { op(plot); });
}

void MainWindow::updateTimeSlider()
{
  auto range = calculateVisibleRangeX();

  ui->timeSlider->setLimits(std::get<0>(range), std::get<1>(range), std::get<2>(range));

  _tracker_time = std::max(_tracker_time, ui->timeSlider->getMinimum());
  _tracker_time = std::min(_tracker_time, ui->timeSlider->getMaximum());
}

void MainWindow::updateTimeOffset()
{
  auto range = calculateVisibleRangeX();
  double min_time = std::get<0>(range);

  const bool remove_offset = ui->pushButtonRemoveTimeOffset->isChecked();
  if (remove_offset && min_time != std::numeric_limits<double>::max())
  {
    _time_offset.set(min_time);
  }
  else
  {
    _time_offset.set(0.0);
  }
}

void MainWindow::updateDataAndReplot(bool replot_hidden_tabs)
{
  _replot_timer->stop();

  MoveDataRet move_ret;

  if (_active_streamer_plugin)
  {
    {
      std::lock_guard<std::mutex> lock(_active_streamer_plugin->mutex());
      move_ret = MoveData(_active_streamer_plugin->dataMap(), _mapped_plot_data, false);
    }

    for (const auto& str : move_ret.added_curves)
    {
      _curvelist_widget->addCurve(str);
    }

    if (move_ret.curves_updated)
    {
      _curvelist_widget->refreshColumns();
    }

    _mapped_plot_data.setMaximumRangeX(ui->streamingSpinBox->value());
  }

  const bool is_streaming_active = isStreamingActive();

  //--------------------------------
  std::vector<TransformFunction*> transforms;
  transforms.reserve(_transform_functions.size());
  for (auto& [id, function] : _transform_functions)
  {
    transforms.push_back(function.get());
  }
  std::sort(
      transforms.begin(), transforms.end(),
      [](TransformFunction* a, TransformFunction* b) { return a->order() < b->order(); });

  // Update the reactive plots
  updateReactivePlots();

  // update all transforms, but not the ReactiveLuaFunction
  for (auto& function : transforms)
  {
    if (dynamic_cast<ReactiveLuaFunction*>(function) == nullptr)
    {
      function->calculate();
    }
  }

  forEachWidget([](PlotWidget* plot) { plot->updateCurves(false); });

  //--------------------------------
  // trigger again the execution of this callback if steaming == true
  if (is_streaming_active)
  {
    auto range = calculateVisibleRangeX();
    double max_time = std::get<1>(range);
    _tracker_time = max_time;
    onTrackerTimeUpdated(_tracker_time, false);
  }
  else
  {
    updateTimeOffset();
    updateTimeSlider();
  }
  //--------------------------------
  linkedZoomOut();
}

void MainWindow::on_streamingSpinBox_valueChanged(int value)
{
  double real_value = value;

  if (isStreamingActive() == false)
  {
    return;
  }

  _mapped_plot_data.setMaximumRangeX(real_value);

  if (_active_streamer_plugin)
  {
    _active_streamer_plugin->setMaximumRangeX(real_value);
  }
}

void MainWindow::on_actionExit_triggered()
{
  this->close();
}

void MainWindow::on_pushButtonRemoveTimeOffset_toggled(bool)
{
  updateTimeOffset();
  updatedDisplayTime();

  forEachWidget([](PlotWidget* plot) { plot->replot(); });

  if (this->signalsBlocked() == false)
  {
    onUndoableChange();
  }
}

void MainWindow::updatedDisplayTime()
{
  QLineEdit* timeLine = ui->displayTime;
  const double relative_time = _tracker_time - _time_offset.get();
  if (ui->pushButtonUseDateTime->isChecked())
  {
    if (ui->pushButtonRemoveTimeOffset->isChecked())
    {
      QTime time = QTime::fromMSecsSinceStartOfDay(std::round(relative_time * 1000.0));
      timeLine->setText(time.toString("HH:mm::ss.zzz"));
    }
    else
    {
      QDateTime datetime =
          QDateTime::fromMSecsSinceEpoch(std::round(_tracker_time * 1000.0));
      timeLine->setText(datetime.toString("[yyyy MMM dd] HH:mm::ss.zzz"));
    }
  }
  else
  {
    timeLine->setText(QString::number(relative_time, 'f', 3));
  }

  QFontMetrics fm(timeLine->font());
  int width = fm.width(timeLine->text()) + 10;
  timeLine->setFixedWidth(std::max(100, width));
}

void MainWindow::on_pushButtonActivateGrid_toggled(bool checked)
{
  forEachWidget([checked](PlotWidget* plot) {
    plot->activateGrid(checked);
    plot->replot();
  });
}

void MainWindow::on_pushButtonRatio_toggled(bool checked)
{
  forEachWidget([checked](PlotWidget* plot) {
    plot->setKeepRatioXY(checked);
    plot->replot();
  });
}

void MainWindow::on_pushButtonPlay_toggled(bool checked)
{
  if (checked)
  {
    _publish_timer->start();
    _prev_publish_time = QDateTime::currentDateTime();
  }
  else
  {
    _publish_timer->stop();
  }
}

void MainWindow::on_actionClearBuffer_triggered()
{
  for (auto& it : _mapped_plot_data.numeric)
  {
    it.second.clear();
  }

  for (auto& it : _mapped_plot_data.strings)
  {
    it.second.clear();
  }

  for (auto& it : _mapped_plot_data.user_defined)
  {
    it.second.clear();
  }

  for (auto& it : _transform_functions)
  {
    it.second->reset();
  }

  forEachWidget([](PlotWidget* plot) {
    plot->reloadPlotData();
    plot->replot();
  });
}

void MainWindow::on_deleteSerieFromGroup(std::string group_name)
{
  std::vector<std::string> names;

  auto AddFromGroup = [&](auto& series) {
    for (auto& it : series)
    {
      const auto& group = it.second.group();
      if (group && group->name() == group_name)
      {
        names.push_back(it.first);
      }
    }
  };
  AddFromGroup(_mapped_plot_data.numeric);
  AddFromGroup(_mapped_plot_data.strings);
  AddFromGroup(_mapped_plot_data.user_defined);

  onDeleteMultipleCurves(names);
}

void MainWindow::on_streamingNotificationsChanged(int active_count)
{
  if (active_count > 0 && _active_streamer_plugin)
  {
    enableStreamingNotificationsButton(true);

    QString tooltipText = QString("%1 has %2 outstanding notitication%3")
                              .arg(_active_streamer_plugin->name())
                              .arg(active_count)
                              .arg(active_count > 1 ? "s" : "");
    ui->buttonStreamingNotifications->setToolTip(tooltipText);
  }
  else
  {
    enableStreamingNotificationsButton(false);
    ui->buttonStreamingNotifications->setToolTip("View streaming alerts");
  }
}

void MainWindow::on_pushButtonUseDateTime_toggled(bool checked)
{
  static bool first = true;
  if (checked && ui->pushButtonRemoveTimeOffset->isChecked())
  {
    if (first)
    {
      QMessageBox::information(this, tr("Note"),
                               tr("When \"Use Date Time\" is checked, the option "
                                  "\"Remove Time Offset\" "
                                  "is automatically disabled.\n"
                                  "This message will be shown only once."));
      first = false;
    }
    ui->pushButtonRemoveTimeOffset->setChecked(false);
  }
  updatedDisplayTime();
}

void MainWindow::on_pushButtonTimeTracker_pressed()
{
  if (_tracker_param == CurveTracker::LINE_ONLY)
  {
    _tracker_param = CurveTracker::VALUE;
  }
  else if (_tracker_param == CurveTracker::VALUE)
  {
    _tracker_param = CurveTracker::VALUE_NAME;
  }
  else if (_tracker_param == CurveTracker::VALUE_NAME)
  {
    _tracker_param = CurveTracker::LINE_ONLY;
  }
  ui->pushButtonTimeTracker->setIcon(_tracker_button_icons[_tracker_param]);

  forEachWidget([&](PlotWidget* plot) {
    plot->configureTracker(_tracker_param);
    plot->replot();
  });
}

void MainWindow::closeEvent(QCloseEvent* event)
{
  _replot_timer->stop();
  _publish_timer->stop();

  if (_active_streamer_plugin)
  {
    _active_streamer_plugin->shutdown();
    _active_streamer_plugin = nullptr;
  }
  QSettings settings;
  settings.setValue("MainWindow.geometry", saveGeometry());
  settings.setValue("MainWindow.state", saveState());

  settings.setValue("MainWindow.activateGrid", ui->pushButtonActivateGrid->isChecked());
  settings.setValue("MainWindow.removeTimeOffset",
                    ui->pushButtonRemoveTimeOffset->isChecked());
  settings.setValue("MainWindow.dateTimeDisplay", ui->pushButtonUseDateTime->isChecked());
  settings.setValue("MainWindow.buttonLink", ui->pushButtonLink->isChecked());
  settings.setValue("MainWindow.buttonRatio", ui->pushButtonRatio->isChecked());

  settings.setValue("MainWindow.streamingBufferValue", ui->streamingSpinBox->value());
  settings.setValue("MainWindow.timeTrackerSetting", (int)_tracker_param);
  settings.setValue("MainWindow.splitterWidth", ui->mainSplitter->sizes()[0]);

  _data_loader.clear();
  _data_streamer.clear();
  _state_publisher.clear();
  _toolboxes.clear();
}

void MainWindow::onAddCustomPlot(const std::string& plot_name)
{
  ui->widgetStack->setCurrentIndex(1);
  _function_editor->setLinkedPlotName(QString::fromStdString(plot_name));
  _function_editor->createNewPlot();
}

void MainWindow::onEditCustomPlot(const std::string& plot_name)
{
  ui->widgetStack->setCurrentIndex(1);
  auto custom_it = _transform_functions.find(plot_name);
  if (custom_it == _transform_functions.end())
  {
    qWarning("failed to find custom equation");
    return;
  }
  _function_editor->editExistingPlot(
      std::dynamic_pointer_cast<LuaCustomFunction>(custom_it->second));
}

void MainWindow::onRefreshCustomPlot(const std::string& plot_name)
{
  try
  {
    auto custom_it = _transform_functions.find(plot_name);
    if (custom_it == _transform_functions.end())
    {
      qWarning("failed to find custom equation");
      return;
    }
    CustomPlotPtr ce = std::dynamic_pointer_cast<LuaCustomFunction>(custom_it->second);
    ce->calculateAndAdd(_mapped_plot_data);

    onUpdateLeftTableValues();
    updateDataAndReplot(true);
  }
  catch (const std::runtime_error& e)
  {
    QMessageBox::critical(this, "error",
                          "Failed to refresh data : " + QString::fromStdString(e.what()));
  }
}

void MainWindow::onPlaybackLoop()
{
  qint64 delta_ms =
      (QDateTime::currentMSecsSinceEpoch() - _prev_publish_time.toMSecsSinceEpoch());
  _prev_publish_time = QDateTime::currentDateTime();
  delta_ms = std::max((qint64)_publish_timer->interval(), delta_ms);

  _tracker_time += delta_ms * 0.001 * ui->playbackRate->value();
  if (_tracker_time >= ui->timeSlider->getMaximum())
  {
    if (!ui->playbackLoop->isChecked())
    {
      ui->pushButtonPlay->setChecked(false);
    }
    _tracker_time = ui->timeSlider->getMinimum();
  }
  //////////////////
  auto prev = ui->timeSlider->blockSignals(true);
  ui->timeSlider->setRealValue(_tracker_time);
  ui->timeSlider->blockSignals(prev);

  //////////////////
  updatedDisplayTime();
  onUpdateLeftTableValues();

  for (auto& it : _state_publisher)
  {
    it.second->play(_tracker_time);
  }

  forEachWidget([&](PlotWidget* plot) {
    plot->setTrackerPosition(_tracker_time);
    plot->replot();
  });
}

void MainWindow::onCustomPlotCreated(std::vector<CustomPlotPtr> custom_plots)
{
  std::set<PlotWidget*> widget_to_replot;

  for (auto custom_plot : custom_plots)
  {
    const std::string& curve_name = custom_plot->aliasName().toStdString();
    // clear already existing data first
    auto data_it = _mapped_plot_data.numeric.find(curve_name);
    if (data_it != _mapped_plot_data.numeric.end())
    {
      data_it->second.clear();
    }
    try
    {
      custom_plot->calculateAndAdd(_mapped_plot_data);
    }
    catch (std::exception& ex)
    {
      QMessageBox::warning(this, tr("Warning"),
                           tr("Failed to create the custom timeseries. "
                              "Error:\n\n%1")
                               .arg(ex.what()));
    }

    // keep data for reference
    auto custom_it = _transform_functions.find(curve_name);
    if (custom_it == _transform_functions.end())
    {
      _transform_functions.insert({ curve_name, custom_plot });
      _curvelist_widget->addCustom(QString::fromStdString(curve_name));
    }
    else
    {
      custom_it->second = custom_plot;
    }

    forEachWidget([&](PlotWidget* plot) {
      if (plot->curveFromTitle(QString::fromStdString(curve_name)))
      {
        widget_to_replot.insert(plot);
      }
    });
  }

  onUpdateLeftTableValues();
  ui->widgetStack->setCurrentIndex(0);
  _function_editor->clear();

  for (auto plot : widget_to_replot)
  {
    plot->updateCurves(true);
    plot->replot();
  }
  _curvelist_widget->clearSelections();
}

void MainWindow::on_actionReportBug_triggered()
{
  QDesktopServices::openUrl(QUrl("https://github.com/facontidavide/PlotJuggler/issues"));
}

void MainWindow::on_actionShare_the_love_triggered()
{
  QDesktopServices::openUrl(QUrl("https://twitter.com/intent/"
                                 "tweet?hashtags=PlotJuggler"));
}

void MainWindow::on_actionAbout_triggered()
{
  QDialog* dialog = new QDialog(this);
  auto ui = new Ui::AboutDialog();
  ui->setupUi(dialog);

  ui->label_version->setText(QString("version: ") + QApplication::applicationVersion());
  dialog->setAttribute(Qt::WA_DeleteOnClose);

  QFile fileTitle(_skin_path + "/about_window_title.html");
  if (fileTitle.open(QIODevice::ReadOnly))
  {
    ui->titleTextBrowser->setHtml(fileTitle.readAll());
  }

  QFile fileBody(_skin_path + "/about_window_body.html");
  if (fileBody.open(QIODevice::ReadOnly))
  {
    ui->bodyTextBrowser->setHtml(fileBody.readAll());
  }

  dialog->setAttribute(Qt::WA_DeleteOnClose);
  dialog->exec();
}

void MainWindow::on_actionCheatsheet_triggered()
{
  QSettings settings;

  CheatsheetDialog* dialog = new CheatsheetDialog(this);
  dialog->restoreGeometry(settings.value("Cheatsheet.geometry").toByteArray());
  dialog->exec();
  settings.setValue("Cheatsheet.geometry", dialog->saveGeometry());
  dialog->deleteLater();
}

void MainWindow::on_actionSupportPlotJuggler_triggered()
{
  QDialog* dialog = new QDialog(this);
  auto ui = new Ui::SupportDialog();
  ui->setupUi(dialog);

  dialog->setAttribute(Qt::WA_DeleteOnClose);

  dialog->exec();
}

/*
void MainWindow::on_actionSaveAllPlotTabs_triggered()
{
  QSettings settings;
  QString directory_path = settings.value("MainWindow.saveAllPlotTabs",
QDir::currentPath()).toString();
  // Get destination folder
  QFileDialog saveDialog(this);
  saveDialog.setDirectory(directory_path);
  saveDialog.setFileMode(QFileDialog::FileMode::Directory);
  saveDialog.setAcceptMode(QFileDialog::AcceptSave);
  saveDialog.exec();

  uint image_number = 1;
  if (saveDialog.result() == QDialog::Accepted && !saveDialog.selectedFiles().empty())
  {
    // Save Plots
    QString directory = saveDialog.selectedFiles().first();
    settings.setValue("MainWindow.saveAllPlotTabs", directory);

    QStringList file_names;
    QStringList existing_files;
    QDateTime current_date_time(QDateTime::currentDateTime());
    QString current_date_time_name(current_date_time.toString("yyyy-MM-dd_HH-mm-ss"));
    for (const auto& it : TabbedPlotWidget::instances())
    {
      auto tab_widget = it.second->tabWidget();
      for (int i = 0; i < tab_widget->count(); i++)
      {
        PlotDocker* matrix = static_cast<PlotDocker*>(tab_widget->widget(i));
        QString name = QString("%1/%2_%3_%4.png")
                           .arg(directory)
                           .arg(current_date_time_name)
                           .arg(image_number, 2, 10, QLatin1Char('0'))
                           .arg(matrix->name());
        file_names.push_back(name);
        image_number++;

        QFileInfo check_file(file_names.back());
        if (check_file.exists() && check_file.isFile())
        {
          existing_files.push_back(name);
        }
      }
    }
    if (existing_files.isEmpty() == false)
    {
      QMessageBox msgBox;
      msgBox.setText("One or more files will be overwritten. ant to continue?");
      QString all_files;
      for (const auto& str : existing_files)
      {
        all_files.push_back("\n");
        all_files.append(str);
      }
      msgBox.setInformativeText(all_files);
      msgBox.setStandardButtons(QMessageBox::Cancel | QMessageBox::Ok);
      msgBox.setDefaultButton(QMessageBox::Ok);

      if (msgBox.exec() != QMessageBox::Ok)
      {
        return;
      }
    }

    image_number = 0;
    for (const auto& it : TabbedPlotWidget::instances())
    {
      auto tab_widget = it.second->tabWidget();
      for (int i = 0; i < tab_widget->count(); i++)
      {
        PlotDocker* matrix = static_cast<PlotDocker*>(tab_widget->widget(i));
        TabbedPlotWidget::saveTabImage(file_names[image_number], matrix);
        image_number++;
      }
    }
  }
}*/

void MainWindow::on_pushButtonLoadDatafile_clicked()
{
  if (_data_loader.empty())
  {
    QMessageBox::warning(this, tr("Warning"),
                         tr("No plugin was loaded to process a data file\n"));
    return;
  }

  QSettings settings;

  QString file_extension_filter;

  std::set<QString> extensions;

  for (auto& it : _data_loader)
  {
    DataLoaderPtr loader = it.second;
    for (QString extension : loader->compatibleFileExtensions())
    {
      extensions.insert(extension.toLower());
    }
  }

  for (const auto& it : extensions)
  {
    file_extension_filter.append(QString(" *.") + it);
  }

  QString directory_path =
      settings.value("MainWindow.lastDatafileDirectory", QDir::currentPath()).toString();

  QFileDialog loadDialog(this);
  loadDialog.setFileMode(QFileDialog::ExistingFiles);
  loadDialog.setViewMode(QFileDialog::Detail);
  loadDialog.setNameFilter(file_extension_filter);
  loadDialog.setDirectory(directory_path);

  QStringList fileNames;
  if (loadDialog.exec())
  {
    fileNames = loadDialog.selectedFiles();
  }

  if (fileNames.isEmpty())
  {
    return;
  }

  directory_path = QFileInfo(fileNames[0]).absolutePath();
  settings.setValue("MainWindow.lastDatafileDirectory", directory_path);

  if (loadDataFromFiles(fileNames))
  {
    updateRecentDataMenu(fileNames);
  }
}

void MainWindow::on_pushButtonLoadLayout_clicked()
{
  QSettings settings;

  QString directory_path =
      settings.value("MainWindow.lastLayoutDirectory", QDir::currentPath()).toString();
  QString filename =
      QFileDialog::getOpenFileName(this, "Open Layout", directory_path, "*.xml");
  if (filename.isEmpty())
  {
    return;
  }

  if (loadLayoutFromFile(filename))
  {
    updateRecentLayoutMenu({ filename });
  }

  directory_path = QFileInfo(filename).absolutePath();
  settings.setValue("MainWindow.lastLayoutDirectory", directory_path);
}

void MainWindow::on_pushButtonSaveLayout_clicked()
{
  QDomDocument doc = xmlSaveState();

  QSettings settings;

  QString directory_path =
      settings.value("MainWindow.lastLayoutDirectory", QDir::currentPath()).toString();

  QFileDialog saveDialog(this);
  saveDialog.setOption(QFileDialog::DontUseNativeDialog, true);

  QGridLayout* save_layout = static_cast<QGridLayout*>(saveDialog.layout());

  QFrame* frame = new QFrame;
  frame->setFrameStyle(QFrame::Box | QFrame::Plain);
  frame->setLineWidth(1);

  QVBoxLayout* vbox = new QVBoxLayout;
  QLabel* title = new QLabel("Save Layout options");
  QFrame* separator = new QFrame;
  separator->setFrameStyle(QFrame::HLine | QFrame::Plain);

  auto checkbox_datasource = new QCheckBox("Save data source");
  checkbox_datasource->setToolTip("the layout will remember the source of your data,\n"
                                  "i.e. the Datafile used or the Streaming Plugin loaded "
                                  "?");
  checkbox_datasource->setFocusPolicy(Qt::NoFocus);
  checkbox_datasource->setChecked(
      settings.value("MainWindow.saveLayoutDataSource", true).toBool());

  auto checkbox_snippets = new QCheckBox("Save Scripts (transforms and colormaps)");
  checkbox_snippets->setToolTip("Do you want the layout to save your Lua scripts?");
  checkbox_snippets->setFocusPolicy(Qt::NoFocus);
  checkbox_snippets->setChecked(
      settings.value("MainWindow.saveLayoutSnippets", true).toBool());

  vbox->addWidget(title);
  vbox->addWidget(separator);
  vbox->addWidget(checkbox_datasource);
  vbox->addWidget(checkbox_snippets);
  frame->setLayout(vbox);

  int rows = save_layout->rowCount();
  int col = save_layout->columnCount();
  save_layout->addWidget(frame, 0, col, rows, 1, Qt::AlignTop);

  saveDialog.setAcceptMode(QFileDialog::AcceptSave);
  saveDialog.setDefaultSuffix("xml");
  saveDialog.setNameFilter("XML (*.xml)");
  saveDialog.setDirectory(directory_path);
  saveDialog.exec();

  if (saveDialog.result() != QDialog::Accepted || saveDialog.selectedFiles().empty())
  {
    return;
  }

  QString fileName = saveDialog.selectedFiles().first();

  if (fileName.isEmpty())
  {
    return;
  }

  directory_path = QFileInfo(fileName).absolutePath();
  settings.setValue("MainWindow.lastLayoutDirectory", directory_path);
  settings.setValue("MainWindow.saveLayoutDataSource", checkbox_datasource->isChecked());
  settings.setValue("MainWindow.saveLayoutSnippets", checkbox_snippets->isChecked());

  QDomElement root = doc.namedItem("root").toElement();

  root.appendChild(doc.createComment(" - - - - - - - - - - - - - - "));

  root.appendChild(doc.createComment(" - - - - - - - - - - - - - - "));

  root.appendChild(savePluginState(doc));

  root.appendChild(doc.createComment(" - - - - - - - - - - - - - - "));

  if (checkbox_datasource->isChecked())
  {
    QDomElement loaded_list = doc.createElement("previouslyLoaded_Datafiles");

    for (const auto& loaded : _loaded_datafiles)
    {
      QString loaded_datafile = QDir(directory_path).relativeFilePath(loaded.filename);

      QDomElement file_elem = doc.createElement("fileInfo");
      file_elem.setAttribute("filename", loaded_datafile);
      file_elem.setAttribute("prefix", loaded.prefix);

      QDomElement datasources_elem = doc.createElement("selected_datasources");
      QString topics_list = loaded.selected_datasources.join(";");
      datasources_elem.setAttribute("value", topics_list);
      file_elem.appendChild(datasources_elem);

      file_elem.appendChild(loaded.plugin_config.firstChild());
      loaded_list.appendChild(file_elem);
    }
    root.appendChild(loaded_list);

    if (_active_streamer_plugin)
    {
      QDomElement loaded_streamer = doc.createElement("previouslyLoaded_Streamer");
      QString streamer_name = _active_streamer_plugin->name();
      loaded_streamer.setAttribute("name", streamer_name);
      root.appendChild(loaded_streamer);
    }
  }
  //-----------------------------------
  root.appendChild(doc.createComment(" - - - - - - - - - - - - - - "));
  if (checkbox_snippets->isChecked())
  {
    QDomElement custom_equations = doc.createElement("customMathEquations");
    for (const auto& custom_it : _transform_functions)
    {
      const auto& custom_plot = custom_it.second;
      custom_plot->xmlSaveState(doc, custom_equations);
    }
    root.appendChild(custom_equations);

    QByteArray snippets_xml_text =
        settings.value("AddCustomPlotDialog.savedXML", QByteArray()).toByteArray();
    auto snipped_saved = GetSnippetsFromXML(snippets_xml_text);
    auto snippets_root = ExportSnippets(snipped_saved, doc);
    root.appendChild(snippets_root);

    QDomElement color_maps = doc.createElement("colorMaps");
    for (const auto& it : ColorMapLibrary())
    {
      QString colormap_name = it.first;
      QDomElement colormap = doc.createElement("colorMap");
      QDomText colormap_script = doc.createTextNode(it.second->script());
      colormap.setAttribute("name", colormap_name);
      colormap.appendChild(colormap_script);
      color_maps.appendChild(colormap);
    }
  }
  root.appendChild(doc.createComment(" - - - - - - - - - - - - - - "));
  //------------------------------------
  QFile file(fileName);
  if (file.open(QIODevice::WriteOnly))
  {
    QTextStream stream(&file);
    stream << doc.toString() << endl;
  }
}

void MainWindow::onActionFullscreenTriggered()
{
  static bool first_call = true;
  if (first_call && !_minimized)
  {
    first_call = false;
    QMessageBox::information(this, "Remember!",
                             "Press F10 to switch back to the normal view");
  }

  _minimized = !_minimized;

  ui->leftMainWindowFrame->setVisible(!_minimized);
  //  ui->widgetOptions->setVisible(!_minimized && ui->pushButtonOptions->isChecked());
  ui->widgetTimescale->setVisible(!_minimized);
  ui->menuBar->setVisible(!_minimized);

  for (auto& it : TabbedPlotWidget::instances())
  {
    it.second->setControlsVisible(!_minimized);
  }
}

void MainWindow::on_actionClearRecentData_triggered()
{
  QMenu* menu = _recent_data_files;
  for (QAction* action : menu->actions())
  {
    if (action->isSeparator())
    {
      break;
    }
    menu->removeAction(action);
  }
  menu->setEnabled(false);
  QSettings settings;
  settings.setValue("MainWindow.recentlyLoadedDatafile", {});
}

void MainWindow::on_actionClearRecentLayout_triggered()
{
  QMenu* menu = _recent_layout_files;
  for (QAction* action : menu->actions())
  {
    if (action->isSeparator())
    {
      break;
    }
    menu->removeAction(action);
  }
  menu->setEnabled(false);
  QSettings settings;
  settings.setValue("MainWindow.recentlyLoadedLayout", {});
}

void MainWindow::on_actionDeleteAllData_triggered()
{
  QMessageBox msgBox(this);
  msgBox.setWindowTitle("Warning. Can't be undone.");
  msgBox.setText(tr("Do you want to remove the previously loaded data?\n"));
  msgBox.addButton(QMessageBox::No);
  msgBox.addButton(QMessageBox::Yes);
  msgBox.setDefaultButton(QMessageBox::Yes);
  auto reply = msgBox.exec();

  if (reply == QMessageBox::No)
  {
    return;
  }

  deleteAllData();
}

void MainWindow::on_actionPreferences_triggered()
{
  QSettings settings;
  QString prev_style = settings.value("Preferences::theme", "light").toString();

  PreferencesDialog dialog;
  dialog.exec();

  QString theme = settings.value("Preferences::theme").toString();

  if (theme != prev_style)
  {
    loadStyleSheet(tr(":/resources/stylesheet_%1.qss").arg(theme));
  }
}

void MainWindow::on_playbackStep_valueChanged(double step)
{
  ui->timeSlider->setFocus();
  ui->timeSlider->setRealStepValue(step);
}

void MainWindow::on_actionLoadStyleSheet_triggered()
{
  QSettings settings;
  QString directory_path =
      settings.value("MainWindow.loadStyleSheetDirectory", QDir::currentPath())
          .toString();

  QString fileName = QFileDialog::getOpenFileName(this, tr("Load StyleSheet"),
                                                  directory_path, tr("(*.qss)"));
  if (fileName.isEmpty())
  {
    return;
  }

  loadStyleSheet(fileName);

  directory_path = QFileInfo(fileName).absolutePath();
  settings.setValue("MainWindow.loadStyleSheetDirectory", directory_path);
}

void MainWindow::on_pushButtonLegend_clicked()
{
  switch (_labels_status)
  {
    case LabelStatus::LEFT:
      _labels_status = LabelStatus::HIDDEN;
      break;
    case LabelStatus::RIGHT:
      _labels_status = LabelStatus::LEFT;
      break;
    case LabelStatus::HIDDEN:
      _labels_status = LabelStatus::RIGHT;
      break;
  }

  auto visitor = [=](PlotWidget* plot) {
    plot->activateLegend(_labels_status != LabelStatus::HIDDEN);

    if (_labels_status == LabelStatus::LEFT)
    {
      plot->setLegendAlignment(Qt::AlignLeft);
    }
    else if (_labels_status == LabelStatus::RIGHT)
    {
      plot->setLegendAlignment(Qt::AlignRight);
    }
    plot->replot();
  };

  this->forEachWidget(visitor);
}

void MainWindow::on_pushButtonZoomOut_clicked()
{
  linkedZoomOut();
  onUndoableChange();
}

void MainWindow::on_comboStreaming_currentIndexChanged(const QString& current_text)
{
  QSettings settings;
  settings.setValue("MainWindow.previousStreamingPlugin", current_text);
  auto streamer = _data_streamer.at(current_text);
  ui->buttonStreamingOptions->setEnabled(!streamer->availableActions().empty());

  std::pair<QAction*, int> notifications_pair = streamer->notificationAction();
  if (notifications_pair.first == nullptr)
  {
    ui->buttonStreamingNotifications->setEnabled(false);
  }
  else
  {
    on_streamingNotificationsChanged(notifications_pair.second);
  }
}

void MainWindow::on_buttonStreamingStart_clicked()
{
  ui->buttonStreamingStart->setEnabled(false);
  if (ui->buttonStreamingStart->text() == "Start")
  {
    startStreamingPlugin(ui->comboStreaming->currentText());
  }
  else
  {
    stopStreamingPlugin();
  }
  ui->buttonStreamingStart->setEnabled(true);
}

PopupMenu::PopupMenu(QWidget* relative_widget, QWidget* parent)
  : QMenu(parent), _w(relative_widget)
{
}

void PopupMenu::showEvent(QShowEvent*)
{
  QPoint p = _w->mapToGlobal({});
  QRect geo = _w->geometry();
  this->move(p.x() + geo.width(), p.y());
}

void PopupMenu::leaveEvent(QEvent*)
{
  close();
}

void PopupMenu::closeEvent(QCloseEvent*)
{
  _w->setAttribute(Qt::WA_UnderMouse, false);
}

void MainWindow::on_buttonRecentData_clicked()
{
  PopupMenu* menu = new PopupMenu(ui->buttonRecentData, this);

  for (auto action : _recent_data_files->actions())
  {
    menu->addAction(action);
  }
  menu->exec();
}

void MainWindow::on_buttonStreamingOptions_clicked()
{
  if (_data_streamer.empty())
  {
    return;
  }
  auto streamer = _data_streamer.at(ui->comboStreaming->currentText());

  PopupMenu* menu = new PopupMenu(ui->buttonStreamingOptions, this);
  for (auto action : streamer->availableActions())
  {
    menu->addAction(action);
  }
  menu->show();
}

void MainWindow::on_buttonHideFileFrame_clicked()
{
  bool hidden = !ui->frameFile->isHidden();
  ui->buttonHideFileFrame->setText(hidden ? "+" : " -");
  ui->frameFile->setHidden(hidden);

  QSettings settings;
  settings.setValue("MainWindow.hiddenFileFrame", hidden);
}

void MainWindow::on_buttonHideStreamingFrame_clicked()
{
  bool hidden = !ui->frameStreaming->isHidden();
  ui->buttonHideStreamingFrame->setText(hidden ? "+" : " -");
  ui->frameStreaming->setHidden(hidden);

  QSettings settings;
  settings.setValue("MainWindow.hiddenStreamingFrame", hidden);
}

void MainWindow::on_buttonHidePublishersFrame_clicked()
{
  bool hidden = !ui->framePublishers->isHidden();
  ui->buttonHidePublishersFrame->setText(hidden ? "+" : " -");
  ui->framePublishers->setHidden(hidden);

  QSettings settings;
  settings.setValue("MainWindow.hiddenPublishersFrame", hidden);
}

void MainWindow::on_buttonRecentLayout_clicked()
{
  PopupMenu* menu = new PopupMenu(ui->buttonRecentLayout, this);

  for (auto action : _recent_layout_files->actions())
  {
    menu->addAction(action);
  }
  menu->exec();
}

QStringList MainWindow::readAllCurvesFromXML(QDomElement root_node)
{
  QStringList curves;

  QStringList level_names = { "tabbed_widget", "Tab",  "Container", "DockSplitter",
                              "DockArea",      "plot", "curve" };

  std::function<void(int, QDomElement)> recursiveXmlStream;
  recursiveXmlStream = [&](int level, QDomElement parent_elem) {
    QString level_name = level_names[level];
    for (auto elem = parent_elem.firstChildElement(level_name); elem.isNull() == false;
         elem = elem.nextSiblingElement(level_name))
    {
      if (level_name == "curve")
      {
        curves.push_back(elem.attribute("name"));
      }
      else
      {
        recursiveXmlStream(level + 1, elem);
      }
    }
  };

  // start recursion
  recursiveXmlStream(0, root_node);

  return curves;
}

void MainWindow::on_actionColorMap_Editor_triggered()
{
  ColorMapEditor dialog;
  dialog.exec();
}

<MSG> Merge pull request #505 from rticommunity/main

Enhanced processing of default command-line arguments
<DFF> @@ -113,7 +113,7 @@ MainWindow::MainWindow(const QCommandLineParser& commandline_parser, QWidget* pa
   }
   QFile fileTitle( _skin_path + "/mainwindow_title.txt");
   if(fileTitle.open(QIODevice::ReadOnly)) {
-    QString title = fileTitle.readAll();
+    QString title = fileTitle.readAll().trimmed();
     setWindowTitle(title);
   }
 

{'commit': 'diff --git a/plotjuggler_app/main.cpp b/plotjuggler_app/main.cpp\nindex bdee36d0..5a5b494b 100644\n--- a/plotjuggler_app/main.cpp\n+++ b/plotjuggler_app/main.cpp\n@@ -96,16 +96,14 @@ std::pair<int,char**> MergeArguments(int argc, char* argv[])\n   static char *new_argv[100];\n \n   // preserve arg[0] => executable path\n-  for (int i=0; i< argc; ++i ) {\n-    new_argv[i] = argv[i];\n-  }\n+  new_argv[0] = argv[0];\n \n   // Add the remain arguments, replacing escaped characters if necessary.\n   // Escaping needed because some chars cannot be entered easily in the -DPJ_DEFAULT_ARGS\n   // preprocessor directive\n   //   _0x20_   -->   \' \'   (space)\n   //   _0x3b_   -->   \';\'   (semicolon)\n-  int index = argc;\n+  int index = 1;\n   for ( auto cmdline_arg : default_cmdline_args  )\n   {\n     // replace(const QString &before, const QString &after, Qt::CaseSensitivity cs = Qt::CaseSensitive)\n@@ -113,6 +111,13 @@ std::pair<int,char**> MergeArguments(int argc, char* argv[])\n     cmdline_arg = cmdline_arg.replace("_0x3b_", ";", Qt::CaseSensitive);\n     new_argv[index++] = strdup(cmdline_arg.toLocal8Bit().data());\n   }\n+\n+  // If an argument appears repeated, the second value overrides previous one. \n+  // Do this after adding default_cmdline_args so the command-line overide default\n+  for (int i=1; i< argc; ++i ) {\n+    new_argv[index++] = argv[i];\n+  }\n+\n   return {new_argc, new_argv};\n \n #else\ndiff --git a/plotjuggler_app/mainwindow.cpp b/plotjuggler_app/mainwindow.cpp\nindex 5547c385..f1c8655c 100644\n--- a/plotjuggler_app/mainwindow.cpp\n+++ b/plotjuggler_app/mainwindow.cpp\n@@ -113,7 +113,7 @@ MainWindow::MainWindow(const QCommandLineParser& commandline_parser, QWidget* pa\n   }\n   QFile fileTitle( _skin_path + "/mainwindow_title.txt");\n   if(fileTitle.open(QIODevice::ReadOnly)) {\n-    QString title = fileTitle.readAll();\n+    QString title = fileTitle.readAll().trimmed();\n     setWindowTitle(title);\n   }\n \n', 'commit_subject': 'Merge pull request #505 from rticommunity/main', 'repo_name': 'facontidavide/PlotJuggler', 'committer': {'name': 'GitHub', 'email': '1505422b2465e9a84f6fdfaa161078890c593f06@github.com', 'time_sec': '1630581757', 'tz_offset': '120', 'date': {'seconds': '1630581757'}}, 'commit_tree': 'd1ef1ae5b07906ab99bd16e1af9f4d210b2902ad', 'commit_message': 'Merge pull request #505 from rticommunity/main\n\nEnhanced processing of default command-line arguments', 'language_name': 'Shell', 'license': 'mpl-2.0', 'hunks': '@@ -113,7 +113,7 @@ MainWindow::MainWindow(const QCommandLineParser& commandline_parser, QWidget* pa\n   }\n   QFile fileTitle( _skin_path + "/mainwindow_title.txt");\n   if(fileTitle.open(QIODevice::ReadOnly)) {\n-    QString title = fileTitle.readAll();\n+    QString title = fileTitle.readAll().trimmed();\n     setWindowTitle(title);\n   }\n \n', 'addition_count': 1, 'deletion_count': 1, 'src_file': 'a/plotjuggler_app/mainwindow.cpp', 'tgt_file': 'b/plotjuggler_app/mainwindow.cpp', 'file_extension': '.cpp', 'before_file': '/*\n * This Source Code Form is subject to the terms of the Mozilla Public\n * License, v. 2.0. If a copy of the MPL was not distributed with this\n * file, You can obtain one at https://mozilla.org/MPL/2.0/.\n */\n\n#include <functional>\n#include <stdio.h>\n#include <numeric>\n\n#include <QApplication>\n#include <QActionGroup>\n#include <QCheckBox>\n#include <QCommandLineParser>\n#include <QDebug>\n#include <QDesktopServices>\n#include <QDomDocument>\n#include <QDoubleSpinBox>\n#include <QElapsedTimer>\n#include <QFileDialog>\n#include <QInputDialog>\n#include <QMenu>\n#include <QGroupBox>\n#include <QMessageBox>\n#include <QMimeData>\n#include <QMouseEvent>\n#include <QPluginLoader>\n#include <QPushButton>\n#include <QKeySequence>\n#include <QScrollBar>\n#include <QSettings>\n#include <QStringListModel>\n#include <QStringRef>\n#include <QThread>\n#include <QTextStream>\n#include <QWindow>\n#include <QHeaderView>\n#include <QStandardPaths>\n#include <QXmlStreamReader>\n\n#include "mainwindow.h"\n#include "curvelist_panel.h"\n#include "tabbedplotwidget.h"\n#include "PlotJuggler/plotdata.h"\n#include "qwt_plot_canvas.h"\n#include "transforms/function_editor.h"\n#include "transforms/lua_custom_function.h"\n#include "utils.h"\n#include "stylesheet.h"\n#include "dummy_data.h"\n#include "PlotJuggler/svg_util.h"\n#include "PlotJuggler/reactive_function.h"\n#include "multifile_prefix.h"\n\n#include "ui_aboutdialog.h"\n#include "ui_support_dialog.h"\n#include "preferences_dialog.h"\n#include "nlohmann_parsers.h"\n#include "cheatsheet/cheatsheet_dialog.h"\n#include "colormap_editor.h"\n\n#ifdef COMPILED_WITH_CATKIN\n\n#endif\n#ifdef COMPILED_WITH_AMENT\n#include <ament_index_cpp/get_package_prefix.hpp>\n#include <ament_index_cpp/get_package_share_directory.hpp>\n#endif\n\nMainWindow::MainWindow(const QCommandLineParser& commandline_parser, QWidget* parent)\n  : QMainWindow(parent)\n  , ui(new Ui::MainWindow)\n  , _undo_shortcut(QKeySequence(Qt::CTRL + Qt::Key_Z), this)\n  , _redo_shortcut(QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_Z), this)\n  , _fullscreen_shortcut(Qt::Key_F10, this)\n  , _streaming_shortcut(QKeySequence(Qt::CTRL + Qt::Key_Space), this)\n  , _playback_shotcut(Qt::Key_Space, this)\n  , _minimized(false)\n  , _active_streamer_plugin(nullptr)\n  , _disable_undo_logging(false)\n  , _tracker_time(0)\n  , _tracker_param(CurveTracker::VALUE)\n  , _labels_status(LabelStatus::RIGHT)\n  , _recent_data_files(new QMenu())\n  , _recent_layout_files(new QMenu())\n{\n  QLocale::setDefault(QLocale::c());  // set as default\n  setAcceptDrops(true);\n\n  _test_option = commandline_parser.isSet("test");\n  _autostart_publishers = commandline_parser.isSet("publish");\n\n  if (commandline_parser.isSet("enabled_plugins"))\n  {\n    _enabled_plugins =\n        commandline_parser.value("enabled_plugins").split(";", QString::SkipEmptyParts);\n    // Treat the command-line parameter  \'--enabled_plugins *\' to mean all plugings are\n    // enabled\n    if ((_enabled_plugins.size() == 1) && (_enabled_plugins.contains("*")))\n    {\n      _enabled_plugins.clear();\n    }\n  }\n  if (commandline_parser.isSet("disabled_plugins"))\n  {\n    _disabled_plugins =\n        commandline_parser.value("disabled_plugins").split(";", QString::SkipEmptyParts);\n  }\n\n  _curvelist_widget = new CurveListPanel(_mapped_plot_data, _transform_functions, this);\n\n  ui->setupUi(this);\n  }\n  QFile fileTitle( _skin_path + "/mainwindow_title.txt");\n  if(fileTitle.open(QIODevice::ReadOnly)) {\n    QString title = fileTitle.readAll();\n    setWindowTitle(title);\n  }\n\n    {\n      _skin_path = path.absolutePath();\n    }\n  }\n  if (commandline_parser.isSet("window_title"))\n  {\n    setWindowTitle(commandline_parser.value("window_title"));\n  }\n  else\n  {\n    QFile fileTitle(_skin_path + "/mainwindow_title.txt");\n    if (fileTitle.open(QIODevice::ReadOnly))\n    {\n      QString title = fileTitle.readAll().trimmed();\n      setWindowTitle(title);\n    }\n  }\n\n  QSettings settings;\n\n  ui->playbackLoop->setText("");\n  ui->pushButtonZoomOut->setText("");\n  ui->pushButtonPlay->setText("");\n  ui->pushButtonUseDateTime->setText("");\n  ui->pushButtonActivateGrid->setText("");\n  ui->pushButtonRatio->setText("");\n  ui->pushButtonLink->setText("");\n  ui->pushButtonTimeTracker->setText("");\n  ui->pushButtonLoadDatafile->setText("");\n  ui->pushButtonRemoveTimeOffset->setText("");\n  ui->pushButtonLegend->setText("");\n\n  if (commandline_parser.isSet("buffer_size"))\n  {\n    int buffer_size = std::max(10, commandline_parser.value("buffer_size").toInt());\n    ui->streamingSpinBox->setMaximum(buffer_size);\n  }\n\n  _animated_streaming_movie = new QMovie(":/resources/animated_radio.gif");\n  _animated_streaming_movie->setScaledSize(ui->labelStreamingAnimation->size());\n  _animated_streaming_movie->jumpToFrame(0);\n\n  _animated_streaming_timer = new QTimer();\n  _animated_streaming_timer->setSingleShot(true);\n\n  connect(_animated_streaming_timer, &QTimer::timeout, this, [this]() {\n    _animated_streaming_movie->stop();\n    _animated_streaming_movie->jumpToFrame(0);\n  });\n\n  _tracker_delay.connectCallback([this]() {\n    updatedDisplayTime();\n    onUpdateLeftTableValues();\n  });\n\n  ui->labelStreamingAnimation->setMovie(_animated_streaming_movie);\n  ui->labelStreamingAnimation->setHidden(true);\n\n  connect(this, &MainWindow::stylesheetChanged, this, &MainWindow::on_stylesheetChanged);\n\n  connect(this, &MainWindow::stylesheetChanged, _curvelist_widget,\n          &CurveListPanel::on_stylesheetChanged);\n\n  connect(_curvelist_widget, &CurveListPanel::hiddenItemsChanged, this,\n          &MainWindow::onUpdateLeftTableValues);\n\n  connect(_curvelist_widget, &CurveListPanel::deleteCurves, this,\n          &MainWindow::onDeleteMultipleCurves);\n\n  connect(_curvelist_widget, &CurveListPanel::createMathPlot, this,\n          &MainWindow::onAddCustomPlot);\n\n  connect(_curvelist_widget, &CurveListPanel::editMathPlot, this,\n          &MainWindow::onEditCustomPlot);\n\n  connect(_curvelist_widget, &CurveListPanel::refreshMathPlot, this,\n          &MainWindow::onRefreshCustomPlot);\n\n  connect(ui->timeSlider, &RealSlider::realValueChanged, this,\n          &MainWindow::onTimeSlider_valueChanged);\n\n  connect(ui->playbackRate, &QDoubleSpinBox::editingFinished, this,\n          [this]() { ui->playbackRate->clearFocus(); });\n\n  connect(ui->playbackStep, &QDoubleSpinBox::editingFinished, this,\n          [this]() { ui->playbackStep->clearFocus(); });\n\n  connect(_curvelist_widget, &CurveListPanel::requestDeleteAll, this, [this](int option) {\n    if (option == 1)\n    {\n      deleteAllData();\n    }\n    else if (option == 2)\n    {\n      on_actionClearBuffer_triggered();\n    }\n  });\n\n  _main_tabbed_widget =\n      new TabbedPlotWidget("Main Window", this, _mapped_plot_data, this);\n\n  connect(this, &MainWindow::stylesheetChanged, _main_tabbed_widget,\n          &TabbedPlotWidget::on_stylesheetChanged);\n\n  ui->plottingLayout->insertWidget(0, _main_tabbed_widget, 1);\n  ui->leftLayout->addWidget(_curvelist_widget, 1);\n\n  ui->mainSplitter->setCollapsible(0, true);\n  ui->mainSplitter->setStretchFactor(0, 2);\n  ui->mainSplitter->setStretchFactor(1, 6);\n\n  ui->layoutTimescale->removeWidget(ui->widgetButtons);\n  _main_tabbed_widget->tabWidget()->setCornerWidget(ui->widgetButtons);\n\n  connect(ui->mainSplitter, SIGNAL(splitterMoved(int, int)),\n          SLOT(on_splitterMoved(int, int)));\n\n  initializeActions();\n\n  LoadColorMapFromSettings();\n\n  //------------ Load plugins -------------\n  auto plugin_extra_folders =\n      commandline_parser.value("plugin_folders").split(";", QString::SkipEmptyParts);\n\n  _default_streamer = commandline_parser.value("start_streamer");\n\n  loadAllPlugins(plugin_extra_folders);\n\n  //------------------------------------\n\n  _undo_timer.start();\n\n  // save initial state\n  onUndoableChange();\n\n  _replot_timer = new QTimer(this);\n  connect(_replot_timer, &QTimer::timeout, this,\n          [this]() { updateDataAndReplot(false); });\n\n  _publish_timer = new QTimer(this);\n  _publish_timer->setInterval(20);\n  connect(_publish_timer, &QTimer::timeout, this, &MainWindow::onPlaybackLoop);\n\n  ui->menuFile->setToolTipsVisible(true);\n\n  this->setMenuBar(ui->menuBar);\n  ui->menuBar->setNativeMenuBar(false);\n\n  if (_test_option)\n  {\n    buildDummyData();\n  }\n\n  bool file_loaded = false;\n  if (commandline_parser.isSet("datafile"))\n  {\n    QStringList datafiles = commandline_parser.values("datafile");\n    file_loaded = loadDataFromFiles(datafiles);\n  }\n  if (commandline_parser.isSet("layout"))\n  {\n    loadLayoutFromFile(commandline_parser.value("layout"));\n  }\n\n  restoreGeometry(settings.value("MainWindow.geometry").toByteArray());\n  restoreState(settings.value("MainWindow.state").toByteArray());\n\n  // qDebug() << "restoreGeometry";\n\n  bool activate_grid = settings.value("MainWindow.activateGrid", false).toBool();\n  ui->pushButtonActivateGrid->setChecked(activate_grid);\n\n  bool zoom_link_active = settings.value("MainWindow.buttonLink", true).toBool();\n  ui->pushButtonLink->setChecked(zoom_link_active);\n\n  bool ration_active = settings.value("MainWindow.buttonRatio", true).toBool();\n  ui->pushButtonRatio->setChecked(ration_active);\n\n  int streaming_buffer_value =\n      settings.value("MainWindow.streamingBufferValue", 5).toInt();\n  ui->streamingSpinBox->setValue(streaming_buffer_value);\n\n  bool datetime_display = settings.value("MainWindow.dateTimeDisplay", false).toBool();\n  ui->pushButtonUseDateTime->setChecked(datetime_display);\n\n  bool remove_time_offset = settings.value("MainWindow.removeTimeOffset", true).toBool();\n  ui->pushButtonRemoveTimeOffset->setChecked(remove_time_offset);\n\n  //  ui->widgetOptions->setVisible(ui->pushButtonOptions->isChecked());\n\n  if (settings.value("MainWindow.hiddenFileFrame", false).toBool())\n  {\n    ui->buttonHideFileFrame->setText("+");\n    ui->frameFile->setHidden(true);\n  }\n  if (settings.value("MainWindow.hiddenStreamingFrame", false).toBool())\n  {\n    ui->buttonHideStreamingFrame->setText("+");\n    ui->frameStreaming->setHidden(true);\n  }\n  if (settings.value("MainWindow.hiddenPublishersFrame", false).toBool())\n  {\n    ui->buttonHidePublishersFrame->setText("+");\n    ui->framePublishers->setHidden(true);\n  }\n\n  //----------------------------------------------------------\n  QIcon trackerIconA, trackerIconB, trackerIconC;\n\n  trackerIconA.addFile(QStringLiteral(":/style_light/line_tracker.png"), QSize(36, 36));\n  trackerIconB.addFile(QStringLiteral(":/style_light/line_tracker_1.png"), QSize(36, 36));\n  trackerIconC.addFile(QStringLiteral(":/style_light/line_tracker_a.png"), QSize(36, 36));\n\n  _tracker_button_icons[CurveTracker::LINE_ONLY] = trackerIconA;\n  _tracker_button_icons[CurveTracker::VALUE] = trackerIconB;\n  _tracker_button_icons[CurveTracker::VALUE_NAME] = trackerIconC;\n\n  int tracker_setting =\n      settings.value("MainWindow.timeTrackerSetting", (int)CurveTracker::VALUE).toInt();\n  _tracker_param = static_cast<CurveTracker::Parameter>(tracker_setting);\n\n  ui->pushButtonTimeTracker->setIcon(_tracker_button_icons[_tracker_param]);\n\n  forEachWidget([&](PlotWidget* plot) { plot->configureTracker(_tracker_param); });\n\n  auto editor_layout = new QVBoxLayout();\n  editor_layout->setMargin(0);\n  ui->formulaPage->setLayout(editor_layout);\n  _function_editor =\n      new FunctionEditorWidget(_mapped_plot_data, _transform_functions, this);\n  editor_layout->addWidget(_function_editor);\n\n  connect(_function_editor, &FunctionEditorWidget::closed, this,\n          [this]() { ui->widgetStack->setCurrentIndex(0); });\n\n  connect(this, &MainWindow::stylesheetChanged, _function_editor,\n          &FunctionEditorWidget::on_stylesheetChanged);\n\n  connect(_function_editor, &FunctionEditorWidget::accept, this,\n          &MainWindow::onCustomPlotCreated);\n\n  QString theme = settings.value("Preferences::theme", "light").toString();\n  if (theme != "dark")\n  {\n    theme = "light";\n  }\n  loadStyleSheet(tr(":/resources/stylesheet_%1.qss").arg(theme));\n\n  // builtin messageParsers\n  auto json_parser = std::make_shared<JSON_ParserFactory>();\n  _parser_factories.insert({ json_parser->encoding(), json_parser });\n\n  auto cbor_parser = std::make_shared<CBOR_ParserFactory>();\n  _parser_factories.insert({ cbor_parser->encoding(), cbor_parser });\n\n  auto bson_parser = std::make_shared<BSON_ParserFactory>();\n  _parser_factories.insert({ bson_parser->encoding(), bson_parser });\n\n  auto msgpack = std::make_shared<MessagePack_ParserFactory>();\n  _parser_factories.insert({ msgpack->encoding(), msgpack });\n\n  if (!_default_streamer.isEmpty())\n  {\n    auto index = ui->comboStreaming->findText(_default_streamer);\n    if (index != -1)\n    {\n      ui->comboStreaming->setCurrentIndex(index);\n      settings.setValue("MainWindow.previousStreamingPlugin", _default_streamer);\n    }\n  }\n}\n\nMainWindow::~MainWindow()\n{\n  // important: avoid problems with plugins\n  _mapped_plot_data.user_defined.clear();\n\n  delete ui;\n}\n\nvoid MainWindow::onUndoableChange()\n{\n  if (_disable_undo_logging)\n    return;\n\n  int elapsed_ms = _undo_timer.restart();\n\n  // overwrite the previous\n  if (elapsed_ms < 100)\n  {\n    if (_undo_states.empty() == false)\n      _undo_states.pop_back();\n  }\n\n  while (_undo_states.size() >= 100)\n    _undo_states.pop_front();\n  _undo_states.push_back(xmlSaveState());\n  _redo_states.clear();\n  // qDebug() << "undo " << _undo_states.size();\n}\n\nvoid MainWindow::onRedoInvoked()\n{\n  _disable_undo_logging = true;\n  if (_redo_states.size() > 0)\n  {\n    QDomDocument state_document = _redo_states.back();\n    while (_undo_states.size() >= 100)\n      _undo_states.pop_front();\n    _undo_states.push_back(state_document);\n    _redo_states.pop_back();\n\n    xmlLoadState(state_document);\n  }\n  // qDebug() << "undo " << _undo_states.size();\n  _disable_undo_logging = false;\n}\n\nvoid MainWindow::onUndoInvoked()\n{\n  _disable_undo_logging = true;\n  if (_undo_states.size() > 1)\n  {\n    QDomDocument state_document = _undo_states.back();\n    while (_redo_states.size() >= 100)\n      _redo_states.pop_front();\n    _redo_states.push_back(state_document);\n    _undo_states.pop_back();\n    state_document = _undo_states.back();\n\n    xmlLoadState(state_document);\n  }\n  // qDebug() << "undo " << _undo_states.size();\n  _disable_undo_logging = false;\n}\n\nvoid MainWindow::onUpdateLeftTableValues()\n{\n  _curvelist_widget->update2ndColumnValues(_tracker_time);\n}\n\nvoid MainWindow::onTrackerMovedFromWidget(QPointF relative_pos)\n{\n  _tracker_time = relative_pos.x() + _time_offset.get();\n\n  auto prev = ui->timeSlider->blockSignals(true);\n  ui->timeSlider->setRealValue(_tracker_time);\n  ui->timeSlider->blockSignals(prev);\n\n  onTrackerTimeUpdated(_tracker_time, true);\n}\n\nvoid MainWindow::onTimeSlider_valueChanged(double abs_time)\n{\n  _tracker_time = abs_time;\n  onTrackerTimeUpdated(_tracker_time, true);\n}\n\nvoid MainWindow::onTrackerTimeUpdated(double absolute_time, bool do_replot)\n{\n  _tracker_delay.triggerSignal(100);\n\n  for (auto& it : _state_publisher)\n  {\n    it.second->updateState(absolute_time);\n  }\n\n  updateReactivePlots();\n\n  forEachWidget([&](PlotWidget* plot) {\n    plot->setTrackerPosition(_tracker_time);\n    if (do_replot)\n    {\n      plot->replot();\n    }\n  });\n}\n\nvoid MainWindow::initializeActions()\n{\n  _undo_shortcut.setContext(Qt::ApplicationShortcut);\n  _redo_shortcut.setContext(Qt::ApplicationShortcut);\n  _fullscreen_shortcut.setContext(Qt::ApplicationShortcut);\n\n  connect(&_undo_shortcut, &QShortcut::activated, this, &MainWindow::onUndoInvoked);\n  connect(&_redo_shortcut, &QShortcut::activated, this, &MainWindow::onRedoInvoked);\n  connect(&_streaming_shortcut, &QShortcut::activated, this,\n          &MainWindow::on_streamingToggled);\n  connect(&_playback_shotcut, &QShortcut::activated, ui->pushButtonPlay,\n          &QPushButton::toggle);\n  connect(&_fullscreen_shortcut, &QShortcut::activated, this,\n          &MainWindow::onActionFullscreenTriggered);\n\n  QShortcut* open_menu_shortcut = new QShortcut(QKeySequence(Qt::ALT + Qt::Key_F), this);\n  connect(open_menu_shortcut, &QShortcut::activated,\n          [this]() { ui->menuFile->exec(ui->menuBar->mapToGlobal(QPoint(0, 25))); });\n\n  QShortcut* open_help_shortcut = new QShortcut(QKeySequence(Qt::ALT + Qt::Key_H), this);\n  connect(open_help_shortcut, &QShortcut::activated,\n          [this]() { ui->menuHelp->exec(ui->menuBar->mapToGlobal(QPoint(230, 25))); });\n\n  //---------------------------------------------\n\n  QSettings settings;\n  updateRecentDataMenu(\n      settings.value("MainWindow.recentlyLoadedDatafile").toStringList());\n  updateRecentLayoutMenu(\n      settings.value("MainWindow.recentlyLoadedLayout").toStringList());\n}\n\nvoid MainWindow::loadAllPlugins(QStringList command_line_plugin_folders)\n{\n  QSettings settings;\n  QStringList loaded;\n  QStringList plugin_folders;\n  QStringList builtin_folders;\n\n  plugin_folders += command_line_plugin_folders;\n  plugin_folders +=\n      settings.value("Preferences::plugin_folders", QStringList()).toStringList();\n\n  builtin_folders += QCoreApplication::applicationDirPath();\n\n  try\n  {\n#ifdef COMPILED_WITH_CATKIN\n    builtin_folders += QCoreApplication::applicationDirPath() + "_ros";\n\n    const char* env = std::getenv("CMAKE_PREFIX_PATH");\n    if (env)\n    {\n      QString env_catkin_paths = QString::fromStdString(env);\n      env_catkin_paths.replace(";", ":");  // for windows\n      auto catkin_paths = env_catkin_paths.split(":");\n\n      for (const auto& path : catkin_paths)\n      {\n        builtin_folders += path + "/lib/plotjuggler_ros";\n      }\n    }\n#endif\n#ifdef COMPILED_WITH_AMENT\n    auto ros2_path = QString::fromStdString(ament_index_cpp::get_package_prefix("plotjugg"\n                                                                                "ler_"\n                                                                                "ros"));\n    ros2_path += "/lib/plotjuggler_ros";\n    loaded += initializePlugins(ros2_path);\n#endif\n  }\n  catch (...)\n  {\n    QMessageBox::warning(nullptr, "Missing package [plotjuggler-ros]",\n                         "If you just upgraded from PlotJuggler 2.x to 3.x , try "\n                         "installing this package:\\n\\n"\n                         "sudo apt install ros-${ROS_DISTRO}-plotjuggler-ros",\n                         QMessageBox::Cancel, QMessageBox::Cancel);\n  }\n\n  builtin_folders +=\n      QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + "/PlotJuggl"\n                                                                              "er";\n  builtin_folders.removeDuplicates();\n\n  plugin_folders += builtin_folders;\n  plugin_folders.removeDuplicates();\n\n  for (const auto& folder : plugin_folders)\n  {\n    loaded += initializePlugins(folder);\n  }\n\n  settings.setValue("Preferences::builtin_plugin_folders", builtin_folders);\n}\n\nQStringList MainWindow::initializePlugins(QString directory_name)\n{\n  static std::set<QString> loaded_plugins;\n  QStringList loaded_out;\n\n  qDebug() << "Loading compatible plugins from directory: " << directory_name;\n  int loaded_count = 0;\n\n  QDir pluginsDir(directory_name);\n\n  for (const QString& filename : pluginsDir.entryList(QDir::Files))\n  {\n    QFileInfo fileinfo(filename);\n    if (fileinfo.suffix() != "so" && fileinfo.suffix() != "dll" &&\n        fileinfo.suffix() != "dylib")\n    {\n      continue;\n    }\n\n    if (loaded_plugins.find(filename) != loaded_plugins.end())\n    {\n      continue;\n    }\n\n    QPluginLoader pluginLoader(pluginsDir.absoluteFilePath(filename), this);\n\n    QObject* plugin = pluginLoader.instance();\n    if (plugin && dynamic_cast<PlotJugglerPlugin*>(plugin))\n    {\n      auto class_name = pluginLoader.metaData().value("className").toString();\n      loaded_out.push_back(class_name);\n\n      DataLoader* loader = qobject_cast<DataLoader*>(plugin);\n      StatePublisher* publisher = qobject_cast<StatePublisher*>(plugin);\n      DataStreamer* streamer = qobject_cast<DataStreamer*>(plugin);\n      ParserFactoryPlugin* message_parser = qobject_cast<ParserFactoryPlugin*>(plugin);\n      ToolboxPlugin* toolbox = qobject_cast<ToolboxPlugin*>(plugin);\n\n      QString plugin_name;\n      QString plugin_type;\n      bool is_debug_plugin = dynamic_cast<PlotJugglerPlugin*>(plugin)->isDebugPlugin();\n\n      if (loader)\n      {\n        plugin_name = loader->name();\n        plugin_type = "DataLoader";\n      }\n      else if (publisher)\n      {\n        plugin_name = publisher->name();\n        plugin_type = "StatePublisher";\n      }\n      else if (streamer)\n      {\n        plugin_name = streamer->name();\n        plugin_type = "DataStreamer";\n      }\n      else if (message_parser)\n      {\n        plugin_name = message_parser->name();\n        plugin_type = "MessageParser";\n      }\n      else if (toolbox)\n      {\n        plugin_name = toolbox->name();\n        plugin_type = "Toolbox";\n      }\n\n      QString message = QString("%1 is a %2 plugin").arg(filename).arg(plugin_type);\n\n      if ((_enabled_plugins.size() > 0) &&\n          (_enabled_plugins.contains(fileinfo.baseName()) == false))\n      {\n        qDebug() << message << " ...skipping, because it is not explicitly enabled";\n        continue;\n      }\n      if ((_disabled_plugins.size() > 0) &&\n          (_disabled_plugins.contains(fileinfo.baseName()) == true))\n      {\n        qDebug() << message << " ...skipping, because it is explicitly disabled";\n        continue;\n      }\n      if (!_test_option && is_debug_plugin)\n      {\n        qDebug() << message << " ...disabled, unless option -t is used";\n        continue;\n      }\n      if (loaded_plugins.find(plugin_name) != loaded_plugins.end())\n      {\n        qDebug() << message << " ...skipping, because already loaded";\n        continue;\n      }\n\n      qDebug() << message;\n\n      loaded_plugins.insert(plugin_name);\n      loaded_count++;\n\n      if (loader)\n      {\n        _data_loader.insert(std::make_pair(plugin_name, loader));\n      }\n      else if (publisher)\n      {\n        publisher->setDataMap(&_mapped_plot_data);\n        _state_publisher.insert(std::make_pair(plugin_name, publisher));\n\n        ui->layoutPublishers->setColumnStretch(0, 1.0);\n\n        int row = _state_publisher.size() - 1;\n        auto label = new QLabel(plugin_name, ui->framePublishers);\n        ui->layoutPublishers->addWidget(label, row, 0);\n\n        auto start_checkbox = new QCheckBox(ui->framePublishers);\n        ui->layoutPublishers->addWidget(start_checkbox, row, 1);\n        start_checkbox->setFocusPolicy(Qt::FocusPolicy::NoFocus);\n\n        connect(start_checkbox, &QCheckBox::toggled, this,\n                [=](bool enable) { publisher->setEnabled(enable); });\n\n        connect(publisher, &StatePublisher::closed, start_checkbox,\n                [=]() { start_checkbox->setChecked(false); });\n\n        if (publisher->availableActions().empty())\n        {\n          QFrame* empty = new QFrame(ui->framePublishers);\n          empty->setFixedSize({ 22, 22 });\n          ui->layoutPublishers->addWidget(empty, row, 2);\n        }\n        else\n        {\n          auto options_button = new QPushButton(ui->framePublishers);\n          options_button->setFlat(true);\n          options_button->setFixedSize({ 24, 24 });\n          ui->layoutPublishers->addWidget(options_button, row, 2);\n\n          options_button->setIcon(LoadSvg(":/resources/svg/settings_cog.svg", "light"));\n          options_button->setIconSize({ 16, 16 });\n\n          auto optionsMenu = [=]() {\n            PopupMenu* menu = new PopupMenu(options_button, this);\n            for (auto action : publisher->availableActions())\n            {\n              menu->addAction(action);\n            }\n            menu->exec();\n          };\n\n          connect(options_button, &QPushButton::clicked, options_button, optionsMenu);\n\n          connect(this, &MainWindow::stylesheetChanged, options_button,\n                  [=](QString style) {\n                    options_button->setIcon(\n                        LoadSvg(":/resources/svg/settings_cog.svg", style));\n                  });\n        }\n      }\n      else if (message_parser)\n      {\n        _parser_factories.insert(\n            std::make_pair(message_parser->encoding(), message_parser));\n      }\n      else if (streamer)\n      {\n        if (_default_streamer == fileinfo.baseName())\n        {\n          _default_streamer = plugin_name;\n        }\n        _data_streamer.insert(std::make_pair(plugin_name, streamer));\n\n        connect(streamer, &DataStreamer::closed, this,\n                [this]() { this->stopStreamingPlugin(); });\n\n        connect(streamer, &DataStreamer::clearBuffers, this,\n                &MainWindow::on_actionClearBuffer_triggered);\n\n        connect(streamer, &DataStreamer::dataReceived, _animated_streaming_movie,\n                [this]() {\n                  _animated_streaming_movie->start();\n                  _animated_streaming_timer->start(500);\n                });\n\n        connect(streamer, &DataStreamer::removeGroup, this,\n                &MainWindow::on_deleteSerieFromGroup);\n\n        connect(streamer, &DataStreamer::dataReceived, this, [this]() {\n          if (isStreamingActive() && !_replot_timer->isActive())\n          {\n            _replot_timer->setSingleShot(true);\n            _replot_timer->start(40);\n          }\n        });\n\n        connect(streamer, &DataStreamer::notificationsChanged, this,\n                &MainWindow::on_streamingNotificationsChanged);\n      }\n      else if (toolbox)\n      {\n        toolbox->init(_mapped_plot_data, _transform_functions);\n\n        _toolboxes.insert(std::make_pair(plugin_name, toolbox));\n\n        auto action = ui->menuTools->addAction(toolbox->name());\n\n        int new_index = ui->widgetStack->count();\n        auto provided = toolbox->providedWidget();\n        auto widget = provided.first;\n        ui->widgetStack->addWidget(widget);\n\n        connect(action, &QAction::triggered, toolbox, &ToolboxPlugin::onShowWidget);\n\n        connect(action, &QAction::triggered, this,\n                [=]() { ui->widgetStack->setCurrentIndex(new_index); });\n\n        connect(toolbox, &ToolboxPlugin::closed, this,\n                [=]() { ui->widgetStack->setCurrentIndex(0); });\n\n        connect(toolbox, &ToolboxPlugin::plotCreated, this, [=](std::string name) {\n          _curvelist_widget->addCustom(QString::fromStdString(name));\n          _curvelist_widget->updateAppearance();\n          _curvelist_widget->clearSelections();\n        });\n      }\n    }\n    else\n    {\n      if (pluginLoader.errorString().contains("is not an ELF object") == false)\n      {\n        qDebug() << filename << ": " << pluginLoader.errorString();\n      }\n    }\n  }\n\n  for (auto& [name, streamer] : _data_streamer)\n  {\n    streamer->setParserFactories(&_parser_factories);\n  }\n\n  for (auto& [name, loader] : _data_loader)\n  {\n    loader->setParserFactories(&_parser_factories);\n  }\n\n  if (!_data_streamer.empty())\n  {\n    QSignalBlocker block(ui->comboStreaming);\n    ui->comboStreaming->setEnabled(true);\n    ui->buttonStreamingStart->setEnabled(true);\n\n    for (const auto& it : _data_streamer)\n    {\n      if (ui->comboStreaming->findText(it.first) == -1)\n      {\n        ui->comboStreaming->addItem(it.first);\n      }\n    }\n\n    // remember the previous one\n    QSettings settings;\n    QString streaming_name =\n        settings\n            .value("MainWindow.previousStreamingPlugin", ui->comboStreaming->itemText(0))\n            .toString();\n\n    auto streamer_it = _data_streamer.find(streaming_name);\n    if (streamer_it == _data_streamer.end())\n    {\n      streamer_it = _data_streamer.begin();\n      streaming_name = streamer_it->first;\n    }\n\n    ui->comboStreaming->setCurrentText(streaming_name);\n\n    bool contains_options = !streamer_it->second->availableActions().empty();\n    ui->buttonStreamingOptions->setEnabled(contains_options);\n  }\n  qDebug() << "Number of plugins loaded: " << loaded_count << "\\n";\n  return loaded_out;\n}\n\nvoid MainWindow::buildDummyData()\n{\n  PlotDataMapRef datamap;\n  BuildDummyData(datamap);\n  importPlotDataMap(datamap, true);\n}\n\nvoid MainWindow::on_splitterMoved(int, int)\n{\n  QList<int> sizes = ui->mainSplitter->sizes();\n  int max_left_size = _curvelist_widget->maximumWidth();\n  int totalWidth = sizes[0] + sizes[1];\n\n  // this is needed only once to restore the old size\n  static bool first = true;\n  if (sizes[0] != 0 && first)\n  {\n    first = false;\n    QSettings settings;\n    int splitter_width = settings.value("MainWindow.splitterWidth", 200).toInt();\n    auto sizes = ui->mainSplitter->sizes();\n    int tot_splitter_width = sizes[0] + sizes[1];\n    sizes[0] = splitter_width;\n    sizes[1] = tot_splitter_width - splitter_width;\n    ui->mainSplitter->setSizes(sizes);\n    return;\n  }\n\n  if (sizes[0] > max_left_size)\n  {\n    sizes[0] = max_left_size;\n    sizes[1] = totalWidth - max_left_size;\n    ui->mainSplitter->setSizes(sizes);\n  }\n}\n\nvoid MainWindow::resizeEvent(QResizeEvent*)\n{\n  on_splitterMoved(0, 0);\n}\n\nvoid MainWindow::onPlotAdded(PlotWidget* plot)\n{\n  connect(plot, &PlotWidget::undoableChange, this, &MainWindow::onUndoableChange);\n\n  connect(plot, &PlotWidget::trackerMoved, this, &MainWindow::onTrackerMovedFromWidget);\n\n  connect(this, &MainWindow::dataSourceRemoved, plot, &PlotWidget::onDataSourceRemoved);\n\n  connect(plot, &PlotWidget::curveListChanged, this, [this]() {\n    updateTimeOffset();\n    updateTimeSlider();\n  });\n\n  connect(&_time_offset, SIGNAL(valueChanged(double)), plot,\n          SLOT(on_changeTimeOffset(double)));\n\n  connect(ui->pushButtonUseDateTime, &QPushButton::toggled, plot,\n          &PlotWidget::on_changeDateTimeScale);\n\n  connect(plot, &PlotWidget::curvesDropped, _curvelist_widget,\n          &CurveListPanel::clearSelections);\n\n  connect(plot, &PlotWidget::legendSizeChanged, this, [=](int point_size) {\n    auto visitor = [=](PlotWidget* p) {\n      if (plot != p)\n        p->setLegendSize(point_size);\n    };\n    this->forEachWidget(visitor);\n  });\n\n  connect(plot, &PlotWidget::rectChanged, this, &MainWindow::onPlotZoomChanged);\n\n  plot->on_changeTimeOffset(_time_offset.get());\n  plot->on_changeDateTimeScale(ui->pushButtonUseDateTime->isChecked());\n  plot->activateGrid(ui->pushButtonActivateGrid->isChecked());\n  plot->enableTracker(!isStreamingActive());\n  plot->setKeepRatioXY(ui->pushButtonRatio->isChecked());\n  plot->configureTracker(_tracker_param);\n}\n\nvoid MainWindow::onPlotZoomChanged(PlotWidget* modified_plot, QRectF new_range)\n{\n  if (ui->pushButtonLink->isChecked())\n  {\n    auto visitor = [=](PlotWidget* plot) {\n      if (plot != modified_plot && !plot->isEmpty() && !plot->isXYPlot() &&\n          plot->isZoomLinkEnabled())\n      {\n        QRectF bound_act = plot->currentBoundingRect();\n        bound_act.setLeft(new_range.left());\n        bound_act.setRight(new_range.right());\n        plot->setZoomRectangle(bound_act, false);\n        plot->on_zoomOutVertical_triggered(false);\n        plot->replot();\n      }\n    };\n    this->forEachWidget(visitor);\n  }\n\n  onUndoableChange();\n}\n\nvoid MainWindow::onPlotTabAdded(PlotDocker* docker)\n{\n  connect(docker, &PlotDocker::plotWidgetAdded, this, &MainWindow::onPlotAdded);\n\n  connect(this, &MainWindow::stylesheetChanged, docker,\n          &PlotDocker::on_stylesheetChanged);\n\n  // TODO  connect(matrix, &PlotMatrix::undoableChange, this,\n  // &MainWindow::onUndoableChange);\n}\n\nQDomDocument MainWindow::xmlSaveState() const\n{\n  QDomDocument doc;\n  QDomProcessingInstruction instr = doc.createProcessingInstruction("xml", "version=\'1.0\'"\n                                                                           " encoding=\'"\n                                                                           "UTF-8\'");\n\n  doc.appendChild(instr);\n\n  QDomElement root = doc.createElement("root");\n\n  for (auto& it : TabbedPlotWidget::instances())\n  {\n    QDomElement tabbed_area = it.second->xmlSaveState(doc);\n    root.appendChild(tabbed_area);\n  }\n\n  doc.appendChild(root);\n\n  QDomElement relative_time = doc.createElement("use_relative_time_offset");\n  relative_time.setAttribute("enabled", ui->pushButtonRemoveTimeOffset->isChecked());\n  root.appendChild(relative_time);\n\n  return doc;\n}\n\nvoid MainWindow::checkAllCurvesFromLayout(const QDomElement& root)\n{\n  std::set<std::string> curves;\n\n  for (QDomElement tw = root.firstChildElement("tabbed_widget"); !tw.isNull();\n       tw = tw.nextSiblingElement("tabbed_widget"))\n  {\n    for (QDomElement pm = tw.firstChildElement("plotmatrix"); !pm.isNull();\n         pm = pm.nextSiblingElement("plotmatrix"))\n    {\n      for (QDomElement pl = pm.firstChildElement("plot"); !pl.isNull();\n           pl = pl.nextSiblingElement("plot"))\n      {\n        QDomElement tran_elem = pl.firstChildElement("transform");\n        std::string trans = tran_elem.attribute("value").toStdString();\n        bool is_XY_plot = (trans == "XYPlot");\n\n        for (QDomElement cv = pl.firstChildElement("curve"); !cv.isNull();\n             cv = cv.nextSiblingElement("curve"))\n        {\n          if (is_XY_plot)\n          {\n            curves.insert(cv.attribute("curve_x").toStdString());\n            curves.insert(cv.attribute("curve_y").toStdString());\n          }\n          else\n          {\n            curves.insert(cv.attribute("name").toStdString());\n          }\n        }\n      }\n    }\n  }\n\n  std::vector<std::string> missing_curves;\n\n  for (auto& curve_name : curves)\n  {\n    if (_mapped_plot_data.numeric.count(curve_name) == 0)\n    {\n      missing_curves.push_back(curve_name);\n    }\n    if (_mapped_plot_data.strings.count(curve_name) == 0)\n    {\n      missing_curves.push_back(curve_name);\n    }\n  }\n  if (missing_curves.size() > 0)\n  {\n    QMessageBox msgBox(this);\n    msgBox.setWindowTitle("Warning");\n    msgBox.setText(tr("One or more timeseries in the layout haven\'t been loaded yet\\n"\n                      "What do you want to do?"));\n\n    QPushButton* buttonRemove =\n        msgBox.addButton(tr("Remove curves from plots"), QMessageBox::RejectRole);\n    QPushButton* buttonPlaceholder =\n        msgBox.addButton(tr("Create empty placeholders"), QMessageBox::YesRole);\n    msgBox.setDefaultButton(buttonPlaceholder);\n    msgBox.exec();\n    if (msgBox.clickedButton() == buttonPlaceholder)\n    {\n      for (auto& name : missing_curves)\n      {\n        auto plot_it = _mapped_plot_data.addNumeric(name);\n        _curvelist_widget->addCurve(name);\n      }\n      _curvelist_widget->refreshColumns();\n    }\n  }\n}\n\nbool MainWindow::xmlLoadState(QDomDocument state_document)\n{\n  QDomElement root = state_document.namedItem("root").toElement();\n  if (root.isNull())\n  {\n    qWarning() << "No <root> element found at the top-level of the XML file!";\n    return false;\n  }\n\n  size_t num_floating = 0;\n  std::map<QString, QDomElement> tabbed_widgets_with_name;\n\n  for (QDomElement tw = root.firstChildElement("tabbed_widget"); tw.isNull() == false;\n       tw = tw.nextSiblingElement("tabbed_widget"))\n  {\n    if (tw.attribute("parent") != ("main_window"))\n    {\n      num_floating++;\n    }\n    tabbed_widgets_with_name[tw.attribute("name")] = tw;\n  }\n\n  // add if missing\n  for (const auto& it : tabbed_widgets_with_name)\n  {\n    if (TabbedPlotWidget::instance(it.first) == nullptr)\n    {\n      // TODO createTabbedDialog(it.first, nullptr);\n    }\n  }\n\n  // remove those which don\'t share list of names\n  for (const auto& it : TabbedPlotWidget::instances())\n  {\n    if (tabbed_widgets_with_name.count(it.first) == 0)\n    {\n      it.second->deleteLater();\n    }\n  }\n\n  //-----------------------------------------------------\n  checkAllCurvesFromLayout(root);\n  //-----------------------------------------------------\n\n  for (QDomElement tw = root.firstChildElement("tabbed_widget"); tw.isNull() == false;\n       tw = tw.nextSiblingElement("tabbed_widget"))\n  {\n    TabbedPlotWidget* tabwidget = TabbedPlotWidget::instance(tw.attribute("name"));\n    tabwidget->xmlLoadState(tw);\n  }\n\n  QDomElement relative_time = root.firstChildElement("use_relative_time_offset");\n  if (!relative_time.isNull())\n  {\n    bool remove_offset = (relative_time.attribute("enabled") == QString("1"));\n    ui->pushButtonRemoveTimeOffset->setChecked(remove_offset);\n  }\n  return true;\n}\n\nvoid MainWindow::onDeleteMultipleCurves(const std::vector<std::string>& curve_names)\n{\n  std::set<std::string> to_be_deleted;\n  for (auto& name : curve_names)\n  {\n    to_be_deleted.insert(name);\n  }\n  // add to the list of curves to delete the derived transforms\n  size_t prev_size = 0;\n  while (prev_size < to_be_deleted.size())\n  {\n    prev_size = to_be_deleted.size();\n    for (auto& [trans_name, transform] : _transform_functions)\n    {\n      for (const auto& source : transform->dataSources())\n      {\n        if (to_be_deleted.count(source->plotName()) > 0)\n        {\n          to_be_deleted.insert(trans_name);\n        }\n      }\n    }\n  }\n\n  for (const auto& curve_name : to_be_deleted)\n  {\n    emit dataSourceRemoved(curve_name);\n    _curvelist_widget->removeCurve(curve_name);\n    _mapped_plot_data.erase(curve_name);\n    _transform_functions.erase(curve_name);\n  }\n  updateTimeOffset();\n  forEachWidget([](PlotWidget* plot) { plot->replot(); });\n}\n\nvoid MainWindow::updateRecentDataMenu(QStringList new_filenames)\n{\n  QMenu* menu = _recent_data_files;\n\n  QAction* separator = nullptr;\n  QStringList prev_filenames;\n  for (QAction* action : menu->actions())\n  {\n    if (action->isSeparator())\n    {\n      separator = action;\n      break;\n    }\n    if (new_filenames.contains(action->text()) == false)\n    {\n      prev_filenames.push_back(action->text());\n    }\n    menu->removeAction(action);\n  }\n\n  new_filenames.append(prev_filenames);\n  while (new_filenames.size() > 10)\n  {\n    new_filenames.removeLast();\n  }\n\n  for (const auto& filename : new_filenames)\n  {\n    QAction* action = new QAction(filename, nullptr);\n    connect(action, &QAction::triggered, this,\n            [this, filename] { loadDataFromFiles({ filename }); });\n    menu->insertAction(separator, action);\n  }\n\n  QSettings settings;\n  settings.setValue("MainWindow.recentlyLoadedDatafile", new_filenames);\n  menu->setEnabled(new_filenames.size() > 0);\n}\n\nvoid MainWindow::updateRecentLayoutMenu(QStringList new_filenames)\n{\n  QMenu* menu = _recent_layout_files;\n\n  QAction* separator = nullptr;\n  QStringList prev_filenames;\n  for (QAction* action : menu->actions())\n  {\n    if (action->isSeparator())\n    {\n      separator = action;\n      break;\n    }\n    if (new_filenames.contains(action->text()) == false)\n    {\n      prev_filenames.push_back(action->text());\n    }\n    menu->removeAction(action);\n  }\n\n  new_filenames.append(prev_filenames);\n  while (new_filenames.size() > 10)\n  {\n    new_filenames.removeLast();\n  }\n\n  for (const auto& filename : new_filenames)\n  {\n    QAction* action = new QAction(filename, nullptr);\n    connect(action, &QAction::triggered, this, [this, filename] {\n      if (this->loadLayoutFromFile(filename))\n      {\n        updateRecentLayoutMenu({ filename });\n      }\n    });\n    menu->insertAction(separator, action);\n  }\n\n  QSettings settings;\n  settings.setValue("MainWindow.recentlyLoadedLayout", new_filenames);\n  menu->setEnabled(new_filenames.size() > 0);\n}\n\nvoid MainWindow::deleteAllData()\n{\n  forEachWidget([](PlotWidget* plot) { plot->removeAllCurves(); });\n\n  _mapped_plot_data.clear();\n  _transform_functions.clear();\n  _curvelist_widget->clear();\n  _loaded_datafiles.clear();\n  _undo_states.clear();\n  _redo_states.clear();\n\n  bool stopped = false;\n\n  for (int idx = 0; idx < ui->layoutPublishers->count(); idx++)\n  {\n    QLayoutItem* item = ui->layoutPublishers->itemAt(idx);\n    if (dynamic_cast<QWidgetItem*>(item))\n    {\n      if (auto checkbox = dynamic_cast<QCheckBox*>(item->widget()))\n      {\n        if (checkbox->isChecked())\n        {\n          checkbox->setChecked(false);\n          stopped = true;\n        }\n      }\n    }\n  }\n\n  if (stopped)\n  {\n    QMessageBox::warning(this, "State publishers stopped",\n                         "All the state publishers have been stopped because old data "\n                         "has been deleted.");\n  }\n}\n\nvoid MainWindow::importPlotDataMap(PlotDataMapRef& new_data, bool remove_old)\n{\n  if (remove_old)\n  {\n    auto ClearOldSeries = [](auto& prev_plot_data, auto& new_plot_data) {\n      for (auto& it : prev_plot_data)\n      {\n        // timeseries in both\n        if (new_plot_data.count(it.first) != 0)\n        {\n          it.second.clear();\n        }\n      }\n    };\n\n    ClearOldSeries(_mapped_plot_data.scatter_xy, new_data.scatter_xy);\n    ClearOldSeries(_mapped_plot_data.numeric, new_data.numeric);\n    ClearOldSeries(_mapped_plot_data.strings, new_data.strings);\n  }\n\n  auto [added_curves, curve_updated, data_pushed] =\n      MoveData(new_data, _mapped_plot_data, remove_old);\n\n  for (const auto& added_curve : added_curves)\n  {\n    _curvelist_widget->addCurve(added_curve);\n  }\n\n  if (curve_updated)\n  {\n    _curvelist_widget->refreshColumns();\n  }\n}\n\nbool MainWindow::isStreamingActive() const\n{\n  return !ui->buttonStreamingPause->isChecked() && _active_streamer_plugin;\n}\n\nbool MainWindow::loadDataFromFiles(QStringList filenames)\n{\n  filenames.sort();\n  std::map<QString, QString> filename_prefix;\n\n  if (filenames.size() > 1)\n  {\n    DialogMultifilePrefix dialog(filenames, this);\n    int ret = dialog.exec();\n    if (ret != QDialog::Accepted)\n    {\n      return false;\n    }\n    filename_prefix = dialog.getPrefixes();\n  }\n\n  std::unordered_set<std::string> previous_names = _mapped_plot_data.getAllNames();\n\n  QStringList loaded_filenames;\n\n  for (int i = 0; i < filenames.size(); i++)\n  {\n    FileLoadInfo info;\n    info.filename = filenames[i];\n    if (filename_prefix.count(info.filename) > 0)\n    {\n      info.prefix = filename_prefix[info.filename];\n    }\n    auto added_names = loadDataFromFile(info);\n    if (!added_names.empty())\n    {\n      loaded_filenames.push_back(filenames[i]);\n    }\n    for (const auto& name : added_names)\n    {\n      previous_names.erase(name);\n    }\n  }\n\n  bool data_replaced_entirely = false;\n\n  if (previous_names.empty())\n  {\n    data_replaced_entirely = true;\n  }\n  else\n  {\n    QMessageBox::StandardButton reply;\n    reply = QMessageBox::question(\n        this, tr("Warning"), tr("Do you want to remove the previously loaded data?\\n"),\n        QMessageBox::Yes | QMessageBox::No, QMessageBox::NoButton);\n\n    if (reply == QMessageBox::Yes)\n    {\n      std::vector<std::string> to_delete;\n      for (const auto& name : previous_names)\n      {\n        to_delete.push_back(name);\n      }\n      onDeleteMultipleCurves(to_delete);\n      data_replaced_entirely = true;\n    }\n  }\n\n  // special case when only the last file should be remembered\n  if (loaded_filenames.size() == 1 && data_replaced_entirely &&\n      _loaded_datafiles.size() > 1)\n  {\n    std::swap(_loaded_datafiles.back(), _loaded_datafiles.front());\n    _loaded_datafiles.resize(1);\n  }\n\n  if (loaded_filenames.size() > 0)\n  {\n    updateRecentDataMenu(loaded_filenames);\n    linkedZoomOut();\n    return true;\n  }\n  return false;\n}\n\nstd::unordered_set<std::string> MainWindow::loadDataFromFile(const FileLoadInfo& info)\n{\n  ui->pushButtonPlay->setChecked(false);\n\n  const QString extension = QFileInfo(info.filename).suffix().toLower();\n\n  typedef std::map<QString, DataLoaderPtr>::iterator MapIterator;\n\n  std::vector<MapIterator> compatible_loaders;\n\n  for (auto it = _data_loader.begin(); it != _data_loader.end(); ++it)\n  {\n    DataLoaderPtr data_loader = it->second;\n    std::vector<const char*> extensions = data_loader->compatibleFileExtensions();\n\n    for (auto& ext : extensions)\n    {\n      if (extension == QString(ext).toLower())\n      {\n        compatible_loaders.push_back(it);\n        break;\n      }\n    }\n  }\n\n  DataLoaderPtr dataloader;\n  std::unordered_set<std::string> added_names;\n\n  if (compatible_loaders.size() == 1)\n  {\n    dataloader = compatible_loaders.front()->second;\n  }\n  else\n  {\n    static QString last_plugin_name_used;\n\n    QStringList names;\n    for (auto& cl : compatible_loaders)\n    {\n      const auto& name = cl->first;\n\n      if (name == last_plugin_name_used)\n      {\n        names.push_front(name);\n      }\n      else\n      {\n        names.push_back(name);\n      }\n    }\n\n    bool ok;\n    QString plugin_name =\n        QInputDialog::getItem(this, tr("QInputDialog::getItem()"),\n                              tr("Select the loader to use:"), names, 0, false, &ok);\n    if (ok && !plugin_name.isEmpty() && (_enabled_plugins.size() == 0 || _enabled_plugins.contains(plugin_name)))\n    {\n      dataloader = _data_loader[plugin_name];\n      last_plugin_name_used = plugin_name;\n    }\n  }\n\n  if (dataloader)\n  {\n    QFile file(info.filename);\n\n    if (!file.open(QFile::ReadOnly | QFile::Text))\n    {\n      QMessageBox::warning(\n          this, tr("Datafile"),\n          tr("Cannot read file %1:\\n%2.").arg(info.filename).arg(file.errorString()));\n      return {};\n    }\n    file.close();\n\n    try\n    {\n      PlotDataMapRef mapped_data;\n      FileLoadInfo new_info = info;\n\n      if (dataloader->readDataFromFile(&new_info, mapped_data))\n      {\n        AddPrefixToPlotData(info.prefix.toStdString(), mapped_data.numeric);\n        AddPrefixToPlotData(info.prefix.toStdString(), mapped_data.strings);\n\n        added_names = mapped_data.getAllNames();\n        importPlotDataMap(mapped_data, true);\n\n        QDomElement plugin_elem = dataloader->xmlSaveState(new_info.plugin_config);\n        new_info.plugin_config.appendChild(plugin_elem);\n\n        bool duplicate = false;\n\n        // substitute an old item of _loaded_datafiles or push_back another item.\n        for (auto& prev_loaded : _loaded_datafiles)\n        {\n          if (prev_loaded.filename == new_info.filename &&\n              prev_loaded.prefix == new_info.prefix)\n          {\n            prev_loaded = new_info;\n            duplicate = true;\n            break;\n          }\n        }\n\n        if (!duplicate)\n        {\n          _loaded_datafiles.push_back(new_info);\n        }\n      }\n    }\n    catch (std::exception& ex)\n    {\n      QMessageBox::warning(this, tr("Exception from the plugin"),\n                           tr("The plugin [%1] thrown the following exception: \\n\\n %3\\n")\n                               .arg(dataloader->name())\n                               .arg(ex.what()));\n      return {};\n    }\n  }\n  else\n  {\n    QMessageBox::warning(this, tr("Error"),\n                         tr("Cannot read files with extension %1.\\n No plugin can handle "\n                            "that!\\n")\n                             .arg(info.filename));\n  }\n  _curvelist_widget->updateFilter();\n\n  // clean the custom plot. Function updateDataAndReplot will update them\n  for (auto& custom_it : _transform_functions)\n  {\n    auto it = _mapped_plot_data.numeric.find(custom_it.first);\n    if (it != _mapped_plot_data.numeric.end())\n    {\n      it->second.clear();\n    }\n    custom_it.second->reset();\n  }\n  forEachWidget([](PlotWidget* plot) { plot->updateCurves(true); });\n\n  updateDataAndReplot(true);\n  ui->timeSlider->setRealValue(ui->timeSlider->getMinimum());\n\n  return added_names;\n}\n\nvoid MainWindow::on_buttonStreamingNotifications_clicked()\n{\n  if (_data_streamer.empty())\n  {\n    return;\n  }\n  auto streamer = _data_streamer.at(ui->comboStreaming->currentText());\n  QAction* notification_button_action = streamer->notificationAction().first;\n  if (notification_button_action != nullptr)\n  {\n    notification_button_action->trigger();\n  }\n}\n\nvoid MainWindow::on_buttonStreamingPause_toggled(bool paused)\n{\n  if (!_active_streamer_plugin)\n  {\n    paused = true;\n  }\n\n  ui->pushButtonRemoveTimeOffset->setEnabled(paused);\n  ui->widgetPlay->setEnabled(paused);\n\n  if (!paused && ui->pushButtonPlay->isChecked())\n  {\n    ui->pushButtonPlay->setChecked(false);\n  }\n\n  forEachWidget([&](PlotWidget* plot) {\n    plot->enableTracker(paused);\n    plot->setZoomEnabled(paused);\n  });\n\n  if (!paused)\n  {\n    updateTimeOffset();\n  }\n  else\n  {\n    onUndoableChange();\n  }\n}\n\nvoid MainWindow::on_streamingToggled()\n{\n  if (_active_streamer_plugin)\n  {\n    bool prev_state = ui->buttonStreamingPause->isChecked();\n    ui->buttonStreamingPause->setChecked(!prev_state);\n  }\n}\n\nvoid MainWindow::stopStreamingPlugin()\n{\n  ui->comboStreaming->setEnabled(true);\n  ui->buttonStreamingStart->setText("Start");\n  ui->buttonStreamingPause->setEnabled(false);\n  ui->labelStreamingAnimation->setHidden(true);\n  enableStreamingNotificationsButton(false);\n\n  // force the cleanups typically done in on_buttonStreamingPause_toggled\n  if (ui->buttonStreamingPause->isChecked())\n  {\n    // Will call on_buttonStreamingPause_toggled\n    ui->buttonStreamingPause->setChecked(false);\n  }\n  else\n  {\n    // call it manually\n    on_buttonStreamingPause_toggled(true);\n  }\n\n  if (_active_streamer_plugin)\n  {\n    _active_streamer_plugin->shutdown();\n    _active_streamer_plugin = nullptr;\n  }\n\n  if (!_mapped_plot_data.numeric.empty())\n  {\n    ui->actionDeleteAllData->setToolTip("");\n  }\n\n  // reset max range.\n  _mapped_plot_data.setMaximumRangeX(std::numeric_limits<double>::max());\n}\n\nvoid MainWindow::startStreamingPlugin(QString streamer_name)\n{\n  if (_active_streamer_plugin)\n  {\n    _active_streamer_plugin->shutdown();\n    _active_streamer_plugin = nullptr;\n  }\n\n  if (_data_streamer.empty())\n  {\n    qDebug() << "Error, no streamer loaded";\n    return;\n  }\n\n  auto it = _data_streamer.find(streamer_name);\n  if (it != _data_streamer.end())\n  {\n    _active_streamer_plugin = it->second;\n  }\n  else\n  {\n    qDebug() << "Error. The streamer " << streamer_name << " can\'t be loaded";\n    _active_streamer_plugin = nullptr;\n    return;\n  }\n\n  bool started = false;\n  try\n  {\n    // TODO data sources (argument to _active_streamer_plugin->start()\n    started = _active_streamer_plugin && _active_streamer_plugin->start(nullptr);\n  }\n  catch (std::runtime_error& err)\n  {\n    QMessageBox::warning(\n        this, tr("Exception from the plugin"),\n        tr("The plugin thrown the following exception: \\n\\n %1\\n").arg(err.what()));\n    _active_streamer_plugin = nullptr;\n    return;\n  }\n\n  // The attemp to start the plugin may have succeded or failed\n  if (started)\n  {\n    {\n      std::lock_guard<std::mutex> lock(_active_streamer_plugin->mutex());\n      importPlotDataMap(_active_streamer_plugin->dataMap(), false);\n    }\n\n    ui->actionClearBuffer->setEnabled(true);\n    ui->actionDeleteAllData->setToolTip("Stop streaming to be able to delete the data");\n\n    ui->buttonStreamingStart->setText("Stop");\n    ui->buttonStreamingPause->setEnabled(true);\n    ui->buttonStreamingPause->setChecked(false);\n    ui->comboStreaming->setEnabled(false);\n    ui->labelStreamingAnimation->setHidden(false);\n\n    // force start\n    on_buttonStreamingPause_toggled(false);\n    // this will force the update the max buffer size values\n    on_streamingSpinBox_valueChanged(ui->streamingSpinBox->value());\n  }\n  else\n  {\n    QSignalBlocker block(ui->buttonStreamingStart);\n    ui->buttonStreamingStart->setChecked(false);\n    qDebug() << "Failed to launch the streamer";\n    _active_streamer_plugin = nullptr;\n  }\n}\n\nvoid MainWindow::enableStreamingNotificationsButton(bool enabled)\n{\n  ui->buttonStreamingNotifications->setEnabled(enabled);\n\n  QSettings settings;\n  QString theme = settings.value("Preferences::theme", "light").toString();\n\n  if (enabled)\n  {\n    ui->buttonStreamingNotifications->setIcon(\n        LoadSvg(":/resources/svg/alarm-bell-active.svg", theme));\n  }\n  else\n  {\n    ui->buttonStreamingNotifications->setIcon(\n        LoadSvg(":/resources/svg/alarm-bell.svg", theme));\n  }\n}\n\nvoid MainWindow::loadStyleSheet(QString file_path)\n{\n  QFile styleFile(file_path);\n  styleFile.open(QFile::ReadOnly);\n  try\n  {\n    QString theme = SetApplicationStyleSheet(styleFile.readAll());\n\n    forEachWidget([&](PlotWidget* plot) { plot->replot(); });\n\n    _curvelist_widget->updateAppearance();\n    emit stylesheetChanged(theme);\n  }\n  catch (std::runtime_error& err)\n  {\n    QMessageBox::warning(this, tr("Error loading StyleSheet"), tr(err.what()));\n  }\n}\n\nvoid MainWindow::updateDerivedSeries()\n{\n  for (auto& [id, series] : _transform_functions)\n  {\n  }\n}\n\nvoid MainWindow::updateReactivePlots()\n{\n  std::unordered_set<std::string> updated_curves;\n\n  bool curve_added = false;\n  for (auto& it : _transform_functions)\n  {\n    if (auto reactive_function =\n            std::dynamic_pointer_cast<PJ::ReactiveLuaFunction>(it.second))\n    {\n      reactive_function->setTimeTracker(_tracker_time);\n      reactive_function->calculate();\n\n      for (auto& name : reactive_function->createdCurves())\n      {\n        curve_added |= _curvelist_widget->addCurve(name);\n        updated_curves.insert(name);\n      }\n    }\n  }\n  if (curve_added)\n  {\n    _curvelist_widget->refreshColumns();\n  }\n\n  forEachWidget([&](PlotWidget* plot) {\n    for (auto& curve : plot->curveList())\n    {\n      if (updated_curves.count(curve.src_name) != 0)\n      {\n        plot->replot();\n      }\n    }\n  });\n}\n\nvoid MainWindow::dragEnterEvent(QDragEnterEvent* event)\n{\n  if (event->mimeData()->hasUrls())\n  {\n    event->acceptProposedAction();\n  }\n}\n\nvoid MainWindow::dropEvent(QDropEvent* event)\n{\n  QStringList file_names;\n  const auto urls = event->mimeData()->urls();\n\n  for (const auto& url : urls)\n  {\n    file_names << QDir::toNativeSeparators(url.toLocalFile());\n  }\n\n  loadDataFromFiles(file_names);\n}\n\nvoid MainWindow::on_stylesheetChanged(QString theme)\n{\n  ui->pushButtonLoadDatafile->setIcon(LoadSvg(":/resources/svg/import.svg", theme));\n  ui->buttonStreamingPause->setIcon(LoadSvg(":/resources/svg/pause.svg", theme));\n  if (ui->buttonStreamingNotifications->isEnabled())\n  {\n    ui->buttonStreamingNotifications->setIcon(\n        LoadSvg(":/resources/svg/alarm-bell-active.svg", theme));\n  }\n  else\n  {\n    ui->buttonStreamingNotifications->setIcon(\n        LoadSvg(":/resources/svg/alarm-bell.svg", theme));\n  }\n  ui->buttonRecentData->setIcon(LoadSvg(":/resources/svg/right-arrow.svg", theme));\n  ui->buttonRecentLayout->setIcon(LoadSvg(":/resources/svg/right-arrow.svg", theme));\n\n  ui->pushButtonZoomOut->setIcon(LoadSvg(":/resources/svg/zoom_max.svg", theme));\n  ui->playbackLoop->setIcon(LoadSvg(":/resources/svg/loop.svg", theme));\n  ui->pushButtonPlay->setIcon(LoadSvg(":/resources/svg/play_arrow.svg", theme));\n  ui->pushButtonUseDateTime->setIcon(LoadSvg(":/resources/svg/datetime.svg", theme));\n  ui->pushButtonActivateGrid->setIcon(LoadSvg(":/resources/svg/grid.svg", theme));\n  ui->pushButtonRatio->setIcon(LoadSvg(":/resources/svg/ratio.svg", theme));\n\n  ui->pushButtonLoadLayout->setIcon(LoadSvg(":/resources/svg/import.svg", theme));\n  ui->pushButtonSaveLayout->setIcon(LoadSvg(":/resources/svg/export.svg", theme));\n\n  ui->pushButtonLink->setIcon(LoadSvg(":/resources/svg/link.svg", theme));\n  ui->pushButtonRemoveTimeOffset->setIcon(LoadSvg(":/resources/svg/t0.svg", theme));\n  ui->pushButtonLegend->setIcon(LoadSvg(":/resources/svg/legend.svg", theme));\n\n  ui->buttonStreamingOptions->setIcon(LoadSvg(":/resources/svg/settings_cog.svg", theme));\n}\n\nvoid MainWindow::loadPluginState(const QDomElement& root)\n{\n  QDomElement plugins = root.firstChildElement("Plugins");\n\n  for (QDomElement plugin_elem = plugins.firstChildElement();\n       plugin_elem.isNull() == false; plugin_elem = plugin_elem.nextSiblingElement())\n  {\n    const QString plugin_name = plugin_elem.attribute("ID");\n\n    if (plugin_elem.nodeName() != "plugin" || plugin_name.isEmpty())\n    {\n      QMessageBox::warning(this, tr("Error loading Plugin State from Layout"),\n                           tr("The method xmlSaveState() must return a node like this "\n                              "<plugin ID=\\"PluginName\\" "));\n    }\n\n    if (_data_loader.find(plugin_name) != _data_loader.end())\n    {\n      _data_loader[plugin_name]->xmlLoadState(plugin_elem);\n    }\n    if (_data_streamer.find(plugin_name) != _data_streamer.end())\n    {\n      _data_streamer[plugin_name]->xmlLoadState(plugin_elem);\n    }\n    if (_toolboxes.find(plugin_name) != _toolboxes.end())\n    {\n      _toolboxes[plugin_name]->xmlLoadState(plugin_elem);\n    }\n    if (_state_publisher.find(plugin_name) != _state_publisher.end())\n    {\n      StatePublisherPtr publisher = _state_publisher[plugin_name];\n      publisher->xmlLoadState(plugin_elem);\n\n      if (_autostart_publishers && plugin_elem.attribute("status") == "active")\n      {\n        publisher->setEnabled(true);\n      }\n    }\n  }\n}\n\nQDomElement MainWindow::savePluginState(QDomDocument& doc)\n{\n  QDomElement list_plugins = doc.createElement("Plugins");\n\n  auto AddPlugins = [&](auto& plugins) {\n    for (auto& [name, plugin] : plugins)\n    {\n      QDomElement elem = plugin->xmlSaveState(doc);\n      list_plugins.appendChild(elem);\n    }\n  };\n\n  AddPlugins(_data_loader);\n  AddPlugins(_data_streamer);\n  AddPlugins(_toolboxes);\n  AddPlugins(_state_publisher);\n\n  for (auto& it : _state_publisher)\n  {\n    const auto& state_publisher = it.second;\n    QDomElement plugin_elem = state_publisher->xmlSaveState(doc);\n    plugin_elem.setAttribute("status", state_publisher->enabled() ? "active" : "idle");\n  }\n\n  return list_plugins;\n}\n\nstd::tuple<double, double, int> MainWindow::calculateVisibleRangeX()\n{\n  // find min max time\n  double min_time = std::numeric_limits<double>::max();\n  double max_time = std::numeric_limits<double>::lowest();\n  int max_steps = 0;\n\n  forEachWidget([&](const PlotWidget* widget) {\n    for (auto& it : widget->curveList())\n    {\n      const auto& curve_name = it.src_name;\n\n      auto plot_it = _mapped_plot_data.numeric.find(curve_name);\n      if (plot_it == _mapped_plot_data.numeric.end())\n      {\n        continue;  // FIXME?\n      }\n      const auto& data = plot_it->second;\n      if (data.size() >= 1)\n      {\n        const double t0 = data.front().x;\n        const double t1 = data.back().x;\n        min_time = std::min(min_time, t0);\n        max_time = std::max(max_time, t1);\n        max_steps = std::max(max_steps, (int)data.size());\n      }\n    }\n  });\n\n  // needed if all the plots are empty\n  if (max_steps == 0 || max_time < min_time)\n  {\n    for (const auto& it : _mapped_plot_data.numeric)\n    {\n      const PlotData& data = it.second;\n      if (data.size() >= 1)\n      {\n        const double t0 = data.front().x;\n        const double t1 = data.back().x;\n        min_time = std::min(min_time, t0);\n        max_time = std::max(max_time, t1);\n        max_steps = std::max(max_steps, (int)data.size());\n      }\n    }\n  }\n\n  // last opportunity. Everything else failed\n  if (max_steps == 0 || max_time < min_time)\n  {\n    min_time = 0.0;\n    max_time = 1.0;\n    max_steps = 1;\n  }\n  return std::tuple<double, double, int>(min_time, max_time, max_steps);\n}\n\nbool MainWindow::loadLayoutFromFile(QString filename)\n{\n  QSettings settings;\n\n  QFile file(filename);\n  if (!file.open(QFile::ReadOnly | QFile::Text))\n  {\n    QMessageBox::warning(\n        this, tr("Layout"),\n        tr("Cannot read file %1:\\n%2.").arg(filename).arg(file.errorString()));\n    return false;\n  }\n\n  QString errorStr;\n  int errorLine, errorColumn;\n\n  QDomDocument domDocument;\n\n  if (!domDocument.setContent(&file, true, &errorStr, &errorLine, &errorColumn))\n  {\n    QMessageBox::information(\n        window(), tr("XML Layout"),\n        tr("Parse error at line %1:\\n%2").arg(errorLine).arg(errorStr));\n    return false;\n  }\n\n  //-------------------------------------------------\n  // refresh plugins\n  QDomElement root = domDocument.namedItem("root").toElement();\n\n  loadPluginState(root);\n  //-------------------------------------------------\n  QDomElement previously_loaded_datafile = root.firstChildElement("previouslyLoaded_"\n                                                                  "Datafiles");\n\n  QDomElement datafile_elem = previously_loaded_datafile.firstChildElement("fileInfo");\n  while (!datafile_elem.isNull())\n  {\n    QString datafile_path = datafile_elem.attribute("filename");\n    if (QDir(datafile_path).isRelative())\n    {\n      QDir layout_directory = QFileInfo(filename).absoluteDir();\n      QString new_path = layout_directory.filePath(datafile_path);\n      datafile_path = QFileInfo(new_path).absoluteFilePath();\n    }\n\n    FileLoadInfo info;\n    info.filename = datafile_path;\n    info.prefix = datafile_elem.attribute("prefix");\n\n    QDomElement datasources_elem = datafile_elem.firstChildElement("selected_"\n                                                                   "datasources");\n    QString topics_list = datasources_elem.attribute("value");\n    info.selected_datasources = topics_list.split(";", QString::SkipEmptyParts);\n\n    auto plugin_elem = datafile_elem.firstChildElement("plugin");\n    info.plugin_config.appendChild(info.plugin_config.importNode(plugin_elem, true));\n\n    loadDataFromFile(info);\n    datafile_elem = datafile_elem.nextSiblingElement("fileInfo");\n  }\n\n  QDomElement previous_streamer = root.firstChildElement("previouslyLoaded_Streamer");\n  if (!previous_streamer.isNull())\n  {\n    QString streamer_name = previous_streamer.attribute("name");\n\n    QMessageBox msgBox(this);\n    msgBox.setWindowTitle("Start Streaming?");\n    msgBox.setText(\n        tr("Start the previously used streaming plugin?\\n\\n %1 \\n\\n").arg(streamer_name));\n    QPushButton* yes = msgBox.addButton(tr("Yes"), QMessageBox::YesRole);\n    QPushButton* no = msgBox.addButton(tr("No"), QMessageBox::RejectRole);\n    msgBox.setDefaultButton(yes);\n    msgBox.exec();\n\n    if (msgBox.clickedButton() == yes)\n    {\n      if (_data_streamer.count(streamer_name) != 0)\n      {\n        auto allCurves = readAllCurvesFromXML(root);\n\n        // create placeholders, if necessary\n        for (auto curve_name : allCurves)\n        {\n          std::string curve_str = curve_name.toStdString();\n          if (_mapped_plot_data.numeric.count(curve_str) == 0)\n          {\n            _mapped_plot_data.addNumeric(curve_str);\n          }\n        }\n\n        startStreamingPlugin(streamer_name);\n      }\n      else\n      {\n        QMessageBox::warning(\n            this, tr("Error Loading Streamer"),\n            tr("The streamer named %1 can not be loaded.").arg(streamer_name));\n      }\n    }\n  }\n  //-------------------------------------------------\n  // autostart_publishers\n  QDomElement plugins = root.firstChildElement("Plugins");\n\n  if (!plugins.isNull() && _autostart_publishers)\n  {\n    for (QDomElement plugin_elem = plugins.firstChildElement();\n         plugin_elem.isNull() == false; plugin_elem = plugin_elem.nextSiblingElement())\n    {\n      const QString plugin_name = plugin_elem.nodeName();\n      if (_state_publisher.find(plugin_name) != _state_publisher.end())\n      {\n        StatePublisherPtr publisher = _state_publisher[plugin_name];\n\n        if (plugin_elem.attribute("status") == "active")\n        {\n          publisher->setEnabled(true);\n        }\n      }\n    }\n  }\n  //-------------------------------------------------\n  auto custom_equations = root.firstChildElement("customMathEquations");\n\n  if (!custom_equations.isNull())\n  {\n    using SnippetPair = std::pair<SnippetData, QDomElement>;\n    std::vector<SnippetPair> snippets;\n\n    for (QDomElement custom_eq = custom_equations.firstChildElement("snippet");\n         custom_eq.isNull() == false; custom_eq = custom_eq.nextSiblingElement("snippet"))\n    {\n      snippets.push_back({ GetSnippetFromXML(custom_eq), custom_eq });\n    }\n    // A custom plot may depend on other custom plots.\n    // Reorder them to respect the mutual depencency.\n    auto DependOn = [](const SnippetPair& a, const SnippetPair& b) {\n      if (b.first.linked_source == a.first.alias_name)\n      {\n        return true;\n      }\n      for (const auto& source : b.first.additional_sources)\n      {\n        if (source == a.first.alias_name)\n        {\n          return true;\n        }\n      }\n      return false;\n    };\n    std::sort(snippets.begin(), snippets.end(), DependOn);\n\n    for (const auto& [snippet, custom_eq] : snippets)\n    {\n      try\n      {\n        CustomPlotPtr new_custom_plot = std::make_shared<LuaCustomFunction>(snippet);\n        new_custom_plot->xmlLoadState(custom_eq);\n\n        new_custom_plot->calculateAndAdd(_mapped_plot_data);\n        const auto& alias_name = new_custom_plot->aliasName();\n        _curvelist_widget->addCustom(alias_name);\n\n        _transform_functions.insert({ alias_name.toStdString(), new_custom_plot });\n      }\n      catch (std::runtime_error& err)\n      {\n        QMessageBox::warning(this, tr("Exception"),\n                             tr("Failed to load customMathEquation [%1] \\n\\n %2\\n")\n                                 .arg(snippet.alias_name)\n                                 .arg(err.what()));\n      }\n    }\n    _curvelist_widget->refreshColumns();\n  }\n\n  auto colormaps = root.firstChildElement("colorMaps");\n\n  if (!colormaps.isNull())\n  {\n    for (auto colormap = colormaps.firstChildElement("colorMap");\n         colormap.isNull() == false; colormap = colormap.nextSiblingElement("colorMap"))\n    {\n      QString name = colormap.attribute("name");\n      ColorMapLibrary()[name]->setScrip(colormap.text());\n    }\n  }\n\n  QByteArray snippets_saved_xml =\n      settings.value("AddCustomPlotDialog.savedXML", QByteArray()).toByteArray();\n\n  auto snippets_element = root.firstChildElement("snippets");\n  if (!snippets_element.isNull())\n  {\n    auto snippets_previous = GetSnippetsFromXML(snippets_saved_xml);\n    auto snippets_layout = GetSnippetsFromXML(snippets_element);\n\n    bool snippets_are_different = false;\n    for (const auto& snippet_it : snippets_layout)\n    {\n      auto prev_it = snippets_previous.find(snippet_it.first);\n\n      if (prev_it == snippets_previous.end() ||\n          prev_it->second.function != snippet_it.second.function ||\n          prev_it->second.global_vars != snippet_it.second.global_vars)\n      {\n        snippets_are_different = true;\n        break;\n      }\n    }\n\n    if (snippets_are_different)\n    {\n      QMessageBox msgBox(this);\n      msgBox.setWindowTitle("Overwrite custom transforms?");\n      msgBox.setText("Your layout file contains a set of custom transforms different "\n                     "from "\n                     "the last one you used.\\nWant to load these transformations?");\n      msgBox.addButton(QMessageBox::No);\n      msgBox.addButton(QMessageBox::Yes);\n      msgBox.setDefaultButton(QMessageBox::Yes);\n\n      if (msgBox.exec() == QMessageBox::Yes)\n      {\n        for (const auto& snippet_it : snippets_layout)\n        {\n          snippets_previous[snippet_it.first] = snippet_it.second;\n        }\n        QDomDocument doc;\n        auto snippets_root_element = ExportSnippets(snippets_previous, doc);\n        doc.appendChild(snippets_root_element);\n        settings.setValue("AddCustomPlotDialog.savedXML", doc.toByteArray(2));\n      }\n    }\n  }\n\n  ///--------------------------------------------------\n\n  xmlLoadState(domDocument);\n\n  linkedZoomOut();\n\n  _undo_states.clear();\n  _undo_states.push_back(domDocument);\n  return true;\n}\n\nvoid MainWindow::linkedZoomOut()\n{\n  if (ui->pushButtonLink->isChecked())\n  {\n    for (const auto& it : TabbedPlotWidget::instances())\n    {\n      auto tabs = it.second->tabWidget();\n      for (int t = 0; t < tabs->count(); t++)\n      {\n        if (PlotDocker* matrix = dynamic_cast<PlotDocker*>(tabs->widget(t)))\n        {\n          bool first = true;\n          Range range;\n          // find the ideal zoom\n          for (int index = 0; index < matrix->plotCount(); index++)\n          {\n            PlotWidget* plot = matrix->plotAt(index);\n            if (plot->isEmpty())\n            {\n              continue;\n            }\n\n            auto rect = plot->maxZoomRect();\n            if (first)\n            {\n              range.min = rect.left();\n              range.max = rect.right();\n              first = false;\n            }\n            else\n            {\n              range.min = std::min(rect.left(), range.min);\n              range.max = std::max(rect.right(), range.max);\n            }\n          }\n\n          for (int index = 0; index < matrix->plotCount() && !first; index++)\n          {\n            PlotWidget* plot = matrix->plotAt(index);\n            if (plot->isEmpty())\n            {\n              continue;\n            }\n            QRectF bound_act = plot->maxZoomRect();\n            bound_act.setLeft(range.min);\n            bound_act.setRight(range.max);\n            plot->setZoomRectangle(bound_act, false);\n            plot->replot();\n          }\n        }\n      }\n    }\n  }\n  else\n  {\n    this->forEachWidget([](PlotWidget* plot) { plot->zoomOut(false); });\n  }\n}\n\nvoid MainWindow::on_tabbedAreaDestroyed(QObject* object)\n{\n  this->setFocus();\n}\n\nvoid MainWindow::forEachWidget(\n    std::function<void(PlotWidget*, PlotDocker*, int)> operation)\n{\n  auto func = [&](QTabWidget* tabs) {\n    for (int t = 0; t < tabs->count(); t++)\n    {\n      PlotDocker* matrix = dynamic_cast<PlotDocker*>(tabs->widget(t));\n      if (!matrix)\n      {\n        continue;\n      }\n\n      for (int index = 0; index < matrix->plotCount(); index++)\n      {\n        PlotWidget* plot = matrix->plotAt(index);\n        operation(plot, matrix, index);\n      }\n    }\n  };\n\n  for (const auto& it : TabbedPlotWidget::instances())\n  {\n    func(it.second->tabWidget());\n  }\n}\n\nvoid MainWindow::forEachWidget(std::function<void(PlotWidget*)> op)\n{\n  forEachWidget([&](PlotWidget* plot, PlotDocker*, int) { op(plot); });\n}\n\nvoid MainWindow::updateTimeSlider()\n{\n  auto range = calculateVisibleRangeX();\n\n  ui->timeSlider->setLimits(std::get<0>(range), std::get<1>(range), std::get<2>(range));\n\n  _tracker_time = std::max(_tracker_time, ui->timeSlider->getMinimum());\n  _tracker_time = std::min(_tracker_time, ui->timeSlider->getMaximum());\n}\n\nvoid MainWindow::updateTimeOffset()\n{\n  auto range = calculateVisibleRangeX();\n  double min_time = std::get<0>(range);\n\n  const bool remove_offset = ui->pushButtonRemoveTimeOffset->isChecked();\n  if (remove_offset && min_time != std::numeric_limits<double>::max())\n  {\n    _time_offset.set(min_time);\n  }\n  else\n  {\n    _time_offset.set(0.0);\n  }\n}\n\nvoid MainWindow::updateDataAndReplot(bool replot_hidden_tabs)\n{\n  _replot_timer->stop();\n\n  MoveDataRet move_ret;\n\n  if (_active_streamer_plugin)\n  {\n    {\n      std::lock_guard<std::mutex> lock(_active_streamer_plugin->mutex());\n      move_ret = MoveData(_active_streamer_plugin->dataMap(), _mapped_plot_data, false);\n    }\n\n    for (const auto& str : move_ret.added_curves)\n    {\n      _curvelist_widget->addCurve(str);\n    }\n\n    if (move_ret.curves_updated)\n    {\n      _curvelist_widget->refreshColumns();\n    }\n\n    _mapped_plot_data.setMaximumRangeX(ui->streamingSpinBox->value());\n  }\n\n  const bool is_streaming_active = isStreamingActive();\n\n  //--------------------------------\n  std::vector<TransformFunction*> transforms;\n  transforms.reserve(_transform_functions.size());\n  for (auto& [id, function] : _transform_functions)\n  {\n    transforms.push_back(function.get());\n  }\n  std::sort(\n      transforms.begin(), transforms.end(),\n      [](TransformFunction* a, TransformFunction* b) { return a->order() < b->order(); });\n\n  // Update the reactive plots\n  updateReactivePlots();\n\n  // update all transforms, but not the ReactiveLuaFunction\n  for (auto& function : transforms)\n  {\n    if (dynamic_cast<ReactiveLuaFunction*>(function) == nullptr)\n    {\n      function->calculate();\n    }\n  }\n\n  forEachWidget([](PlotWidget* plot) { plot->updateCurves(false); });\n\n  //--------------------------------\n  // trigger again the execution of this callback if steaming == true\n  if (is_streaming_active)\n  {\n    auto range = calculateVisibleRangeX();\n    double max_time = std::get<1>(range);\n    _tracker_time = max_time;\n    onTrackerTimeUpdated(_tracker_time, false);\n  }\n  else\n  {\n    updateTimeOffset();\n    updateTimeSlider();\n  }\n  //--------------------------------\n  linkedZoomOut();\n}\n\nvoid MainWindow::on_streamingSpinBox_valueChanged(int value)\n{\n  double real_value = value;\n\n  if (isStreamingActive() == false)\n  {\n    return;\n  }\n\n  _mapped_plot_data.setMaximumRangeX(real_value);\n\n  if (_active_streamer_plugin)\n  {\n    _active_streamer_plugin->setMaximumRangeX(real_value);\n  }\n}\n\nvoid MainWindow::on_actionExit_triggered()\n{\n  this->close();\n}\n\nvoid MainWindow::on_pushButtonRemoveTimeOffset_toggled(bool)\n{\n  updateTimeOffset();\n  updatedDisplayTime();\n\n  forEachWidget([](PlotWidget* plot) { plot->replot(); });\n\n  if (this->signalsBlocked() == false)\n  {\n    onUndoableChange();\n  }\n}\n\nvoid MainWindow::updatedDisplayTime()\n{\n  QLineEdit* timeLine = ui->displayTime;\n  const double relative_time = _tracker_time - _time_offset.get();\n  if (ui->pushButtonUseDateTime->isChecked())\n  {\n    if (ui->pushButtonRemoveTimeOffset->isChecked())\n    {\n      QTime time = QTime::fromMSecsSinceStartOfDay(std::round(relative_time * 1000.0));\n      timeLine->setText(time.toString("HH:mm::ss.zzz"));\n    }\n    else\n    {\n      QDateTime datetime =\n          QDateTime::fromMSecsSinceEpoch(std::round(_tracker_time * 1000.0));\n      timeLine->setText(datetime.toString("[yyyy MMM dd] HH:mm::ss.zzz"));\n    }\n  }\n  else\n  {\n    timeLine->setText(QString::number(relative_time, \'f\', 3));\n  }\n\n  QFontMetrics fm(timeLine->font());\n  int width = fm.width(timeLine->text()) + 10;\n  timeLine->setFixedWidth(std::max(100, width));\n}\n\nvoid MainWindow::on_pushButtonActivateGrid_toggled(bool checked)\n{\n  forEachWidget([checked](PlotWidget* plot) {\n    plot->activateGrid(checked);\n    plot->replot();\n  });\n}\n\nvoid MainWindow::on_pushButtonRatio_toggled(bool checked)\n{\n  forEachWidget([checked](PlotWidget* plot) {\n    plot->setKeepRatioXY(checked);\n    plot->replot();\n  });\n}\n\nvoid MainWindow::on_pushButtonPlay_toggled(bool checked)\n{\n  if (checked)\n  {\n    _publish_timer->start();\n    _prev_publish_time = QDateTime::currentDateTime();\n  }\n  else\n  {\n    _publish_timer->stop();\n  }\n}\n\nvoid MainWindow::on_actionClearBuffer_triggered()\n{\n  for (auto& it : _mapped_plot_data.numeric)\n  {\n    it.second.clear();\n  }\n\n  for (auto& it : _mapped_plot_data.strings)\n  {\n    it.second.clear();\n  }\n\n  for (auto& it : _mapped_plot_data.user_defined)\n  {\n    it.second.clear();\n  }\n\n  for (auto& it : _transform_functions)\n  {\n    it.second->reset();\n  }\n\n  forEachWidget([](PlotWidget* plot) {\n    plot->reloadPlotData();\n    plot->replot();\n  });\n}\n\nvoid MainWindow::on_deleteSerieFromGroup(std::string group_name)\n{\n  std::vector<std::string> names;\n\n  auto AddFromGroup = [&](auto& series) {\n    for (auto& it : series)\n    {\n      const auto& group = it.second.group();\n      if (group && group->name() == group_name)\n      {\n        names.push_back(it.first);\n      }\n    }\n  };\n  AddFromGroup(_mapped_plot_data.numeric);\n  AddFromGroup(_mapped_plot_data.strings);\n  AddFromGroup(_mapped_plot_data.user_defined);\n\n  onDeleteMultipleCurves(names);\n}\n\nvoid MainWindow::on_streamingNotificationsChanged(int active_count)\n{\n  if (active_count > 0 && _active_streamer_plugin)\n  {\n    enableStreamingNotificationsButton(true);\n\n    QString tooltipText = QString("%1 has %2 outstanding notitication%3")\n                              .arg(_active_streamer_plugin->name())\n                              .arg(active_count)\n                              .arg(active_count > 1 ? "s" : "");\n    ui->buttonStreamingNotifications->setToolTip(tooltipText);\n  }\n  else\n  {\n    enableStreamingNotificationsButton(false);\n    ui->buttonStreamingNotifications->setToolTip("View streaming alerts");\n  }\n}\n\nvoid MainWindow::on_pushButtonUseDateTime_toggled(bool checked)\n{\n  static bool first = true;\n  if (checked && ui->pushButtonRemoveTimeOffset->isChecked())\n  {\n    if (first)\n    {\n      QMessageBox::information(this, tr("Note"),\n                               tr("When \\"Use Date Time\\" is checked, the option "\n                                  "\\"Remove Time Offset\\" "\n                                  "is automatically disabled.\\n"\n                                  "This message will be shown only once."));\n      first = false;\n    }\n    ui->pushButtonRemoveTimeOffset->setChecked(false);\n  }\n  updatedDisplayTime();\n}\n\nvoid MainWindow::on_pushButtonTimeTracker_pressed()\n{\n  if (_tracker_param == CurveTracker::LINE_ONLY)\n  {\n    _tracker_param = CurveTracker::VALUE;\n  }\n  else if (_tracker_param == CurveTracker::VALUE)\n  {\n    _tracker_param = CurveTracker::VALUE_NAME;\n  }\n  else if (_tracker_param == CurveTracker::VALUE_NAME)\n  {\n    _tracker_param = CurveTracker::LINE_ONLY;\n  }\n  ui->pushButtonTimeTracker->setIcon(_tracker_button_icons[_tracker_param]);\n\n  forEachWidget([&](PlotWidget* plot) {\n    plot->configureTracker(_tracker_param);\n    plot->replot();\n  });\n}\n\nvoid MainWindow::closeEvent(QCloseEvent* event)\n{\n  _replot_timer->stop();\n  _publish_timer->stop();\n\n  if (_active_streamer_plugin)\n  {\n    _active_streamer_plugin->shutdown();\n    _active_streamer_plugin = nullptr;\n  }\n  QSettings settings;\n  settings.setValue("MainWindow.geometry", saveGeometry());\n  settings.setValue("MainWindow.state", saveState());\n\n  settings.setValue("MainWindow.activateGrid", ui->pushButtonActivateGrid->isChecked());\n  settings.setValue("MainWindow.removeTimeOffset",\n                    ui->pushButtonRemoveTimeOffset->isChecked());\n  settings.setValue("MainWindow.dateTimeDisplay", ui->pushButtonUseDateTime->isChecked());\n  settings.setValue("MainWindow.buttonLink", ui->pushButtonLink->isChecked());\n  settings.setValue("MainWindow.buttonRatio", ui->pushButtonRatio->isChecked());\n\n  settings.setValue("MainWindow.streamingBufferValue", ui->streamingSpinBox->value());\n  settings.setValue("MainWindow.timeTrackerSetting", (int)_tracker_param);\n  settings.setValue("MainWindow.splitterWidth", ui->mainSplitter->sizes()[0]);\n\n  _data_loader.clear();\n  _data_streamer.clear();\n  _state_publisher.clear();\n  _toolboxes.clear();\n}\n\nvoid MainWindow::onAddCustomPlot(const std::string& plot_name)\n{\n  ui->widgetStack->setCurrentIndex(1);\n  _function_editor->setLinkedPlotName(QString::fromStdString(plot_name));\n  _function_editor->createNewPlot();\n}\n\nvoid MainWindow::onEditCustomPlot(const std::string& plot_name)\n{\n  ui->widgetStack->setCurrentIndex(1);\n  auto custom_it = _transform_functions.find(plot_name);\n  if (custom_it == _transform_functions.end())\n  {\n    qWarning("failed to find custom equation");\n    return;\n  }\n  _function_editor->editExistingPlot(\n      std::dynamic_pointer_cast<LuaCustomFunction>(custom_it->second));\n}\n\nvoid MainWindow::onRefreshCustomPlot(const std::string& plot_name)\n{\n  try\n  {\n    auto custom_it = _transform_functions.find(plot_name);\n    if (custom_it == _transform_functions.end())\n    {\n      qWarning("failed to find custom equation");\n      return;\n    }\n    CustomPlotPtr ce = std::dynamic_pointer_cast<LuaCustomFunction>(custom_it->second);\n    ce->calculateAndAdd(_mapped_plot_data);\n\n    onUpdateLeftTableValues();\n    updateDataAndReplot(true);\n  }\n  catch (const std::runtime_error& e)\n  {\n    QMessageBox::critical(this, "error",\n                          "Failed to refresh data : " + QString::fromStdString(e.what()));\n  }\n}\n\nvoid MainWindow::onPlaybackLoop()\n{\n  qint64 delta_ms =\n      (QDateTime::currentMSecsSinceEpoch() - _prev_publish_time.toMSecsSinceEpoch());\n  _prev_publish_time = QDateTime::currentDateTime();\n  delta_ms = std::max((qint64)_publish_timer->interval(), delta_ms);\n\n  _tracker_time += delta_ms * 0.001 * ui->playbackRate->value();\n  if (_tracker_time >= ui->timeSlider->getMaximum())\n  {\n    if (!ui->playbackLoop->isChecked())\n    {\n      ui->pushButtonPlay->setChecked(false);\n    }\n    _tracker_time = ui->timeSlider->getMinimum();\n  }\n  //////////////////\n  auto prev = ui->timeSlider->blockSignals(true);\n  ui->timeSlider->setRealValue(_tracker_time);\n  ui->timeSlider->blockSignals(prev);\n\n  //////////////////\n  updatedDisplayTime();\n  onUpdateLeftTableValues();\n\n  for (auto& it : _state_publisher)\n  {\n    it.second->play(_tracker_time);\n  }\n\n  forEachWidget([&](PlotWidget* plot) {\n    plot->setTrackerPosition(_tracker_time);\n    plot->replot();\n  });\n}\n\nvoid MainWindow::onCustomPlotCreated(std::vector<CustomPlotPtr> custom_plots)\n{\n  std::set<PlotWidget*> widget_to_replot;\n\n  for (auto custom_plot : custom_plots)\n  {\n    const std::string& curve_name = custom_plot->aliasName().toStdString();\n    // clear already existing data first\n    auto data_it = _mapped_plot_data.numeric.find(curve_name);\n    if (data_it != _mapped_plot_data.numeric.end())\n    {\n      data_it->second.clear();\n    }\n    try\n    {\n      custom_plot->calculateAndAdd(_mapped_plot_data);\n    }\n    catch (std::exception& ex)\n    {\n      QMessageBox::warning(this, tr("Warning"),\n                           tr("Failed to create the custom timeseries. "\n                              "Error:\\n\\n%1")\n                               .arg(ex.what()));\n    }\n\n    // keep data for reference\n    auto custom_it = _transform_functions.find(curve_name);\n    if (custom_it == _transform_functions.end())\n    {\n      _transform_functions.insert({ curve_name, custom_plot });\n      _curvelist_widget->addCustom(QString::fromStdString(curve_name));\n    }\n    else\n    {\n      custom_it->second = custom_plot;\n    }\n\n    forEachWidget([&](PlotWidget* plot) {\n      if (plot->curveFromTitle(QString::fromStdString(curve_name)))\n      {\n        widget_to_replot.insert(plot);\n      }\n    });\n  }\n\n  onUpdateLeftTableValues();\n  ui->widgetStack->setCurrentIndex(0);\n  _function_editor->clear();\n\n  for (auto plot : widget_to_replot)\n  {\n    plot->updateCurves(true);\n    plot->replot();\n  }\n  _curvelist_widget->clearSelections();\n}\n\nvoid MainWindow::on_actionReportBug_triggered()\n{\n  QDesktopServices::openUrl(QUrl("https://github.com/facontidavide/PlotJuggler/issues"));\n}\n\nvoid MainWindow::on_actionShare_the_love_triggered()\n{\n  QDesktopServices::openUrl(QUrl("https://twitter.com/intent/"\n                                 "tweet?hashtags=PlotJuggler"));\n}\n\nvoid MainWindow::on_actionAbout_triggered()\n{\n  QDialog* dialog = new QDialog(this);\n  auto ui = new Ui::AboutDialog();\n  ui->setupUi(dialog);\n\n  ui->label_version->setText(QString("version: ") + QApplication::applicationVersion());\n  dialog->setAttribute(Qt::WA_DeleteOnClose);\n\n  QFile fileTitle(_skin_path + "/about_window_title.html");\n  if (fileTitle.open(QIODevice::ReadOnly))\n  {\n    ui->titleTextBrowser->setHtml(fileTitle.readAll());\n  }\n\n  QFile fileBody(_skin_path + "/about_window_body.html");\n  if (fileBody.open(QIODevice::ReadOnly))\n  {\n    ui->bodyTextBrowser->setHtml(fileBody.readAll());\n  }\n\n  dialog->setAttribute(Qt::WA_DeleteOnClose);\n  dialog->exec();\n}\n\nvoid MainWindow::on_actionCheatsheet_triggered()\n{\n  QSettings settings;\n\n  CheatsheetDialog* dialog = new CheatsheetDialog(this);\n  dialog->restoreGeometry(settings.value("Cheatsheet.geometry").toByteArray());\n  dialog->exec();\n  settings.setValue("Cheatsheet.geometry", dialog->saveGeometry());\n  dialog->deleteLater();\n}\n\nvoid MainWindow::on_actionSupportPlotJuggler_triggered()\n{\n  QDialog* dialog = new QDialog(this);\n  auto ui = new Ui::SupportDialog();\n  ui->setupUi(dialog);\n\n  dialog->setAttribute(Qt::WA_DeleteOnClose);\n\n  dialog->exec();\n}\n\n/*\nvoid MainWindow::on_actionSaveAllPlotTabs_triggered()\n{\n  QSettings settings;\n  QString directory_path = settings.value("MainWindow.saveAllPlotTabs",\nQDir::currentPath()).toString();\n  // Get destination folder\n  QFileDialog saveDialog(this);\n  saveDialog.setDirectory(directory_path);\n  saveDialog.setFileMode(QFileDialog::FileMode::Directory);\n  saveDialog.setAcceptMode(QFileDialog::AcceptSave);\n  saveDialog.exec();\n\n  uint image_number = 1;\n  if (saveDialog.result() == QDialog::Accepted && !saveDialog.selectedFiles().empty())\n  {\n    // Save Plots\n    QString directory = saveDialog.selectedFiles().first();\n    settings.setValue("MainWindow.saveAllPlotTabs", directory);\n\n    QStringList file_names;\n    QStringList existing_files;\n    QDateTime current_date_time(QDateTime::currentDateTime());\n    QString current_date_time_name(current_date_time.toString("yyyy-MM-dd_HH-mm-ss"));\n    for (const auto& it : TabbedPlotWidget::instances())\n    {\n      auto tab_widget = it.second->tabWidget();\n      for (int i = 0; i < tab_widget->count(); i++)\n      {\n        PlotDocker* matrix = static_cast<PlotDocker*>(tab_widget->widget(i));\n        QString name = QString("%1/%2_%3_%4.png")\n                           .arg(directory)\n                           .arg(current_date_time_name)\n                           .arg(image_number, 2, 10, QLatin1Char(\'0\'))\n                           .arg(matrix->name());\n        file_names.push_back(name);\n        image_number++;\n\n        QFileInfo check_file(file_names.back());\n        if (check_file.exists() && check_file.isFile())\n        {\n          existing_files.push_back(name);\n        }\n      }\n    }\n    if (existing_files.isEmpty() == false)\n    {\n      QMessageBox msgBox;\n      msgBox.setText("One or more files will be overwritten. ant to continue?");\n      QString all_files;\n      for (const auto& str : existing_files)\n      {\n        all_files.push_back("\\n");\n        all_files.append(str);\n      }\n      msgBox.setInformativeText(all_files);\n      msgBox.setStandardButtons(QMessageBox::Cancel | QMessageBox::Ok);\n      msgBox.setDefaultButton(QMessageBox::Ok);\n\n      if (msgBox.exec() != QMessageBox::Ok)\n      {\n        return;\n      }\n    }\n\n    image_number = 0;\n    for (const auto& it : TabbedPlotWidget::instances())\n    {\n      auto tab_widget = it.second->tabWidget();\n      for (int i = 0; i < tab_widget->count(); i++)\n      {\n        PlotDocker* matrix = static_cast<PlotDocker*>(tab_widget->widget(i));\n        TabbedPlotWidget::saveTabImage(file_names[image_number], matrix);\n        image_number++;\n      }\n    }\n  }\n}*/\n\nvoid MainWindow::on_pushButtonLoadDatafile_clicked()\n{\n  if (_data_loader.empty())\n  {\n    QMessageBox::warning(this, tr("Warning"),\n                         tr("No plugin was loaded to process a data file\\n"));\n    return;\n  }\n\n  QSettings settings;\n\n  QString file_extension_filter;\n\n  std::set<QString> extensions;\n\n  for (auto& it : _data_loader)\n  {\n    DataLoaderPtr loader = it.second;\n    for (QString extension : loader->compatibleFileExtensions())\n    {\n      extensions.insert(extension.toLower());\n    }\n  }\n\n  for (const auto& it : extensions)\n  {\n    file_extension_filter.append(QString(" *.") + it);\n  }\n\n  QString directory_path =\n      settings.value("MainWindow.lastDatafileDirectory", QDir::currentPath()).toString();\n\n  QFileDialog loadDialog(this);\n  loadDialog.setFileMode(QFileDialog::ExistingFiles);\n  loadDialog.setViewMode(QFileDialog::Detail);\n  loadDialog.setNameFilter(file_extension_filter);\n  loadDialog.setDirectory(directory_path);\n\n  QStringList fileNames;\n  if (loadDialog.exec())\n  {\n    fileNames = loadDialog.selectedFiles();\n  }\n\n  if (fileNames.isEmpty())\n  {\n    return;\n  }\n\n  directory_path = QFileInfo(fileNames[0]).absolutePath();\n  settings.setValue("MainWindow.lastDatafileDirectory", directory_path);\n\n  if (loadDataFromFiles(fileNames))\n  {\n    updateRecentDataMenu(fileNames);\n  }\n}\n\nvoid MainWindow::on_pushButtonLoadLayout_clicked()\n{\n  QSettings settings;\n\n  QString directory_path =\n      settings.value("MainWindow.lastLayoutDirectory", QDir::currentPath()).toString();\n  QString filename =\n      QFileDialog::getOpenFileName(this, "Open Layout", directory_path, "*.xml");\n  if (filename.isEmpty())\n  {\n    return;\n  }\n\n  if (loadLayoutFromFile(filename))\n  {\n    updateRecentLayoutMenu({ filename });\n  }\n\n  directory_path = QFileInfo(filename).absolutePath();\n  settings.setValue("MainWindow.lastLayoutDirectory", directory_path);\n}\n\nvoid MainWindow::on_pushButtonSaveLayout_clicked()\n{\n  QDomDocument doc = xmlSaveState();\n\n  QSettings settings;\n\n  QString directory_path =\n      settings.value("MainWindow.lastLayoutDirectory", QDir::currentPath()).toString();\n\n  QFileDialog saveDialog(this);\n  saveDialog.setOption(QFileDialog::DontUseNativeDialog, true);\n\n  QGridLayout* save_layout = static_cast<QGridLayout*>(saveDialog.layout());\n\n  QFrame* frame = new QFrame;\n  frame->setFrameStyle(QFrame::Box | QFrame::Plain);\n  frame->setLineWidth(1);\n\n  QVBoxLayout* vbox = new QVBoxLayout;\n  QLabel* title = new QLabel("Save Layout options");\n  QFrame* separator = new QFrame;\n  separator->setFrameStyle(QFrame::HLine | QFrame::Plain);\n\n  auto checkbox_datasource = new QCheckBox("Save data source");\n  checkbox_datasource->setToolTip("the layout will remember the source of your data,\\n"\n                                  "i.e. the Datafile used or the Streaming Plugin loaded "\n                                  "?");\n  checkbox_datasource->setFocusPolicy(Qt::NoFocus);\n  checkbox_datasource->setChecked(\n      settings.value("MainWindow.saveLayoutDataSource", true).toBool());\n\n  auto checkbox_snippets = new QCheckBox("Save Scripts (transforms and colormaps)");\n  checkbox_snippets->setToolTip("Do you want the layout to save your Lua scripts?");\n  checkbox_snippets->setFocusPolicy(Qt::NoFocus);\n  checkbox_snippets->setChecked(\n      settings.value("MainWindow.saveLayoutSnippets", true).toBool());\n\n  vbox->addWidget(title);\n  vbox->addWidget(separator);\n  vbox->addWidget(checkbox_datasource);\n  vbox->addWidget(checkbox_snippets);\n  frame->setLayout(vbox);\n\n  int rows = save_layout->rowCount();\n  int col = save_layout->columnCount();\n  save_layout->addWidget(frame, 0, col, rows, 1, Qt::AlignTop);\n\n  saveDialog.setAcceptMode(QFileDialog::AcceptSave);\n  saveDialog.setDefaultSuffix("xml");\n  saveDialog.setNameFilter("XML (*.xml)");\n  saveDialog.setDirectory(directory_path);\n  saveDialog.exec();\n\n  if (saveDialog.result() != QDialog::Accepted || saveDialog.selectedFiles().empty())\n  {\n    return;\n  }\n\n  QString fileName = saveDialog.selectedFiles().first();\n\n  if (fileName.isEmpty())\n  {\n    return;\n  }\n\n  directory_path = QFileInfo(fileName).absolutePath();\n  settings.setValue("MainWindow.lastLayoutDirectory", directory_path);\n  settings.setValue("MainWindow.saveLayoutDataSource", checkbox_datasource->isChecked());\n  settings.setValue("MainWindow.saveLayoutSnippets", checkbox_snippets->isChecked());\n\n  QDomElement root = doc.namedItem("root").toElement();\n\n  root.appendChild(doc.createComment(" - - - - - - - - - - - - - - "));\n\n  root.appendChild(doc.createComment(" - - - - - - - - - - - - - - "));\n\n  root.appendChild(savePluginState(doc));\n\n  root.appendChild(doc.createComment(" - - - - - - - - - - - - - - "));\n\n  if (checkbox_datasource->isChecked())\n  {\n    QDomElement loaded_list = doc.createElement("previouslyLoaded_Datafiles");\n\n    for (const auto& loaded : _loaded_datafiles)\n    {\n      QString loaded_datafile = QDir(directory_path).relativeFilePath(loaded.filename);\n\n      QDomElement file_elem = doc.createElement("fileInfo");\n      file_elem.setAttribute("filename", loaded_datafile);\n      file_elem.setAttribute("prefix", loaded.prefix);\n\n      QDomElement datasources_elem = doc.createElement("selected_datasources");\n      QString topics_list = loaded.selected_datasources.join(";");\n      datasources_elem.setAttribute("value", topics_list);\n      file_elem.appendChild(datasources_elem);\n\n      file_elem.appendChild(loaded.plugin_config.firstChild());\n      loaded_list.appendChild(file_elem);\n    }\n    root.appendChild(loaded_list);\n\n    if (_active_streamer_plugin)\n    {\n      QDomElement loaded_streamer = doc.createElement("previouslyLoaded_Streamer");\n      QString streamer_name = _active_streamer_plugin->name();\n      loaded_streamer.setAttribute("name", streamer_name);\n      root.appendChild(loaded_streamer);\n    }\n  }\n  //-----------------------------------\n  root.appendChild(doc.createComment(" - - - - - - - - - - - - - - "));\n  if (checkbox_snippets->isChecked())\n  {\n    QDomElement custom_equations = doc.createElement("customMathEquations");\n    for (const auto& custom_it : _transform_functions)\n    {\n      const auto& custom_plot = custom_it.second;\n      custom_plot->xmlSaveState(doc, custom_equations);\n    }\n    root.appendChild(custom_equations);\n\n    QByteArray snippets_xml_text =\n        settings.value("AddCustomPlotDialog.savedXML", QByteArray()).toByteArray();\n    auto snipped_saved = GetSnippetsFromXML(snippets_xml_text);\n    auto snippets_root = ExportSnippets(snipped_saved, doc);\n    root.appendChild(snippets_root);\n\n    QDomElement color_maps = doc.createElement("colorMaps");\n    for (const auto& it : ColorMapLibrary())\n    {\n      QString colormap_name = it.first;\n      QDomElement colormap = doc.createElement("colorMap");\n      QDomText colormap_script = doc.createTextNode(it.second->script());\n      colormap.setAttribute("name", colormap_name);\n      colormap.appendChild(colormap_script);\n      color_maps.appendChild(colormap);\n    }\n  }\n  root.appendChild(doc.createComment(" - - - - - - - - - - - - - - "));\n  //------------------------------------\n  QFile file(fileName);\n  if (file.open(QIODevice::WriteOnly))\n  {\n    QTextStream stream(&file);\n    stream << doc.toString() << endl;\n  }\n}\n\nvoid MainWindow::onActionFullscreenTriggered()\n{\n  static bool first_call = true;\n  if (first_call && !_minimized)\n  {\n    first_call = false;\n    QMessageBox::information(this, "Remember!",\n                             "Press F10 to switch back to the normal view");\n  }\n\n  _minimized = !_minimized;\n\n  ui->leftMainWindowFrame->setVisible(!_minimized);\n  //  ui->widgetOptions->setVisible(!_minimized && ui->pushButtonOptions->isChecked());\n  ui->widgetTimescale->setVisible(!_minimized);\n  ui->menuBar->setVisible(!_minimized);\n\n  for (auto& it : TabbedPlotWidget::instances())\n  {\n    it.second->setControlsVisible(!_minimized);\n  }\n}\n\nvoid MainWindow::on_actionClearRecentData_triggered()\n{\n  QMenu* menu = _recent_data_files;\n  for (QAction* action : menu->actions())\n  {\n    if (action->isSeparator())\n    {\n      break;\n    }\n    menu->removeAction(action);\n  }\n  menu->setEnabled(false);\n  QSettings settings;\n  settings.setValue("MainWindow.recentlyLoadedDatafile", {});\n}\n\nvoid MainWindow::on_actionClearRecentLayout_triggered()\n{\n  QMenu* menu = _recent_layout_files;\n  for (QAction* action : menu->actions())\n  {\n    if (action->isSeparator())\n    {\n      break;\n    }\n    menu->removeAction(action);\n  }\n  menu->setEnabled(false);\n  QSettings settings;\n  settings.setValue("MainWindow.recentlyLoadedLayout", {});\n}\n\nvoid MainWindow::on_actionDeleteAllData_triggered()\n{\n  QMessageBox msgBox(this);\n  msgBox.setWindowTitle("Warning. Can\'t be undone.");\n  msgBox.setText(tr("Do you want to remove the previously loaded data?\\n"));\n  msgBox.addButton(QMessageBox::No);\n  msgBox.addButton(QMessageBox::Yes);\n  msgBox.setDefaultButton(QMessageBox::Yes);\n  auto reply = msgBox.exec();\n\n  if (reply == QMessageBox::No)\n  {\n    return;\n  }\n\n  deleteAllData();\n}\n\nvoid MainWindow::on_actionPreferences_triggered()\n{\n  QSettings settings;\n  QString prev_style = settings.value("Preferences::theme", "light").toString();\n\n  PreferencesDialog dialog;\n  dialog.exec();\n\n  QString theme = settings.value("Preferences::theme").toString();\n\n  if (theme != prev_style)\n  {\n    loadStyleSheet(tr(":/resources/stylesheet_%1.qss").arg(theme));\n  }\n}\n\nvoid MainWindow::on_playbackStep_valueChanged(double step)\n{\n  ui->timeSlider->setFocus();\n  ui->timeSlider->setRealStepValue(step);\n}\n\nvoid MainWindow::on_actionLoadStyleSheet_triggered()\n{\n  QSettings settings;\n  QString directory_path =\n      settings.value("MainWindow.loadStyleSheetDirectory", QDir::currentPath())\n          .toString();\n\n  QString fileName = QFileDialog::getOpenFileName(this, tr("Load StyleSheet"),\n                                                  directory_path, tr("(*.qss)"));\n  if (fileName.isEmpty())\n  {\n    return;\n  }\n\n  loadStyleSheet(fileName);\n\n  directory_path = QFileInfo(fileName).absolutePath();\n  settings.setValue("MainWindow.loadStyleSheetDirectory", directory_path);\n}\n\nvoid MainWindow::on_pushButtonLegend_clicked()\n{\n  switch (_labels_status)\n  {\n    case LabelStatus::LEFT:\n      _labels_status = LabelStatus::HIDDEN;\n      break;\n    case LabelStatus::RIGHT:\n      _labels_status = LabelStatus::LEFT;\n      break;\n    case LabelStatus::HIDDEN:\n      _labels_status = LabelStatus::RIGHT;\n      break;\n  }\n\n  auto visitor = [=](PlotWidget* plot) {\n    plot->activateLegend(_labels_status != LabelStatus::HIDDEN);\n\n    if (_labels_status == LabelStatus::LEFT)\n    {\n      plot->setLegendAlignment(Qt::AlignLeft);\n    }\n    else if (_labels_status == LabelStatus::RIGHT)\n    {\n      plot->setLegendAlignment(Qt::AlignRight);\n    }\n    plot->replot();\n  };\n\n  this->forEachWidget(visitor);\n}\n\nvoid MainWindow::on_pushButtonZoomOut_clicked()\n{\n  linkedZoomOut();\n  onUndoableChange();\n}\n\nvoid MainWindow::on_comboStreaming_currentIndexChanged(const QString& current_text)\n{\n  QSettings settings;\n  settings.setValue("MainWindow.previousStreamingPlugin", current_text);\n  auto streamer = _data_streamer.at(current_text);\n  ui->buttonStreamingOptions->setEnabled(!streamer->availableActions().empty());\n\n  std::pair<QAction*, int> notifications_pair = streamer->notificationAction();\n  if (notifications_pair.first == nullptr)\n  {\n    ui->buttonStreamingNotifications->setEnabled(false);\n  }\n  else\n  {\n    on_streamingNotificationsChanged(notifications_pair.second);\n  }\n}\n\nvoid MainWindow::on_buttonStreamingStart_clicked()\n{\n  ui->buttonStreamingStart->setEnabled(false);\n  if (ui->buttonStreamingStart->text() == "Start")\n  {\n    startStreamingPlugin(ui->comboStreaming->currentText());\n  }\n  else\n  {\n    stopStreamingPlugin();\n  }\n  ui->buttonStreamingStart->setEnabled(true);\n}\n\nPopupMenu::PopupMenu(QWidget* relative_widget, QWidget* parent)\n  : QMenu(parent), _w(relative_widget)\n{\n}\n\nvoid PopupMenu::showEvent(QShowEvent*)\n{\n  QPoint p = _w->mapToGlobal({});\n  QRect geo = _w->geometry();\n  this->move(p.x() + geo.width(), p.y());\n}\n\nvoid PopupMenu::leaveEvent(QEvent*)\n{\n  close();\n}\n\nvoid PopupMenu::closeEvent(QCloseEvent*)\n{\n  _w->setAttribute(Qt::WA_UnderMouse, false);\n}\n\nvoid MainWindow::on_buttonRecentData_clicked()\n{\n  PopupMenu* menu = new PopupMenu(ui->buttonRecentData, this);\n\n  for (auto action : _recent_data_files->actions())\n  {\n    menu->addAction(action);\n  }\n  menu->exec();\n}\n\nvoid MainWindow::on_buttonStreamingOptions_clicked()\n{\n  if (_data_streamer.empty())\n  {\n    return;\n  }\n  auto streamer = _data_streamer.at(ui->comboStreaming->currentText());\n\n  PopupMenu* menu = new PopupMenu(ui->buttonStreamingOptions, this);\n  for (auto action : streamer->availableActions())\n  {\n    menu->addAction(action);\n  }\n  menu->show();\n}\n\nvoid MainWindow::on_buttonHideFileFrame_clicked()\n{\n  bool hidden = !ui->frameFile->isHidden();\n  ui->buttonHideFileFrame->setText(hidden ? "+" : " -");\n  ui->frameFile->setHidden(hidden);\n\n  QSettings settings;\n  settings.setValue("MainWindow.hiddenFileFrame", hidden);\n}\n\nvoid MainWindow::on_buttonHideStreamingFrame_clicked()\n{\n  bool hidden = !ui->frameStreaming->isHidden();\n  ui->buttonHideStreamingFrame->setText(hidden ? "+" : " -");\n  ui->frameStreaming->setHidden(hidden);\n\n  QSettings settings;\n  settings.setValue("MainWindow.hiddenStreamingFrame", hidden);\n}\n\nvoid MainWindow::on_buttonHidePublishersFrame_clicked()\n{\n  bool hidden = !ui->framePublishers->isHidden();\n  ui->buttonHidePublishersFrame->setText(hidden ? "+" : " -");\n  ui->framePublishers->setHidden(hidden);\n\n  QSettings settings;\n  settings.setValue("MainWindow.hiddenPublishersFrame", hidden);\n}\n\nvoid MainWindow::on_buttonRecentLayout_clicked()\n{\n  PopupMenu* menu = new PopupMenu(ui->buttonRecentLayout, this);\n\n  for (auto action : _recent_layout_files->actions())\n  {\n    menu->addAction(action);\n  }\n  menu->exec();\n}\n\nQStringList MainWindow::readAllCurvesFromXML(QDomElement root_node)\n{\n  QStringList curves;\n\n  QStringList level_names = { "tabbed_widget", "Tab",  "Container", "DockSplitter",\n                              "DockArea",      "plot", "curve" };\n\n  std::function<void(int, QDomElement)> recursiveXmlStream;\n  recursiveXmlStream = [&](int level, QDomElement parent_elem) {\n    QString level_name = level_names[level];\n    for (auto elem = parent_elem.firstChildElement(level_name); elem.isNull() == false;\n         elem = elem.nextSiblingElement(level_name))\n    {\n      if (level_name == "curve")\n      {\n        curves.push_back(elem.attribute("name"));\n      }\n      else\n      {\n        recursiveXmlStream(level + 1, elem);\n      }\n    }\n  };\n\n  // start recursion\n  recursiveXmlStream(0, root_node);\n\n  return curves;\n}\n\nvoid MainWindow::on_actionColorMap_Editor_triggered()\n{\n  ColorMapEditor dialog;\n  dialog.exec();\n}\n', 'message': 'Merge pull request #505 from rticommunity/main\n\nEnhanced processing of default command-line arguments', 'author_name': 'GitHub', 'lang': 'cpp'}