00001 #include <functional>
00002 #include <stdio.h>
00003 #include <numeric>
00004
00005 #include <QActionGroup>
00006 #include <QCheckBox>
00007 #include <QCommandLineParser>
00008 #include <QDebug>
00009 #include <QDesktopServices>
00010 #include <QDomDocument>
00011 #include <QDoubleSpinBox>
00012 #include <QElapsedTimer>
00013 #include <QFileDialog>
00014 #include <QInputDialog>
00015 #include <QMenu>
00016 #include <QGroupBox>
00017 #include <QMessageBox>
00018 #include <QMimeData>
00019 #include <QMouseEvent>
00020 #include <QPluginLoader>
00021 #include <QPushButton>
00022 #include <QKeySequence>
00023 #include <QScrollBar>
00024 #include <QSettings>
00025 #include <QStringListModel>
00026 #include <QStringRef>
00027 #include <QThread>
00028 #include <QTextStream>
00029 #include <QWindow>
00030 #include <QHeaderView>
00031
00032 #include "mainwindow.h"
00033 #include "curvelist_panel.h"
00034 #include "tabbedplotwidget.h"
00035 #include "selectlistdialog.h"
00036 #include "PlotJuggler/plotdata.h"
00037 #include "qwt_plot_canvas.h"
00038 #include "transforms/function_editor.h"
00039 #include "utils.h"
00040
00041 #include "ui_aboutdialog.h"
00042 #include "ui_support_dialog.h"
00043 #include "cheatsheet/video_cheatsheet.h"
00044
00045 MainWindow::MainWindow(const QCommandLineParser &commandline_parser, QWidget *parent) :
00046 QMainWindow(parent),
00047 ui(new Ui::MainWindow),
00048 _undo_shortcut(QKeySequence(Qt::CTRL + Qt::Key_Z), this),
00049 _redo_shortcut(QKeySequence(Qt::CTRL + Qt::SHIFT + Qt::Key_Z), this),
00050 _fullscreen_shortcut(Qt::Key_F10, this),
00051 _streaming_shortcut(QKeySequence(Qt::CTRL + Qt::Key_Space), this),
00052 _playback_shotcut(Qt::Key_Space, this),
00053 _minimized(false),
00054 _current_streamer(nullptr),
00055 _disable_undo_logging(false),
00056 _tracker_time(0),
00057 _tracker_param( CurveTracker::VALUE )
00058 {
00059 QLocale::setDefault(QLocale::c());
00060
00061 _test_option = commandline_parser.isSet("test");
00062 _autostart_publishers = commandline_parser.isSet("publish");
00063
00064 _curvelist_widget = new CurveListPanel(_custom_plots, this);
00065
00066 ui->setupUi(this);
00067
00068 if( commandline_parser.isSet("buffer_size"))
00069 {
00070 int buffer_size = std::max(10, commandline_parser.value("buffer_size").toInt() );
00071 ui->streamingSpinBox->setMaximum(buffer_size);
00072 }
00073
00074 {
00075 QIcon icon(":/icons/resources/light/office_chart_line_stacked.png");
00076 if (!icon.isNull())
00077 {
00078 this->setWindowIcon(icon);
00079 QApplication::setWindowIcon(icon);
00080 }
00081 }
00082
00083 connect( _curvelist_widget->getTableView()->verticalScrollBar(), &QScrollBar::sliderMoved,
00084 this, &MainWindow::onUpdateLeftTableValues );
00085
00086 connect( _curvelist_widget, &CurveListPanel::hiddenItemsChanged,
00087 this, &MainWindow::onUpdateLeftTableValues );
00088
00089 connect(_curvelist_widget, &CurveListPanel::deleteCurves,
00090 this, &MainWindow::onDeleteMultipleCurves );
00091
00092 connect(_curvelist_widget, &CurveListPanel::createMathPlot,
00093 this, &MainWindow::on_addMathPlot);
00094
00095 connect(_curvelist_widget, &CurveListPanel::editMathPlot,
00096 this, &MainWindow::on_editMathPlot);
00097
00098 connect(_curvelist_widget, &CurveListPanel::refreshMathPlot,
00099 this, &MainWindow::on_refreshMathPlot);
00100
00101 connect(_curvelist_widget->getTableView()->verticalScrollBar(), &QScrollBar::valueChanged,
00102 this, &MainWindow::onUpdateLeftTableValues );
00103
00104 connect( ui->timeSlider, &RealSlider::realValueChanged,
00105 this, &MainWindow::onTimeSlider_valueChanged );
00106
00107 connect( ui->playbackRate, &QDoubleSpinBox::editingFinished, this, [this]()
00108 {
00109 ui->playbackRate->clearFocus();
00110 });
00111
00112
00113 _main_tabbed_widget = new TabbedPlotWidget("Main Window", this, nullptr, _mapped_plot_data, this);
00114
00115 ui->plottingLayout->insertWidget(0, _main_tabbed_widget, 1);
00116 ui->leftLayout->addWidget( _curvelist_widget );
00117
00118 ui->splitter->setCollapsible(0,true);
00119 ui->splitter->setStretchFactor(0,2);
00120 ui->splitter->setStretchFactor(1,6);
00121
00122 connect( ui->splitter, SIGNAL(splitterMoved(int,int)), SLOT(on_splitterMoved(int,int)) );
00123
00124 initializeActions();
00125 initializePlugins( QCoreApplication::applicationDirPath() );
00126 initializePlugins("/usr/local/PlotJuggler/plugins");
00127
00128 _undo_timer.start();
00129
00130
00131 onUndoableChange();
00132
00133 _replot_timer = new QTimer(this);
00134 _replot_timer->setInterval(40);
00135 connect(_replot_timer, &QTimer::timeout, this, [this](){ updateDataAndReplot(false); } );
00136
00137 _publish_timer = new QTimer(this);
00138 _publish_timer->setInterval(20);
00139 connect(_publish_timer, &QTimer::timeout, this, &MainWindow::onPlaybackLoop );
00140
00141 ui->menuFile->setToolTipsVisible(true);
00142 ui->horizontalSpacer->changeSize(0,0, QSizePolicy::Fixed, QSizePolicy::Fixed);
00143 ui->streamingLabel->setHidden(true);
00144 ui->streamingSpinBox->setHidden(true);
00145
00146 this->setMenuBar(ui->menuBar);
00147 ui->menuBar->setNativeMenuBar(false);
00148
00149 if( _test_option )
00150 {
00151 connect( ui->actionLoadDummyData, &QAction::triggered,
00152 this, &MainWindow::buildDummyData );
00153 buildDummyData();
00154 }
00155 else{
00156 ui->actionLoadDummyData->setVisible(false);
00157 }
00158
00159 bool file_loaded = false;
00160 if( commandline_parser.isSet("datafile") )
00161 {
00162 QStringList datafiles = commandline_parser.values("datafile");
00163 file_loaded = loadDataFromFiles( datafiles );
00164 }
00165 if( commandline_parser.isSet("layout"))
00166 {
00167 loadLayoutFromFile( commandline_parser.value("layout"));
00168 }
00169
00170 QSettings settings;
00171 restoreGeometry(settings.value("MainWindow.geometry").toByteArray());
00172
00173 bool activate_grid = settings.value("MainWindow.activateGrid", false).toBool();
00174 ui->pushButtonActivateGrid->setChecked(activate_grid);
00175
00176 int streaming_buffer_value = settings.value("MainWindow.streamingBufferValue", 5).toInt();
00177 ui->streamingSpinBox->setValue(streaming_buffer_value);
00178
00179 bool datetime_display = settings.value("MainWindow.dateTimeDisplay", false).toBool();
00180 ui->pushButtonUseDateTime->setChecked( datetime_display );
00181
00182 bool remove_time_offset = settings.value("MainWindow.removeTimeOffset", true).toBool();
00183 ui->pushButtonRemoveTimeOffset->setChecked(remove_time_offset);
00184
00185 ui->widgetOptions->setVisible( ui->pushButtonOptions->isChecked() );
00186 ui->line->setVisible( ui->pushButtonOptions->isChecked() );
00187
00188
00189 QIcon trackerIconA, trackerIconB, trackerIconC;
00190
00191 trackerIconA.addFile(QStringLiteral(":/icons/resources/light/line_tracker.png"), QSize(36, 36));
00192 trackerIconB.addFile(QStringLiteral(":/icons/resources/light/line_tracker_1.png"), QSize(36, 36));
00193 trackerIconC.addFile(QStringLiteral(":/icons/resources/light/line_tracker_a.png"), QSize(36, 36));
00194
00195 _tracker_button_icons[CurveTracker::LINE_ONLY] = trackerIconA;
00196 _tracker_button_icons[CurveTracker::VALUE] = trackerIconB;
00197 _tracker_button_icons[CurveTracker::VALUE_NAME] = trackerIconC;
00198
00199 int tracker_setting = settings.value("MainWindow.timeTrackerSetting", (int)CurveTracker::VALUE ).toInt();
00200 _tracker_param = static_cast<CurveTracker::Parameter>(tracker_setting);
00201
00202 ui->pushButtonTimeTracker->setIcon( _tracker_button_icons[_tracker_param] );
00203
00204 forEachWidget( [&](PlotWidget* plot) {
00205 plot->configureTracker(_tracker_param);
00206 });
00207 }
00208
00209 MainWindow::~MainWindow()
00210 {
00211 delete ui;
00212 }
00213
00214 void MainWindow::onUndoableChange()
00215 {
00216 if(_disable_undo_logging) return;
00217
00218 int elapsed_ms = _undo_timer.restart();
00219
00220
00221 if( elapsed_ms < 100)
00222 {
00223 if( _undo_states.empty() == false)
00224 _undo_states.pop_back();
00225 }
00226
00227 while( _undo_states.size() >= 100 ) _undo_states.pop_front();
00228 _undo_states.push_back( xmlSaveState() );
00229 _redo_states.clear();
00230
00231 }
00232
00233
00234 void MainWindow::onRedoInvoked()
00235 {
00236 _disable_undo_logging = true;
00237 if( _redo_states.size() > 0)
00238 {
00239 QDomDocument state_document = _redo_states.back();
00240 while( _undo_states.size() >= 100 ) _undo_states.pop_front();
00241 _undo_states.push_back( state_document );
00242 _redo_states.pop_back();
00243
00244 xmlLoadState( state_document );
00245 }
00246
00247 _disable_undo_logging = false;
00248 }
00249
00250 void MainWindow::onUndoInvoked( )
00251 {
00252 _disable_undo_logging = true;
00253 if( _undo_states.size() > 1)
00254 {
00255 QDomDocument state_document = _undo_states.back();
00256 while( _redo_states.size() >= 100 ) _redo_states.pop_front();
00257 _redo_states.push_back( state_document );
00258 _undo_states.pop_back();
00259 state_document = _undo_states.back();
00260
00261 xmlLoadState( state_document );
00262 }
00263
00264 _disable_undo_logging = false;
00265 }
00266
00267 void MainWindow::onUpdateLeftTableValues()
00268 {
00269 auto table_model = _curvelist_widget->getTableModel();
00270
00271 for(auto table_view: { _curvelist_widget->getTableView(), _curvelist_widget->getCustomView() } )
00272 {
00273 if( _curvelist_widget->is2ndColumnHidden() )
00274 {
00275 continue;
00276 }
00277
00278 const int vertical_height = table_view->visibleRegion().boundingRect().height();
00279
00280 for (int row = 0; row < _curvelist_widget->rowCount(); row++)
00281 {
00282 int vertical_pos = table_view->rowViewportPosition(row);
00283 if( vertical_pos < 0 || table_view->isRowHidden(row) ){ continue; }
00284 if( vertical_pos > vertical_height){ break; }
00285
00286 const std::string& name = table_model->item(row,0)->text().toStdString();
00287 auto it = _mapped_plot_data.numeric.find(name);
00288 if( it != _mapped_plot_data.numeric.end())
00289 {
00290 auto& data = it->second;
00291
00292 double num = 0.0;
00293 bool valid = false;
00294
00295 if( _tracker_time < std::numeric_limits<double>::max())
00296 {
00297 auto value = data.getYfromX( _tracker_time );
00298 if(value){
00299 valid = true;
00300 num = value.value();
00301 }
00302 }
00303 else if( data.size() > 0)
00304 {
00305 valid = true;
00306 num = data.back().y;
00307 }
00308 if( valid )
00309 {
00310 QString num_text = QString::number( num, 'f', 3);
00311 if(num_text.contains('.'))
00312 {
00313 int idx = num_text.length() -1;
00314 while( num_text[idx] == '0' )
00315 {
00316 num_text[idx] = ' ';
00317 idx--;
00318 }
00319 if( num_text[idx] == '.') num_text[idx] = ' ';
00320 }
00321 table_model->item(row,1)->setText(num_text + ' ');
00322 }
00323 }
00324 }
00325 }
00326 }
00327
00328
00329 void MainWindow::onTrackerMovedFromWidget(QPointF relative_pos)
00330 {
00331 _tracker_time = relative_pos.x() + _time_offset.get();
00332
00333 auto prev = ui->timeSlider->blockSignals(true);
00334 ui->timeSlider->setRealValue( _tracker_time );
00335 ui->timeSlider->blockSignals(prev);
00336
00337 onTrackerTimeUpdated( _tracker_time, true );
00338 }
00339
00340 void MainWindow::onTimeSlider_valueChanged(double abs_time)
00341 {
00342 _tracker_time = abs_time;
00343 onTrackerTimeUpdated( _tracker_time, true );
00344 }
00345
00346 void MainWindow::onTrackerTimeUpdated(double absolute_time, bool do_replot)
00347 {
00348 updatedDisplayTime();
00349 onUpdateLeftTableValues();
00350
00351 for ( auto& it: _state_publisher)
00352 {
00353 it.second->updateState( absolute_time);
00354 }
00355
00356 forEachWidget( [&](PlotWidget* plot)
00357 {
00358 plot->setTrackerPosition( _tracker_time );
00359 if(do_replot)
00360 {
00361 plot->replot();
00362 }
00363 } );
00364 }
00365
00366 void MainWindow::createTabbedDialog(QString suggest_win_name, PlotMatrix* first_tab)
00367 {
00368 if( suggest_win_name.isEmpty())
00369 {
00370 for (size_t i=0; i<= TabbedPlotWidget::instances().size(); i++)
00371 {
00372 suggest_win_name = QString("Window%1").arg(i);
00373 TabbedPlotWidget* tw = TabbedPlotWidget::instance(suggest_win_name);
00374 if( tw == nullptr )
00375 {
00376 break;
00377 }
00378 }
00379 }
00380
00381 SubWindow* window = new SubWindow(suggest_win_name, first_tab, _mapped_plot_data, this );
00382
00383 connect( window, SIGNAL(destroyed(QObject*)), this, SLOT(onFloatingWindowDestroyed(QObject*)) );
00384 connect( window, SIGNAL(destroyed(QObject*)), this, SLOT(onUndoableChange()) );
00385
00386 window->tabbedWidget()->setStreamingMode( isStreamingActive() );
00387
00388 window->setAttribute( Qt::WA_DeleteOnClose, true );
00389 window->show();
00390 window->activateWindow();
00391 window->raise();
00392
00393 if (this->signalsBlocked() == false) onUndoableChange();
00394 }
00395
00396
00397 void MainWindow::initializeActions()
00398 {
00399 _undo_shortcut.setContext(Qt::ApplicationShortcut);
00400 _redo_shortcut.setContext(Qt::ApplicationShortcut);
00401 _fullscreen_shortcut.setContext(Qt::ApplicationShortcut);
00402
00403 connect( &_undo_shortcut, &QShortcut::activated, this, &MainWindow::onUndoInvoked );
00404 connect( &_redo_shortcut, &QShortcut::activated, this, &MainWindow::onRedoInvoked );
00405 connect( &_streaming_shortcut, &QShortcut::activated, this, &MainWindow::on_streamingToggled );
00406 connect( &_playback_shotcut, &QShortcut::activated, ui->pushButtonPlay, &QPushButton::toggle );
00407 connect( &_fullscreen_shortcut, &QShortcut::activated, this, &MainWindow::on_actionFullscreen_triggered);
00408
00409 QShortcut* open_menu_shortcut = new QShortcut(QKeySequence(Qt::ALT + Qt::Key_F), this);
00410 connect( open_menu_shortcut, &QShortcut::activated, [this](){
00411 ui->menuFile->exec( ui->menuBar->mapToGlobal(QPoint(0,25)));
00412 } );
00413
00414 QShortcut* open_streaming_shortcut = new QShortcut(QKeySequence(Qt::ALT + Qt::Key_S), this);
00415 connect( open_streaming_shortcut, &QShortcut::activated, [this](){
00416 ui->menuStreaming->exec( ui->menuBar->mapToGlobal(QPoint(50,25)));
00417 } );
00418
00419 QShortcut* open_publish_shortcut = new QShortcut(QKeySequence(Qt::ALT + Qt::Key_P), this);
00420 connect( open_publish_shortcut, &QShortcut::activated, [this](){
00421 ui->menuPublishers->exec( ui->menuBar->mapToGlobal(QPoint(140,25)));
00422 } );
00423
00424 QShortcut* open_help_shortcut = new QShortcut(QKeySequence(Qt::ALT + Qt::Key_H), this);
00425 connect( open_help_shortcut, &QShortcut::activated, [this](){
00426 ui->menuHelp->exec( ui->menuBar->mapToGlobal(QPoint(230,25)));
00427 } );
00428
00429
00430
00431 QSettings settings;
00432 updateRecentDataMenu( settings.value("MainWindow.recentlyLoadedDatafile").toStringList() );
00433 updateRecentLayoutMenu( settings.value("MainWindow.recentlyLoadedLayout").toStringList() );
00434 }
00435
00436 void MainWindow::initializePlugins(QString directory_name)
00437 {
00438 static std::set<QString> loaded_plugins;
00439
00440 QDir pluginsDir( directory_name );
00441
00442 for (const QString& filename: pluginsDir.entryList(QDir::Files))
00443 {
00444 QFileInfo fileinfo(filename);
00445 if( fileinfo.suffix() != "so" && fileinfo.suffix() != "dll" && fileinfo.suffix() != "dylib"){
00446 continue;
00447 }
00448
00449 if( loaded_plugins.find( filename ) != loaded_plugins.end())
00450 {
00451 continue;
00452 }
00453
00454 QPluginLoader pluginLoader(pluginsDir.absoluteFilePath(filename), this);
00455
00456 QObject *plugin = pluginLoader.instance();
00457 if (plugin)
00458 {
00459 DataLoader *loader = qobject_cast<DataLoader *>(plugin);
00460 StatePublisher *publisher = qobject_cast<StatePublisher *>(plugin);
00461 DataStreamer *streamer = qobject_cast<DataStreamer *>(plugin);
00462
00463 QString plugin_name;
00464 if( loader ) plugin_name = loader->name();
00465 if( publisher ) plugin_name = publisher->name();
00466 if( streamer ) plugin_name = streamer->name();
00467 plugin_name.replace(" ", "_");
00468
00469 if( loaded_plugins.find(plugin_name) == loaded_plugins.end())
00470 {
00471 loaded_plugins.insert( plugin_name );
00472 }
00473 else{
00474 QMessageBox::warning(this, tr("Warning"),
00475 tr("Trying to load twice a plugin with name [%1].\n"
00476 "Only the first will be loaded.").arg(plugin_name) );
00477 continue;
00478 }
00479
00480 if (loader)
00481 {
00482 qDebug() << filename << ": is a DataLoader plugin";
00483 if( !_test_option && loader->isDebugPlugin())
00484 {
00485 qDebug() << filename << "...but will be ignored unless the argument -t is used.";
00486 }
00487 else{
00488 _data_loader.insert( std::make_pair( plugin_name, loader) );
00489 }
00490 }
00491 else if (publisher)
00492 {
00493 publisher->setDataMap( &_mapped_plot_data );
00494 qDebug() << filename << ": is a StatePublisher plugin";
00495 if( !_test_option && publisher->isDebugPlugin())
00496 {
00497 qDebug() << filename << "...but will be ignored unless the argument -t is used.";
00498 }
00499 else
00500 {
00501 ui->menuPublishers->setEnabled(true);
00502
00503 _state_publisher.insert( std::make_pair(plugin_name, publisher) );
00504 QAction* activatePublisher = new QAction(tr("Start: ") + plugin_name , this);
00505 activatePublisher->setProperty("starter_button", true);
00506 activatePublisher->setCheckable(true);
00507 activatePublisher->setChecked(false);
00508
00509 ui->menuPublishers->addSeparator();
00510 ui->menuPublishers->addSection(plugin_name);
00511 ui->menuPublishers->addAction(activatePublisher);
00512 publisher->setParentMenu( ui->menuPublishers, activatePublisher );
00513
00514 connect(activatePublisher, &QAction::toggled,
00515 [=](bool enable)
00516 {
00517 publisher->setEnabled( enable );
00518 } );
00519 }
00520 }
00521 else if (streamer)
00522 {
00523 qDebug() << filename << ": is a DataStreamer plugin";
00524 if( !_test_option && streamer->isDebugPlugin())
00525 {
00526 qDebug() << filename << "...but will be ignored unless the argument -t is used.";
00527 }
00528 else{
00529 _data_streamer.insert( std::make_pair(plugin_name , streamer ) );
00530
00531 QAction* startStreamer = new QAction(QString("Start: ") + plugin_name, this);
00532 ui->menuStreaming->setEnabled(true);
00533 ui->menuStreaming->addAction(startStreamer);
00534
00535 streamer->addActionsToParentMenu( ui->menuStreaming );
00536 ui->menuStreaming->addSeparator();
00537
00538 connect(startStreamer, &QAction::triggered, this, [this, plugin_name]()
00539 {
00540 on_actionStartStreaming(plugin_name);
00541 });
00542
00543
00544 connect(streamer, &DataStreamer::connectionClosed,
00545 ui->actionStopStreaming, &QAction::trigger );
00546
00547 connect(streamer, &DataStreamer::clearBuffers,
00548 this, &MainWindow::on_actionClearBuffer_triggered );
00549 }
00550 }
00551 }
00552 else{
00553 if( pluginLoader.errorString().contains("is not an ELF object") == false)
00554 {
00555 qDebug() << filename << ": " << pluginLoader.errorString();
00556 }
00557 }
00558 }
00559 }
00560
00561 void MainWindow::buildDummyData()
00562 {
00563 PlotDataMapRef datamap;
00564
00565 static int count = 0;
00566 size_t SIZE = 10000;
00567 QElapsedTimer timer;
00568 timer.start();
00569 QStringList words_list;
00570 words_list << "world/siam" << "world/tre" << "walk/piccoli" << "walk/porcellin"
00571 << "fly/high/mai" << "fly/high/nessun" << "fly/low/ci" << "fly/low/dividera"
00572 << "data_1" << "data_2" << "data_3" << "data_10";
00573
00574 for( int i=0; i<10; i++)
00575 {
00576 words_list.append(QString("data_vect/%1").arg(count++));
00577 }
00578
00579 for( const QString& name: words_list)
00580 {
00581 double A = 6* ((double)qrand()/(double)RAND_MAX) - 3;
00582 double B = 3* ((double)qrand()/(double)RAND_MAX) ;
00583 double C = 3* ((double)qrand()/(double)RAND_MAX) ;
00584 double D = 20* ((double)qrand()/(double)RAND_MAX) ;
00585
00586 auto it = datamap.addNumeric( name.toStdString() );
00587 PlotData& plot = it->second;
00588
00589 double t = 0;
00590 for (unsigned indx=0; indx<SIZE; indx++)
00591 {
00592 t += 0.01;
00593 plot.pushBack( PlotData::Point( t+35, A*sin(B*t + C) + D*t*0.02 ) ) ;
00594 }
00595 }
00596
00597 PlotData& sin_plot = datamap.addNumeric( "_sin" )->second;
00598 PlotData& cos_plot = datamap.addNumeric( "_cos" )->second;
00599
00600 double t = 0;
00601 for (unsigned indx=0; indx<SIZE; indx++)
00602 {
00603 t += 0.01;
00604 sin_plot.pushBack( PlotData::Point( t+20, sin(t*0.4) ) ) ;
00605 cos_plot.pushBack( PlotData::Point( t+20, cos(t*0.4) ) ) ;
00606 }
00607
00608 importPlotDataMap(datamap, true);
00609 }
00610
00611 void MainWindow::on_splitterMoved(int , int )
00612 {
00613 QList<int> sizes = ui->splitter->sizes();
00614 int maxLeftWidth = _curvelist_widget->maximumWidth();
00615 int totalWidth = sizes[0] + sizes[1];
00616
00617 if( sizes[0] > maxLeftWidth)
00618 {
00619 sizes[0] = maxLeftWidth;
00620 sizes[1] = totalWidth - maxLeftWidth;
00621 ui->splitter->setSizes(sizes);
00622 }
00623 }
00624
00625 void MainWindow::resizeEvent(QResizeEvent *)
00626 {
00627 on_splitterMoved( 0, 0 );
00628 }
00629
00630
00631 void MainWindow::onPlotAdded(PlotWidget* plot)
00632 {
00633 connect( plot, &PlotWidget::undoableChange,
00634 this, &MainWindow::onUndoableChange );
00635
00636 connect( plot, &PlotWidget::trackerMoved,
00637 this, &MainWindow::onTrackerMovedFromWidget);
00638
00639 connect( plot, &PlotWidget::swapWidgetsRequested,
00640 this, &MainWindow::onSwapPlots);
00641
00642 connect( this, &MainWindow::requestRemoveCurveByName,
00643 plot, &PlotWidget::removeCurve) ;
00644
00645 connect( plot, &PlotWidget::curveListChanged,
00646 this, [this]()
00647 {
00648 updateTimeOffset();
00649 updateTimeSlider();
00650 });
00651
00652 connect( &_time_offset, SIGNAL( valueChanged(double)),
00653 plot, SLOT(on_changeTimeOffset(double)) );
00654
00655 connect( ui->pushButtonUseDateTime, &QPushButton::toggled,
00656 plot, &PlotWidget::on_changeDateTimeScale);
00657
00658 connect( plot, &PlotWidget::curvesDropped,
00659 _curvelist_widget, &CurveListPanel::clearSelections);
00660
00661 plot->on_changeTimeOffset( _time_offset.get() );
00662 plot->on_changeDateTimeScale( ui->pushButtonUseDateTime->isChecked() );
00663 plot->activateGrid( ui->pushButtonActivateGrid->isChecked() );
00664 plot->enableTracker( !isStreamingActive() );
00665 plot->configureTracker( _tracker_param );
00666 }
00667
00668 void MainWindow::onPlotMatrixAdded(PlotMatrix* matrix)
00669 {
00670 connect( matrix, &PlotMatrix::plotAdded, this, &MainWindow:: onPlotAdded);
00671 connect( matrix, &PlotMatrix::undoableChange, this, &MainWindow:: onUndoableChange );
00672 }
00673
00674 QDomDocument MainWindow::xmlSaveState() const
00675 {
00676 QDomDocument doc;
00677 QDomProcessingInstruction instr =
00678 doc.createProcessingInstruction("xml", "version='1.0' encoding='UTF-8'");
00679
00680 doc.appendChild(instr);
00681
00682 QDomElement root = doc.createElement( "root" );
00683
00684 for (auto& it: TabbedPlotWidget::instances() )
00685 {
00686 QDomElement tabbed_area = it.second->xmlSaveState(doc);
00687 root.appendChild( tabbed_area );
00688 }
00689
00690 doc.appendChild(root);
00691
00692 QDomElement relative_time = doc.createElement( "use_relative_time_offset" );
00693 relative_time.setAttribute("enabled", ui->pushButtonRemoveTimeOffset->isChecked() );
00694 root.appendChild( relative_time );
00695
00696 return doc;
00697 }
00698
00699 void MainWindow::checkAllCurvesFromLayout(const QDomElement& root)
00700 {
00701 std::set<std::string> curves;
00702
00703 for ( QDomElement tw = root.firstChildElement( "tabbed_widget" ) ;
00704 !tw.isNull(); tw = tw.nextSiblingElement( "tabbed_widget" ) )
00705 {
00706 for ( QDomElement pm = tw.firstChildElement( "plotmatrix" ) ;
00707 !pm.isNull(); pm = pm.nextSiblingElement( "plotmatrix" ) )
00708 {
00709 for ( QDomElement pl = pm.firstChildElement( "plot" ) ;
00710 !pl.isNull(); pl = pl.nextSiblingElement( "plot" ) )
00711 {
00712 for ( QDomElement cv = pl.firstChildElement( "curve" ) ;
00713 !cv.isNull(); cv = cv.nextSiblingElement( "curve" ) )
00714 {
00715 curves.insert( cv.attribute("name").toStdString() );
00716 }
00717 }
00718 }
00719 }
00720
00721 std::vector<std::string> missing_curves;
00722
00723 for (auto& curve_name: curves)
00724 {
00725 if( _mapped_plot_data.numeric.count( curve_name ) == 0)
00726 {
00727 missing_curves.push_back(curve_name);
00728 }
00729 }
00730 if( missing_curves.size() > 0 )
00731 {
00732 QMessageBox msgBox(this);
00733 msgBox.setWindowTitle("Warning");
00734 msgBox.setText(tr("One or more timeseries in the layout haven't been loaded yet\n"
00735 "What do you want to do?"));
00736
00737 QPushButton* buttonRemove = msgBox.addButton(tr("Remove curves from plots"), QMessageBox::RejectRole);
00738 QPushButton* buttonPlaceholder = msgBox.addButton(tr("Create empty placeholders"), QMessageBox::YesRole);
00739 msgBox.setDefaultButton(buttonPlaceholder);
00740 msgBox.exec();
00741 if( msgBox.clickedButton() == buttonPlaceholder )
00742 {
00743 for(auto& name: missing_curves )
00744 {
00745 _curvelist_widget->addItem( QString::fromStdString( name ) );
00746 _mapped_plot_data.addNumeric(name);
00747 }
00748 _curvelist_widget->refreshColumns();
00749 }
00750 }
00751 }
00752
00753 bool MainWindow::xmlLoadState(QDomDocument state_document)
00754 {
00755 QDomElement root = state_document.namedItem("root").toElement();
00756 if ( root.isNull() ) {
00757 qWarning() << "No <root> element found at the top-level of the XML file!";
00758 return false;
00759 }
00760
00761 size_t num_floating = 0;
00762 std::map<QString,QDomElement> tabbed_widgets_with_name;
00763
00764 for (QDomElement tw = root.firstChildElement( "tabbed_widget" ) ;
00765 tw.isNull() == false;
00766 tw = tw.nextSiblingElement( "tabbed_widget" ) )
00767 {
00768 if( tw.attribute("parent") != ("main_window") )
00769 {
00770 num_floating++;
00771 }
00772 tabbed_widgets_with_name[ tw.attribute("name") ] = tw;
00773 }
00774
00775
00776 for(const auto& it: tabbed_widgets_with_name)
00777 {
00778 if( TabbedPlotWidget::instance( it.first ) == nullptr)
00779 {
00780 createTabbedDialog( it.first, nullptr );
00781 }
00782 }
00783
00784
00785 for(const auto& it: TabbedPlotWidget::instances())
00786 {
00787 if( tabbed_widgets_with_name.count( it.first ) == 0)
00788 {
00789 it.second->deleteLater();
00790 }
00791 }
00792
00793
00794 checkAllCurvesFromLayout(root);
00795
00796
00797 for ( QDomElement tw = root.firstChildElement( "tabbed_widget" ) ;
00798 tw.isNull() == false;
00799 tw = tw.nextSiblingElement( "tabbed_widget" ) )
00800 {
00801 TabbedPlotWidget* tabwidget = TabbedPlotWidget::instance( tw.attribute("name"));
00802 tabwidget->xmlLoadState( tw );
00803 }
00804
00805 QDomElement relative_time = root.firstChildElement( "use_relative_time_offset" );
00806 if( !relative_time.isNull())
00807 {
00808 bool remove_offset = (relative_time.attribute("enabled") == QString("1"));
00809 ui->pushButtonRemoveTimeOffset->setChecked(remove_offset);
00810 }
00811 return true;
00812 }
00813
00814
00815 void MainWindow::onDeleteMultipleCurves(const std::vector<std::string> &curve_names)
00816 {
00817 for( const auto& curve_name: curve_names )
00818 {
00819 auto plot_curve = _mapped_plot_data.numeric.find( curve_name );
00820 if( plot_curve == _mapped_plot_data.numeric.end())
00821 {
00822 continue;
00823 }
00824
00825 emit requestRemoveCurveByName( curve_name );
00826 _mapped_plot_data.numeric.erase( plot_curve );
00827
00828 auto custom_it = _custom_plots.find( curve_name );
00829 if( custom_it != _custom_plots.end())
00830 {
00831 _custom_plots.erase( custom_it );
00832 }
00833
00834 int row = _curvelist_widget->findRowByName( curve_name );
00835 if( row != -1 )
00836 {
00837 _curvelist_widget->removeRow(row);
00838 }
00839 }
00840
00841 forEachWidget( [](PlotWidget* plot) {
00842 plot->replot();
00843 } );
00844 }
00845
00846 void MainWindow::updateRecentDataMenu(QStringList new_filenames)
00847 {
00848 QMenu* menu = ui->menuRecentData;
00849
00850 QAction* separator = nullptr;
00851 QStringList prev_filenames;
00852 for (QAction *action: menu->actions())
00853 {
00854 if ( action->isSeparator() )
00855 {
00856 separator = action;
00857 break;
00858 }
00859 if(new_filenames.contains( action->text() ) == false)
00860 {
00861 prev_filenames.push_back( action->text() );
00862 }
00863 menu->removeAction( action );
00864 }
00865
00866 new_filenames.append( prev_filenames );
00867 while( new_filenames.size() > 10 )
00868 {
00869 new_filenames.removeLast();
00870 }
00871
00872 for (const auto& filename: new_filenames)
00873 {
00874 QAction* action = new QAction(filename, nullptr);
00875 connect( action, &QAction::triggered, this, [this, filename]
00876 {
00877 loadDataFromFiles( {filename} );
00878 } );
00879 menu->insertAction(separator, action );
00880 }
00881
00882 QSettings settings;
00883 settings.setValue("MainWindow.recentlyLoadedDatafile", new_filenames );
00884 menu->setEnabled( new_filenames.size() > 0 );
00885 }
00886
00887 void MainWindow::updateRecentLayoutMenu(QStringList new_filenames)
00888 {
00889 QMenu* menu = ui->menuRecentLayout;
00890
00891 QAction* separator = nullptr;
00892 QStringList prev_filenames;
00893 for (QAction *action: menu->actions())
00894 {
00895 if ( action->isSeparator() )
00896 {
00897 separator = action;
00898 break;
00899 }
00900 if(new_filenames.contains( action->text() ) == false)
00901 {
00902 prev_filenames.push_back( action->text() );
00903 }
00904 menu->removeAction( action );
00905 }
00906
00907 new_filenames.append( prev_filenames );
00908 while( new_filenames.size() > 10 )
00909 {
00910 new_filenames.removeLast();
00911 }
00912
00913 for (const auto& filename: new_filenames)
00914 {
00915 QAction* action = new QAction(filename, nullptr);
00916 connect( action, &QAction::triggered, this, [this, filename]
00917 {
00918 if ( this->loadLayoutFromFile(filename) )
00919 {
00920 updateRecentLayoutMenu( {filename} );
00921 }
00922 } );
00923 menu->insertAction(separator, action );
00924 }
00925
00926 QSettings settings;
00927 settings.setValue("MainWindow.recentlyLoadedLayout", new_filenames );
00928 menu->setEnabled( new_filenames.size() > 0 );
00929 }
00930
00931
00932 void MainWindow::deleteAllData()
00933 {
00934 forEachWidget( [](PlotWidget* plot) {
00935 plot->detachAllCurves();
00936 } );
00937
00938 _mapped_plot_data.numeric.clear();
00939 _mapped_plot_data.user_defined.clear();
00940 _custom_plots.clear();
00941 _curvelist_widget->clear();
00942 _loaded_datafiles.clear();
00943
00944 bool stopped = false;
00945 for (QAction* action: ui->menuPublishers->actions())
00946 {
00947 auto is_start_button = action->property("starter_button");
00948 if( is_start_button.isValid() && is_start_button.toBool() && action->isChecked() )
00949 {
00950 action->setChecked( false );
00951 stopped = true;
00952 }
00953 }
00954
00955 if( stopped )
00956 {
00957 QMessageBox::warning(this, "State publishers stopped",
00958 "All the state publishers have been stopped because old data has been deleted.");
00959 }
00960 }
00961
00962
00963 template <typename T>
00964 void importPlotDataMapHelper(std::unordered_map<std::string,T>& source,
00965 std::unordered_map<std::string,T>& destination,
00966 bool delete_older)
00967 {
00968 for (auto& it: source)
00969 {
00970 const std::string& name = it.first;
00971 T& source_plot = it.second;
00972 auto plot_with_same_name = destination.find(name);
00973
00974
00975 if( plot_with_same_name == destination.end() )
00976 {
00977 plot_with_same_name = destination.emplace( std::piecewise_construct,
00978 std::forward_as_tuple(name),
00979 std::forward_as_tuple(name)
00980 ).first;
00981 }
00982 T& destination_plot = plot_with_same_name->second;
00983
00984 if( delete_older )
00985 {
00986 double max_range_x = destination_plot.maximumRangeX();
00987 destination_plot.swapData(source_plot);
00988 destination_plot.setMaximumRangeX(max_range_x);
00989 }
00990 else
00991 {
00992 for (size_t i=0; i< source_plot.size(); i++)
00993 {
00994 destination_plot.pushBack( source_plot.at(i) );
00995 }
00996 }
00997 source_plot.clear();
00998 }
00999 }
01000
01001 void MainWindow::importPlotDataMap(PlotDataMapRef& new_data, bool remove_old)
01002 {
01003 if( new_data.user_defined.empty() && new_data.numeric.empty() )
01004 {
01005 return;
01006 }
01007
01008 if( remove_old )
01009 {
01010 std::vector<std::string> old_plots_to_delete;
01011
01012 for (auto& it: _mapped_plot_data.numeric)
01013 {
01014
01015 if( new_data.numeric.count( it.first ) == 0 )
01016 {
01017 old_plots_to_delete.push_back( it.first );
01018 }
01019 }
01020
01021 if( !old_plots_to_delete.empty() )
01022 {
01023 QMessageBox::StandardButton reply;
01024 reply = QMessageBox::question(this, tr("Warning"),
01025 tr("Do you want to remove the previously loaded data?\n"),
01026 QMessageBox::Yes | QMessageBox::No,
01027 QMessageBox::Yes );
01028 if( reply == QMessageBox::Yes )
01029 {
01030 onDeleteMultipleCurves(old_plots_to_delete);
01031 }
01032 }
01033 }
01034
01035 bool curvelist_modified = false;
01036 for (auto& it: new_data.numeric)
01037 {
01038 const std::string& name = it.first;
01039 if( it.second.size()>0 && _mapped_plot_data.numeric.count(name) == 0)
01040 {
01041 _curvelist_widget->addItem( QString::fromStdString( name ) );
01042 curvelist_modified = true;
01043 }
01044 }
01045
01046 importPlotDataMapHelper( new_data.numeric, _mapped_plot_data.numeric, remove_old );
01047 importPlotDataMapHelper( new_data.user_defined, _mapped_plot_data.user_defined, remove_old );
01048
01049 if( curvelist_modified )
01050 {
01051 _curvelist_widget->refreshColumns();
01052 }
01053 }
01054
01055 bool MainWindow::isStreamingActive() const
01056 {
01057 return ui->pushButtonStreaming->isChecked() && _current_streamer;
01058 }
01059
01060 bool MainWindow::loadDataFromFiles( QStringList filenames )
01061 {
01062 if( filenames.size() > 1 )
01063 {
01064 static bool show_me = true;
01065
01066 QMessageBox msgbox;
01067 msgbox.setWindowTitle("Loading multiple files");
01068 msgbox.setText("You are loading multiple files at once. A prefix will be automatically added to the name of the timeseries.\n\n"
01069 "This is an experimental feature. Publishers will not work as you may expect.");
01070 msgbox.addButton(QMessageBox::Ok);
01071
01072
01073
01074 msgbox.exec();
01075 }
01076
01077 char prefix_ch = 'A';
01078 QStringList loaded_filenames;
01079
01080 for( const auto& filename: filenames)
01081 {
01082 FileLoadInfo info;
01083 info.filename = filename;
01084 if( filenames.size() > 1 )
01085 {
01086 info.prefix = prefix_ch;
01087 }
01088
01089 if( loadDataFromFile(info) )
01090 {
01091 loaded_filenames.push_back(filename);
01092 prefix_ch++;
01093 }
01094 }
01095 if( loaded_filenames.size() > 0 )
01096 {
01097 updateRecentDataMenu(loaded_filenames);
01098 return true;
01099 }
01100 return false;
01101 }
01102
01103 bool MainWindow::loadDataFromFile(const FileLoadInfo& info)
01104 {
01105 const QString extension = QFileInfo(info.filename).suffix().toLower();
01106
01107 typedef std::map<QString,DataLoader*>::iterator MapIterator;
01108
01109 std::vector<MapIterator> compatible_loaders;
01110
01111 for (MapIterator it = _data_loader.begin(); it != _data_loader.end(); ++it)
01112 {
01113 DataLoader* data_loader = it->second;
01114 std::vector<const char*> extensions = data_loader->compatibleFileExtensions();
01115
01116 for(auto& ext: extensions){
01117
01118 if( extension == QString(ext).toLower()){
01119 compatible_loaders.push_back( it );
01120 break;
01121 }
01122 }
01123 }
01124
01125 DataLoader* dataloader = nullptr;
01126
01127 if( compatible_loaders.size() == 1)
01128 {
01129 dataloader = compatible_loaders.front()->second;
01130 }
01131 else{
01132 static QString last_plugin_name_used;
01133
01134 QStringList names;
01135 for (auto& cl: compatible_loaders)
01136 {
01137 const auto& name = cl->first;
01138
01139 if( name == last_plugin_name_used ){
01140 names.push_front( name );
01141 }
01142 else{
01143 names.push_back( name );
01144 }
01145 }
01146
01147 bool ok;
01148 QString plugin_name = QInputDialog::getItem(this, tr("QInputDialog::getItem()"),
01149 tr("Select the loader to use:"),
01150 names, 0, false, &ok);
01151 if (ok && !plugin_name.isEmpty())
01152 {
01153 dataloader = _data_loader[ plugin_name ];
01154 last_plugin_name_used = plugin_name;
01155 }
01156 }
01157
01158 if( dataloader )
01159 {
01160 QFile file(info.filename);
01161
01162 if (!file.open(QFile::ReadOnly | QFile::Text)) {
01163 QMessageBox::warning(this, tr("Datafile"),
01164 tr("Cannot read file %1:\n%2.")
01165 .arg(info.filename)
01166 .arg(file.errorString()));
01167 return false;
01168 }
01169 file.close();
01170
01171 try{
01172 PlotDataMapRef mapped_data;
01173 FileLoadInfo new_info = info;
01174
01175 if( dataloader->readDataFromFile( &new_info, mapped_data ) )
01176 {
01177 AddPrefixToPlotData( info.prefix.toStdString(), mapped_data.numeric );
01178
01179 importPlotDataMap(mapped_data, true);
01180
01181 QDomElement plugin_elem = dataloader->xmlSaveState(new_info.plugin_config);
01182 new_info.plugin_config.appendChild( plugin_elem );
01183
01184 _loaded_datafiles.push_back(new_info);
01185 }
01186 }
01187 catch(std::exception &ex)
01188 {
01189 QMessageBox::warning(this, tr("Exception from the plugin"),
01190 tr("The plugin [%1] thrown the following exception: \n\n %3\n")
01191 .arg(dataloader->name()).arg(ex.what()) );
01192 return false;
01193 }
01194 }
01195 else{
01196 QMessageBox::warning(this, tr("Error"),
01197 tr("Cannot read files with extension %1.\n No plugin can handle that!\n")
01198 .arg(info.filename) );
01199 }
01200 _curvelist_widget->updateFilter();
01201 updateDataAndReplot( true );
01202 ui->timeSlider->setRealValue( ui->timeSlider->getMinimum() );
01203
01204 return true;
01205 }
01206
01207
01208 void MainWindow::on_actionStartStreaming(QString streamer_name)
01209 {
01210 if( _current_streamer )
01211 {
01212 _current_streamer->shutdown();
01213 _current_streamer = nullptr;
01214 }
01215
01216 if( _data_streamer.empty())
01217 {
01218 qDebug() << "Error, no streamer loaded";
01219 return;
01220 }
01221
01222 if( _data_streamer.size() == 1)
01223 {
01224 _current_streamer = _data_streamer.begin()->second;
01225 }
01226 else if( _data_streamer.size() > 1)
01227 {
01228 auto it = _data_streamer.find(streamer_name);
01229 if( it != _data_streamer.end())
01230 {
01231 _current_streamer = it->second;
01232 }
01233 else{
01234 qDebug() << "Error. The streamer " << streamer_name <<
01235 " can't be loaded";
01236 return;
01237 }
01238 }
01239
01240 bool started = false;
01241 try{
01242
01243 started = _current_streamer && _current_streamer->start( nullptr );
01244 }
01245 catch(std::runtime_error& err)
01246 {
01247 QMessageBox::warning(this, tr("Exception from the plugin"),
01248 tr("The plugin thrown the following exception: \n\n %1\n")
01249 .arg(err.what()) );
01250 return;
01251 }
01252 if( started )
01253 {
01254 {
01255 std::lock_guard<std::mutex> lock( _current_streamer->mutex() );
01256 importPlotDataMap( _current_streamer->dataMap(), true );
01257 }
01258
01259 for(auto& action: ui->menuStreaming->actions()) {
01260 action->setEnabled(false);
01261 }
01262 ui->actionClearBuffer->setEnabled(true);
01263
01264 ui->actionStopStreaming->setEnabled(true);
01265 ui->actionDeleteAllData->setToolTip("Stop streaming to be able to delete the data");
01266
01267 ui->pushButtonStreaming->setEnabled(true);
01268 ui->pushButtonStreaming->setChecked(true);
01269 ui->pushButtonRemoveTimeOffset->setEnabled( false );
01270
01271 on_streamingSpinBox_valueChanged( ui->streamingSpinBox->value() );
01272 }
01273 else{
01274 qDebug() << "Failed to launch the streamer";
01275 }
01276 }
01277
01278 void MainWindow::loadPluginState(const QDomElement& root)
01279 {
01280 QDomElement plugins = root.firstChildElement("Plugins");
01281
01282 for ( QDomElement plugin_elem = plugins.firstChildElement() ;
01283 plugin_elem.isNull() == false;
01284 plugin_elem = plugin_elem.nextSiblingElement() )
01285 {
01286 const QString plugin_name = plugin_elem.attribute("ID");
01287
01288 if( plugin_elem.nodeName() != "plugin" || plugin_name.isEmpty() )
01289 {
01290 QMessageBox::warning(this, tr("Error loading Plugin State from Layout"),
01291 tr("The method xmlSaveState() must return a node line this <plugin ID=\"PluginName\" ") );
01292 }
01293
01294 if( _data_loader.find(plugin_name) != _data_loader.end() )
01295 {
01296 _data_loader[plugin_name]->xmlLoadState( plugin_elem );
01297 }
01298 if( _data_streamer.find(plugin_name) != _data_streamer.end() )
01299 {
01300 _data_streamer[plugin_name]->xmlLoadState( plugin_elem );
01301 }
01302 if( _state_publisher.find(plugin_name) != _state_publisher.end() )
01303 {
01304 StatePublisher* publisher = _state_publisher[plugin_name];
01305 publisher->xmlLoadState( plugin_elem );
01306
01307 if( _autostart_publishers && plugin_elem.attribute("status") == "active" )
01308 {
01309 publisher->setEnabled(true);
01310 }
01311 }
01312 }
01313 }
01314
01315 QDomElement MainWindow::savePluginState(QDomDocument& doc)
01316 {
01317 QDomElement list_plugins = doc.createElement( "Plugins" );
01318
01319 auto CheckValidFormat = [this](const QString& expected_name, const QDomElement& elem)
01320 {
01321 if( elem.nodeName() != "plugin" || elem.attribute("ID") != expected_name )
01322 {
01323 QMessageBox::warning(this, tr("Error saving Plugin State to Layout"),
01324 tr("[%1] The method xmlSaveState() must return a node line this <plugin ID=\"PluginName\">")
01325 .arg(expected_name) );
01326 }
01327 };
01328
01329 for (auto& it: _data_loader)
01330 {
01331 const DataLoader* dataloader = it.second;
01332 QDomElement plugin_elem = dataloader->xmlSaveState(doc);
01333 if( !plugin_elem.isNull() )
01334 {
01335 list_plugins.appendChild( plugin_elem );
01336 CheckValidFormat( it.first, plugin_elem );
01337 }
01338 }
01339
01340 for (auto& it: _data_streamer)
01341 {
01342 const DataStreamer* datastreamer = it.second;
01343 QDomElement plugin_elem = datastreamer->xmlSaveState(doc);
01344 if( !plugin_elem.isNull() )
01345 {
01346 list_plugins.appendChild( plugin_elem );
01347 CheckValidFormat( it.first, plugin_elem );
01348 }
01349 }
01350
01351 for (auto& it: _state_publisher)
01352 {
01353 const StatePublisher* state_publisher = it.second;
01354 QDomElement plugin_elem = state_publisher->xmlSaveState(doc);
01355 if( !plugin_elem.isNull() )
01356 {
01357 list_plugins.appendChild( plugin_elem );
01358 CheckValidFormat( it.first, plugin_elem );
01359 }
01360
01361 plugin_elem.setAttribute("status", state_publisher->enabled() ? "active" : "idle");
01362
01363 }
01364 return list_plugins;
01365 }
01366
01367 std::tuple<double, double, int> MainWindow::calculateVisibleRangeX()
01368 {
01369
01370 double min_time = std::numeric_limits<double>::max();
01371 double max_time = -std::numeric_limits<double>::max();
01372 int max_steps = 0;
01373
01374 forEachWidget([&](PlotWidget* widget)
01375 {
01376 for (auto& it: widget->curveList())
01377 {
01378 const auto& curve_name = it.first;
01379
01380 const auto& data = _mapped_plot_data.numeric.find(curve_name)->second;
01381 if(data.size() >=1)
01382 {
01383 const double t0 = data.front().x;
01384 const double t1 = data.back().x;
01385 min_time = std::min( min_time, t0);
01386 max_time = std::max( max_time, t1);
01387 max_steps = std::max( max_steps, (int)data.size());
01388 }
01389 }
01390 });
01391
01392
01393 if( max_steps == 0 || max_time < min_time)
01394 {
01395 for (const auto& it: _mapped_plot_data.numeric)
01396 {
01397 const PlotData& data = it.second;
01398 if(data.size() >=1)
01399 {
01400 const double t0 = data.front().x;
01401 const double t1 = data.back().x;
01402 min_time = std::min( min_time, t0);
01403 max_time = std::max( max_time, t1);
01404 max_steps = std::max( max_steps, (int)data.size());
01405 }
01406 }
01407 }
01408
01409
01410 if( max_steps == 0 || max_time < min_time)
01411 {
01412 min_time = 0.0;
01413 max_time = 1.0;
01414 max_steps = 1;
01415 }
01416 return std::tuple<double,double,int>( min_time, max_time, max_steps );
01417 }
01418
01419 static const QString LAYOUT_VERSION = "2.2.1";
01420
01421 bool MainWindow::loadLayoutFromFile(QString filename)
01422 {
01423 QSettings settings;
01424
01425 QFile file(filename);
01426 if (!file.open(QFile::ReadOnly | QFile::Text)) {
01427 QMessageBox::warning(this, tr("Layout"),
01428 tr("Cannot read file %1:\n%2.")
01429 .arg(filename)
01430 .arg(file.errorString()));
01431 return false;
01432 }
01433
01434 QString errorStr;
01435 int errorLine, errorColumn;
01436
01437 QDomDocument domDocument;
01438
01439 if (!domDocument.setContent(&file, true, &errorStr, &errorLine, &errorColumn)) {
01440 QMessageBox::information(window(), tr("XML Layout"),
01441 tr("Parse error at line %1:\n%2")
01442 .arg(errorLine)
01443 .arg(errorStr));
01444 return false;
01445 }
01446
01447
01448
01449 QDomElement root = domDocument.namedItem("root").toElement();
01450
01451 if( !root.hasAttribute("version") && root.attribute("version") != LAYOUT_VERSION )
01452 {
01453 QMessageBox::warning(this, tr("Wrong Layout version"),
01454 tr("This Layout ID is not supported [%1].\nThis version of PlotJuggler use Layout ID [%2]")
01455 .arg(root.attribute("version"))
01456 .arg(LAYOUT_VERSION) );
01457 return false;
01458 }
01459
01460 loadPluginState(root);
01461
01462 QDomElement previously_loaded_datafile = root.firstChildElement( "previouslyLoaded_Datafiles" );
01463
01464 QDomElement datafile_elem = previously_loaded_datafile.firstChildElement( "fileInfo" );
01465 while( !datafile_elem.isNull() )
01466 {
01467 FileLoadInfo info;
01468 info.filename = datafile_elem.attribute("filename");
01469 info.prefix = datafile_elem.attribute("prefix");
01470
01471 QDomElement datasources_elem = datafile_elem.firstChildElement( "selected_datasources" );
01472 QString topics_list = datasources_elem.attribute("value");
01473 info.selected_datasources = topics_list.split(";", QString::SkipEmptyParts);
01474
01475 auto plugin_elem = datafile_elem.firstChildElement( "plugin" );
01476 info.plugin_config.appendChild( info.plugin_config.importNode( plugin_elem, true ) );
01477
01478 loadDataFromFile( info );
01479 datafile_elem = datafile_elem.nextSiblingElement( "fileInfo" );
01480 }
01481
01482 QDomElement previousl_streamer = root.firstChildElement( "previouslyLoaded_Streamer" );
01483 if( !previousl_streamer.isNull() )
01484 {
01485 QString streamer_name = previousl_streamer.attribute("name");
01486
01487 QMessageBox msgBox(this);
01488 msgBox.setWindowTitle("Start Streaming?");
01489 msgBox.setText(tr("Start the previously used streaming plugin?\n\n %1 \n\n").arg(streamer_name));
01490 QPushButton* yes = msgBox.addButton(tr("Yes"), QMessageBox::YesRole);
01491 QPushButton* no = msgBox.addButton(tr("No"), QMessageBox::RejectRole);
01492 msgBox.setDefaultButton(yes);
01493 msgBox.exec();
01494
01495 if( msgBox.clickedButton() == yes )
01496 {
01497 if( _data_streamer.count(streamer_name) != 0 )
01498 {
01499 on_actionStartStreaming( streamer_name );
01500 }
01501 else{
01502 QMessageBox::warning(this, tr("Error Loading Streamer"),
01503 tr("The streamer named %1 can not be loaded.").arg(streamer_name));
01504 }
01505 }
01506 }
01507
01508
01509 QDomElement plugins = root.firstChildElement("Plugins");
01510
01511 if( ! plugins.isNull() && _autostart_publishers )
01512 {
01513 for ( QDomElement plugin_elem = plugins.firstChildElement() ;
01514 plugin_elem.isNull() == false;
01515 plugin_elem = plugin_elem.nextSiblingElement() )
01516 {
01517 const QString plugin_name = plugin_elem.nodeName();
01518 if( _state_publisher.find(plugin_name) != _state_publisher.end() )
01519 {
01520 StatePublisher* publisher = _state_publisher[plugin_name];
01521
01522 if( plugin_elem.attribute("status") == "active" )
01523 {
01524 publisher->setEnabled(true);
01525 }
01526 }
01527 }
01528 }
01529
01530 auto custom_equations = root.firstChildElement( "customMathEquations" );
01531
01532 try{
01533 if( !custom_equations.isNull() )
01534 {
01535 for (QDomElement custom_eq = custom_equations.firstChildElement( "snippet" ) ;
01536 custom_eq.isNull() == false;
01537 custom_eq = custom_eq.nextSiblingElement( "snippet" ) )
01538 {
01539 CustomPlotPtr new_custom_plot = CustomFunction::createFromXML(custom_eq);
01540 const auto& name = new_custom_plot->name();
01541 _custom_plots[name] = new_custom_plot;
01542 new_custom_plot->calculateAndAdd( _mapped_plot_data );
01543 _curvelist_widget->addItem( QString::fromStdString( name ) );
01544 }
01545 _curvelist_widget->refreshColumns();
01546 }
01547 }
01548 catch( std::runtime_error& err)
01549 {
01550 QMessageBox::warning(this, tr("Exception"),
01551 tr("Failed to refresh a customMathEquation \n\n %1\n")
01552 .arg(err.what()) );
01553 }
01554
01555 QByteArray snippets_saved_xml = settings.value("AddCustomPlotDialog.savedXML",
01556 QByteArray() ).toByteArray();
01557
01558 auto snippets_element = root.firstChildElement("snippets");
01559 if( !snippets_element.isNull() )
01560 {
01561 auto snippets_previous = GetSnippetsFromXML(snippets_saved_xml);
01562 auto snippets_layout = GetSnippetsFromXML(snippets_element);
01563
01564 bool snippets_are_different = false;
01565 for(const auto& snippet_it : snippets_layout)
01566 {
01567 auto prev_it = snippets_previous.find( snippet_it.first );
01568
01569 if( prev_it == snippets_previous.end() ||
01570 prev_it->second.equation != snippet_it.second.equation ||
01571 prev_it->second.globalVars != snippet_it.second.globalVars)
01572 {
01573 snippets_are_different = true;
01574 break;
01575 }
01576 }
01577
01578 if( snippets_are_different )
01579 {
01580 QMessageBox msgBox(this);
01581 msgBox.setWindowTitle("Overwrite custom transforms?");
01582 msgBox.setText("Your layour file contains a set of custom transforms different from "
01583 "the last one you used.\nant to load these transformations?");
01584 msgBox.addButton(QMessageBox::No);
01585 msgBox.addButton(QMessageBox::Yes);
01586 msgBox.setDefaultButton(QMessageBox::Yes);
01587
01588 if( msgBox.exec() == QMessageBox::Yes )
01589 {
01590 for(const auto& snippet_it : snippets_layout)
01591 {
01592 snippets_previous[snippet_it.first] = snippet_it.second;
01593 }
01594 QDomDocument doc;
01595 auto snippets_root_element = ExportSnippets( snippets_previous, doc);
01596 doc.appendChild( snippets_root_element );
01597 settings.setValue("AddCustomPlotDialog.savedXML", doc.toByteArray(2));
01598 }
01599 }
01600 }
01601
01603
01604 xmlLoadState( domDocument );
01605
01606 forEachWidget([&](PlotWidget* plot)
01607 {
01608 plot->zoomOut(false);
01609 } );
01610
01611 _undo_states.clear();
01612 _undo_states.push_back( domDocument );
01613 return true;
01614 }
01615
01616
01617 void MainWindow::on_tabbedAreaDestroyed(QObject *object)
01618 {
01619 this->setFocus();
01620 }
01621
01622 void MainWindow::onFloatingWindowDestroyed(QObject *object)
01623 {
01624
01625
01626
01627
01628
01629
01630
01631
01632 }
01633
01634 void MainWindow::onCreateFloatingWindow(PlotMatrix* first_tab)
01635 {
01636 createTabbedDialog( QString(), first_tab );
01637 }
01638
01639 void MainWindow::forEachWidget(std::function<void (PlotWidget*, PlotMatrix*, int,int )> operation)
01640 {
01641 auto func = [&](QTabWidget * tabs)
01642 {
01643 for (int t=0; t < tabs->count(); t++)
01644 {
01645 PlotMatrix* matrix = static_cast<PlotMatrix*>(tabs->widget(t));
01646
01647 for(unsigned row=0; row< matrix->rowsCount(); row++)
01648 {
01649 for(unsigned col=0; col< matrix->colsCount(); col++)
01650 {
01651 PlotWidget* plot = matrix->plotAt(row, col);
01652 operation(plot, matrix, row, col);
01653 }
01654 }
01655 }
01656 };
01657
01658 for(const auto& it: TabbedPlotWidget::instances())
01659 {
01660 func( it.second->tabWidget() );
01661 }
01662 }
01663
01664 void MainWindow::forEachWidget(std::function<void (PlotWidget *)> op)
01665 {
01666 forEachWidget( [&](PlotWidget*plot, PlotMatrix*, int,int) { op(plot); } );
01667 }
01668
01669 void MainWindow::updateTimeSlider()
01670 {
01671 auto range = calculateVisibleRangeX();
01672
01673 ui->timeSlider->setLimits(std::get<0>(range),
01674 std::get<1>(range),
01675 std::get<2>(range));
01676
01677 _tracker_time = std::max( _tracker_time, ui->timeSlider->getMinimum() );
01678 _tracker_time = std::min( _tracker_time, ui->timeSlider->getMaximum() );
01679 }
01680
01681 void MainWindow::updateTimeOffset()
01682 {
01683 auto range = calculateVisibleRangeX();
01684 double min_time = std::get<0>(range);
01685
01686 const bool remove_offset = ui->pushButtonRemoveTimeOffset->isChecked();
01687 if( remove_offset && min_time != std::numeric_limits<double>::max())
01688 {
01689 _time_offset.set( min_time );
01690 }
01691 else{
01692 _time_offset.set( 0.0 );
01693 }
01694 }
01695
01696 void MainWindow::onSwapPlots(PlotWidget *source, PlotWidget *destination)
01697 {
01698 if( !source || !destination ) return;
01699
01700 PlotMatrix* src_matrix = nullptr;
01701 PlotMatrix* dst_matrix = nullptr;
01702 QPoint src_pos;
01703 QPoint dst_pos;
01704
01705 forEachWidget( [&](PlotWidget* plot, PlotMatrix* matrix, int row,int col)
01706 {
01707 if( plot == source ) {
01708 src_matrix = matrix;
01709 src_pos.setX( row );
01710 src_pos.setY( col );
01711 }
01712 else if( plot == destination )
01713 {
01714 dst_matrix = matrix;
01715 dst_pos.setX( row );
01716 dst_pos.setY( col );
01717 }
01718 });
01719
01720 if(src_matrix && dst_matrix)
01721 {
01722 src_matrix->gridLayout()->removeWidget( source );
01723 dst_matrix->gridLayout()->removeWidget( destination );
01724
01725 src_matrix->gridLayout()->addWidget( destination, src_pos.x(), src_pos.y() );
01726 dst_matrix->gridLayout()->addWidget( source, dst_pos.x(), dst_pos.y() );
01727
01728 src_matrix->updateLayout();
01729 if( src_matrix != dst_matrix){
01730 dst_matrix->updateLayout();
01731 }
01732 source->changeBackgroundColor( Qt::white );
01733 destination->changeBackgroundColor( Qt::white );
01734 }
01735 onUndoableChange();
01736 }
01737
01738 void MainWindow::on_pushButtonStreaming_toggled(bool streaming)
01739 {
01740 if( !_current_streamer )
01741 {
01742 streaming = false;
01743 }
01744
01745 ui->pushButtonRemoveTimeOffset->setEnabled( !streaming );
01746
01747 if( streaming )
01748 {
01749 ui->horizontalSpacer->changeSize(1,1, QSizePolicy::Expanding, QSizePolicy::Fixed);
01750 ui->pushButtonStreaming->setText("Streaming ON");
01751 }
01752 else{
01753 _replot_timer->stop( );
01754 ui->horizontalSpacer->changeSize(0,0, QSizePolicy::Fixed, QSizePolicy::Fixed);
01755 ui->pushButtonStreaming->setText("Streaming OFF");
01756 }
01757 ui->streamingLabel->setHidden( !streaming );
01758 ui->streamingSpinBox->setHidden( !streaming );
01759 ui->timeSlider->setHidden( streaming );
01760 ui->pushButtonPlay->setHidden( streaming );
01761
01762 if( streaming && ui->pushButtonPlay->isChecked() )
01763 {
01764 ui->pushButtonPlay->setChecked(false);
01765 }
01766
01767 forEachWidget([&](PlotWidget* plot)
01768 {
01769 plot->enableTracker( !streaming );
01770 } );
01771
01772 emit activateStreamingMode( streaming );
01773
01774 if( _current_streamer && streaming)
01775 {
01776 _replot_timer->start();
01777 updateTimeOffset();
01778 }
01779 else{
01780 updateDataAndReplot( true );
01781 onUndoableChange();
01782 }
01783 }
01784
01785 void MainWindow::on_streamingToggled()
01786 {
01787 if( ui->pushButtonStreaming->isEnabled() )
01788 {
01789 bool streaming = ui->pushButtonStreaming->isChecked();
01790 ui->pushButtonStreaming->setChecked( !streaming );
01791 }
01792 else {
01793 if( ui->pushButtonPlay->isEnabled() )
01794 {
01795 bool playing = ui->pushButtonPlay->isChecked();
01796 ui->pushButtonPlay->setChecked( !playing );
01797 }
01798 }
01799 }
01800
01801 void MainWindow::updateDataAndReplot(bool replot_hidden_tabs)
01802 {
01803 if( _current_streamer )
01804 {
01805 std::lock_guard<std::mutex> lock( _current_streamer->mutex() );
01806
01807 auto curvelist_added = _current_streamer->appendData( _mapped_plot_data );
01808
01809 for(const auto& str: curvelist_added)
01810 {
01811 _curvelist_widget->addItem(str);
01812 }
01813
01814 if( curvelist_added.size() > 0 )
01815 {
01816 _curvelist_widget->refreshColumns();
01817 }
01818
01819 for( auto& custom_it: _custom_plots)
01820 {
01821 auto* dst_plot = &_mapped_plot_data.numeric.at(custom_it.first);
01822 custom_it.second->calculate(_mapped_plot_data, dst_plot);
01823 }
01824 }
01825
01826 const bool is_streaming_active = isStreamingActive();
01827
01828 forEachWidget( [is_streaming_active](PlotWidget* plot)
01829 {
01830 plot->updateCurves();
01831 plot->setZoomEnabled( !is_streaming_active );
01832 } );
01833
01834
01835
01836 if( is_streaming_active )
01837 {
01838 auto range = calculateVisibleRangeX();
01839 double max_time = std::get<1>(range);
01840 _tracker_time = max_time;
01841
01842 onTrackerTimeUpdated(_tracker_time, false);
01843 }
01844 else{
01845 updateTimeOffset();
01846 updateTimeSlider();
01847 }
01848
01849 for(const auto& it: TabbedPlotWidget::instances())
01850 {
01851 if( replot_hidden_tabs )
01852 {
01853 QTabWidget* tabs = it.second->tabWidget();
01854 for (int index=0; index < tabs->count(); index++)
01855 {
01856 PlotMatrix* matrix = static_cast<PlotMatrix*>( tabs->widget(index) );
01857 matrix->maximumZoomOut();
01858 }
01859 }
01860 else{
01861 PlotMatrix* matrix = it.second->currentTab() ;
01862 matrix->maximumZoomOut();
01863 }
01864 }
01865 }
01866
01867 void MainWindow::on_streamingSpinBox_valueChanged(int value)
01868 {
01869 double real_value = value;
01870 if ( value == ui->streamingSpinBox->maximum())
01871 {
01872 real_value = std::numeric_limits<double>::max();
01873 ui->streamingSpinBox->setStyleSheet("QSpinBox { color: red; }");
01874 ui->streamingSpinBox->setSuffix("=inf");
01875 }
01876 else{
01877 ui->streamingSpinBox->setStyleSheet("QSpinBox { color: black; }");
01878 ui->streamingSpinBox->setSuffix(" sec");
01879 }
01880
01881 if( isStreamingActive() == false)
01882 {
01883 return;
01884 }
01885
01886 for (auto& it : _mapped_plot_data.numeric )
01887 {
01888 it.second.setMaximumRangeX( real_value );
01889 }
01890
01891 for (auto& it: _mapped_plot_data.user_defined)
01892 {
01893 it.second.setMaximumRangeX( real_value );
01894 }
01895
01896 if( _current_streamer )
01897 {
01898 _current_streamer->setMaximumRange( real_value );
01899 }
01900 }
01901
01902 void MainWindow::on_actionStopStreaming_triggered()
01903 {
01904 ui->pushButtonStreaming->setChecked(false);
01905 ui->pushButtonStreaming->setEnabled(false);
01906 _replot_timer->stop();
01907 _current_streamer->shutdown();
01908 _current_streamer = nullptr;
01909
01910 for(auto& action: ui->menuStreaming->actions()) {
01911 action->setEnabled(true);
01912 }
01913 ui->actionStopStreaming->setEnabled(false);
01914
01915 if( !_mapped_plot_data.numeric.empty()){
01916 ui->actionDeleteAllData->setToolTip("");
01917 }
01918
01919
01920 for(auto& it: _mapped_plot_data.numeric)
01921 {
01922 it.second.setMaximumRangeX( std::numeric_limits<double>::max() );
01923 }
01924 for(auto& it: _mapped_plot_data.user_defined)
01925 {
01926 it.second.setMaximumRangeX( std::numeric_limits<double>::max() );
01927 }
01928 }
01929
01930
01931 void MainWindow::on_actionExit_triggered()
01932 {
01933 this->close();
01934 }
01935
01936 void MainWindow::on_pushButtonRemoveTimeOffset_toggled(bool )
01937 {
01938 updateTimeOffset();
01939 updatedDisplayTime();
01940
01941 forEachWidget( [](PlotWidget* plot)
01942 {
01943 plot->replot();
01944 } );
01945
01946 if (this->signalsBlocked() == false) onUndoableChange();
01947 }
01948
01949
01950 void MainWindow::on_pushButtonOptions_toggled(bool checked)
01951 {
01952 ui->widgetOptions->setVisible( checked );
01953 ui->line->setVisible( checked );
01954 }
01955
01956 void MainWindow::updatedDisplayTime()
01957 {
01958 QLineEdit* timeLine = ui->displayTime;
01959 const double relative_time = _tracker_time - _time_offset.get();
01960 if( ui->pushButtonUseDateTime->isChecked() )
01961 {
01962 if( ui->pushButtonRemoveTimeOffset->isChecked() )
01963 {
01964 QTime time = QTime::fromMSecsSinceStartOfDay( std::round(relative_time*1000.0));
01965 timeLine->setText( time.toString("HH:mm::ss.zzz") );
01966 }
01967 else{
01968 QDateTime datetime = QDateTime::fromMSecsSinceEpoch( std::round(_tracker_time*1000.0) );
01969 timeLine->setText( datetime.toString("[yyyy MMM dd] HH:mm::ss.zzz") );
01970 }
01971 }
01972 else{
01973 timeLine->setText( QString::number(relative_time, 'f', 3));
01974 }
01975
01976 QFontMetrics fm( timeLine->font() );
01977 int width = fm.width( timeLine->text()) + 10;
01978 timeLine->setFixedWidth( std::max( 100, width ) );
01979 }
01980
01981 void MainWindow::on_pushButtonActivateGrid_toggled(bool checked)
01982 {
01983 forEachWidget( [checked](PlotWidget* plot) {
01984 plot->activateGrid( checked );
01985 plot->replot();
01986 });
01987 }
01988
01989 void MainWindow::on_pushButtonRatio_toggled(bool checked)
01990 {
01991 forEachWidget( [checked](PlotWidget* plot) {
01992 plot->setConstantRatioXY( checked );
01993 plot->replot();
01994 });
01995 }
01996
01997 void MainWindow::on_pushButtonPlay_toggled(bool checked)
01998 {
01999 if( checked )
02000 {
02001 _publish_timer->start();
02002 _prev_publish_time = QDateTime::currentDateTime();
02003 }
02004 else{
02005 _publish_timer->stop();
02006 }
02007 }
02008
02009 void MainWindow::on_actionClearBuffer_triggered()
02010 {
02011 for (auto& it: _mapped_plot_data.numeric )
02012 {
02013 it.second.clear();
02014 }
02015
02016 for (auto& it: _mapped_plot_data.user_defined )
02017 {
02018 it.second.clear();
02019 }
02020
02021 forEachWidget( [](PlotWidget* plot) {
02022 plot->reloadPlotData();
02023 plot->replot();
02024 });
02025 }
02026
02027 void MainWindow::on_pushButtonUseDateTime_toggled(bool checked)
02028 {
02029 updatedDisplayTime();
02030 }
02031
02032 void MainWindow::on_pushButtonTimeTracker_pressed()
02033 {
02034 if( _tracker_param == CurveTracker::LINE_ONLY)
02035 {
02036 _tracker_param = CurveTracker::VALUE;
02037 }
02038 else if( _tracker_param == CurveTracker::VALUE)
02039 {
02040 _tracker_param = CurveTracker::VALUE_NAME;
02041 }
02042 else if( _tracker_param == CurveTracker::VALUE_NAME)
02043 {
02044 _tracker_param = CurveTracker::LINE_ONLY;
02045 }
02046 ui->pushButtonTimeTracker->setIcon( _tracker_button_icons[ _tracker_param ] );
02047
02048 forEachWidget( [&](PlotWidget* plot) {
02049 plot->configureTracker(_tracker_param);
02050 plot->replot();
02051 });
02052 }
02053
02054 void MainWindow::closeEvent(QCloseEvent *event)
02055 {
02056 _replot_timer->stop();
02057 _publish_timer->stop();
02058
02059 if( _current_streamer )
02060 {
02061 _current_streamer->shutdown();
02062 _current_streamer = nullptr;
02063 }
02064 QSettings settings;
02065 settings.setValue("MainWindow.geometry", saveGeometry());
02066 settings.setValue("MainWindow.activateGrid", ui->pushButtonActivateGrid->isChecked() );
02067 settings.setValue("MainWindow.streamingBufferValue", ui->streamingSpinBox->value() );
02068 settings.setValue("MainWindow.removeTimeOffset",ui->pushButtonRemoveTimeOffset->isChecked() );
02069 settings.setValue("MainWindow.dateTimeDisplay", ui->pushButtonUseDateTime->isChecked() );
02070 settings.setValue("MainWindow.timeTrackerSetting", (int)_tracker_param );
02071
02072
02073 for(auto& it : _data_loader ) { delete it.second; }
02074 for(auto& it : _state_publisher ) { delete it.second; }
02075 for(auto& it : _data_streamer ) { delete it.second; }
02076 }
02077
02078 void MainWindow::on_addMathPlot(const std::string& linked_name)
02079 {
02080 addOrEditMathPlot(linked_name, false);
02081 }
02082
02083 void MainWindow::on_editMathPlot(const std::string &plot_name)
02084 {
02085 addOrEditMathPlot(plot_name, true);
02086 }
02087
02088 void MainWindow::on_refreshMathPlot(const std::string &plot_name)
02089 {
02090 try{
02091 auto custom_it = _custom_plots.find(plot_name);
02092 if(custom_it == _custom_plots.end())
02093 {
02094 qWarning("failed to find custom equation");
02095 return;
02096 }
02097 CustomPlotPtr ce = custom_it->second;
02098
02099 ce->calculateAndAdd(_mapped_plot_data);
02100
02101 onUpdateLeftTableValues();
02102 updateDataAndReplot( true );
02103 }
02104 catch(const std::runtime_error &e)
02105 {
02106 QMessageBox::critical(this, "error", "Failed to refresh data : " + QString::fromStdString(e.what()));
02107 }
02108 }
02109
02110 void MainWindow::addOrEditMathPlot(const std::string &name, bool modifying)
02111 {
02112 AddCustomPlotDialog dialog(_mapped_plot_data, _custom_plots, this);
02113
02114 if(!modifying)
02115 {
02116 dialog.setLinkedPlotName(QString::fromStdString(name));
02117 dialog.setEditorMode( AddCustomPlotDialog::FUNCTION_OR_TIMESERIES );
02118 }
02119 else
02120 {
02121 dialog.setEditorMode( AddCustomPlotDialog::TIMESERIES_ONLY );
02122
02123 auto custom_it = _custom_plots.find(name);
02124 if(custom_it == _custom_plots.end())
02125 {
02126 qWarning("failed to find custom equation");
02127 return;
02128 }
02129
02130
02131 auto data_it = _mapped_plot_data.numeric.find( name );
02132 if( data_it != _mapped_plot_data.numeric.end())
02133 {
02134 data_it->second.clear();
02135 }
02136 dialog.editExistingPlot(custom_it->second);
02137 }
02138
02139 if(dialog.exec() == QDialog::Accepted)
02140 {
02141 const QString& qplot_name = dialog.getName();
02142 std::string plot_name = qplot_name.toStdString();
02143 CustomPlotPtr eq = dialog.getCustomPlotData();
02144
02145 try {
02146 eq->calculateAndAdd(_mapped_plot_data);
02147 }
02148 catch(std::exception& ex)
02149 {
02150 QMessageBox::warning(this, tr("Warning"),
02151 tr("Failed to create the custom timeseries. Error:\n\n%1")
02152 .arg( ex.what() ) );
02153
02154 return;
02155 }
02156
02157
02158 auto custom_it = _custom_plots.find(plot_name);
02159 if( custom_it == _custom_plots.end() )
02160 {
02161 _custom_plots.insert( {plot_name, eq} );
02162 }
02163 else{
02164 custom_it->second = eq;
02165 modifying = true;
02166 }
02167
02168 if(!modifying)
02169 {
02170 _curvelist_widget->addItem(qplot_name);
02171 }
02172 onUpdateLeftTableValues();
02173
02174 if(modifying)
02175 {
02176 updateDataAndReplot( true );
02177 }
02178 }
02179 }
02180
02181 void MainWindow::onPlaybackLoop()
02182 {
02183 qint64 delta_ms = (QDateTime::currentMSecsSinceEpoch() - _prev_publish_time.toMSecsSinceEpoch());
02184 _prev_publish_time = QDateTime::currentDateTime();
02185 delta_ms = std::max( (qint64)_publish_timer->interval(), delta_ms);
02186
02187 _tracker_time += delta_ms*0.001*ui->playbackRate->value();
02188 if( _tracker_time >= ui->timeSlider->getMaximum())
02189 {
02190 if( !ui->playbackLoop->isChecked() )
02191 {
02192 ui->pushButtonPlay->setChecked(false);
02193 }
02194 _tracker_time = ui->timeSlider->getMinimum();
02195 }
02197 auto prev = ui->timeSlider->blockSignals(true);
02198 ui->timeSlider->setRealValue( _tracker_time );
02199 ui->timeSlider->blockSignals(prev);
02200
02202 updatedDisplayTime();
02203 onUpdateLeftTableValues();
02204
02205 for ( auto& it: _state_publisher)
02206 {
02207 it.second->play( _tracker_time );
02208 }
02209
02210 forEachWidget( [&](PlotWidget* plot)
02211 {
02212 plot->setTrackerPosition( _tracker_time );
02213 plot->replot();
02214 } );
02215 }
02216
02217 void MainWindow::on_actionReportBug_triggered()
02218 {
02219 QDesktopServices::openUrl( QUrl( "https://github.com/facontidavide/PlotJuggler/issues" ));
02220 }
02221
02222 void MainWindow::on_actionAbout_triggered()
02223 {
02224 QDialog* dialog = new QDialog(this);
02225 auto ui = new Ui::AboutDialog();
02226 ui->setupUi(dialog);
02227
02228 ui->label_version->setText( QApplication::applicationVersion() );
02229 dialog->setAttribute(Qt::WA_DeleteOnClose);
02230
02231 dialog->exec();
02232 }
02233
02234 void MainWindow::on_actionCheatsheet_triggered()
02235 {
02236 QSettings settings;
02237
02238 HelpVideo* dialog = new HelpVideo(this);
02239 dialog->restoreGeometry(settings.value("Cheatsheet.geometry").toByteArray());
02240 dialog->setAttribute(Qt::WA_DeleteOnClose);
02241 dialog->show();
02242
02243 connect(dialog, &QDialog::finished, this, [this, dialog]()
02244 {
02245 QSettings settings;
02246 settings.setValue("Cheatsheet.geometry", dialog->saveGeometry());
02247 } );
02248 }
02249
02250 void MainWindow::on_actionSupportPlotJuggler_triggered()
02251 {
02252 QDialog* dialog = new QDialog(this);
02253 auto ui = new Ui::SupportDialog();
02254 ui->setupUi(dialog);
02255
02256 dialog->setAttribute(Qt::WA_DeleteOnClose);
02257
02258 dialog->exec();
02259 }
02260
02261 void MainWindow::on_actionSaveAllPlotTabs_triggered()
02262 {
02263 QSettings settings;
02264 QString directory_path = settings.value("MainWindow.saveAllPlotTabs",
02265 QDir::currentPath() ).toString();
02266
02267 QFileDialog saveDialog;
02268 saveDialog.setDirectory(directory_path);
02269 saveDialog.setFileMode(QFileDialog::FileMode::Directory);
02270 saveDialog.setAcceptMode(QFileDialog::AcceptSave);
02271 saveDialog.exec();
02272
02273 uint image_number = 1;
02274 if(saveDialog.result() == QDialog::Accepted && !saveDialog.selectedFiles().empty())
02275 {
02276
02277 QString directory = saveDialog.selectedFiles().first();
02278 settings.setValue("MainWindow.saveAllPlotTabs", directory);
02279
02280 QStringList file_names;
02281 QStringList existing_files;
02282 QDateTime current_date_time(QDateTime::currentDateTime());
02283 QString current_date_time_name(current_date_time.toString("yyyy-MM-dd_HH-mm-ss"));
02284 for(const auto& it: TabbedPlotWidget::instances())
02285 {
02286 auto tab_widget = it.second->tabWidget();
02287 for(int i=0; i< tab_widget->count(); i++)
02288 {
02289 PlotMatrix* matrix = static_cast<PlotMatrix*>( tab_widget->widget(i) );
02290 QString name = QString("%1/%2_%3_%4.png")
02291 .arg(directory)
02292 .arg(current_date_time_name)
02293 .arg(image_number, 2, 10, QLatin1Char('0')).arg(matrix->name());
02294 file_names.push_back( name );
02295 image_number++;
02296
02297 QFileInfo check_file( file_names.back() );
02298 if( check_file.exists() && check_file.isFile() )
02299 {
02300 existing_files.push_back( name );
02301 }
02302 }
02303 }
02304 if( existing_files.isEmpty() == false)
02305 {
02306 QMessageBox msgBox;
02307 msgBox.setText("One or more files will be overwritten. ant to continue?");
02308 QString all_files;
02309 for(const auto& str: existing_files)
02310 {
02311 all_files.push_back("\n");
02312 all_files.append(str);
02313 }
02314 msgBox.setInformativeText(all_files);
02315 msgBox.setStandardButtons(QMessageBox::Cancel | QMessageBox::Ok );
02316 msgBox.setDefaultButton(QMessageBox::Ok);
02317
02318 if( msgBox.exec() != QMessageBox::Ok)
02319 {
02320 return;
02321 }
02322 }
02323
02324 image_number = 0;
02325 for(const auto& it: TabbedPlotWidget::instances())
02326 {
02327 auto tab_widget = it.second->tabWidget();
02328 for(int i=0; i< tab_widget->count(); i++)
02329 {
02330 PlotMatrix* matrix = static_cast<PlotMatrix*>( tab_widget->widget(i) );
02331 TabbedPlotWidget::saveTabImage( file_names[image_number], matrix);
02332 image_number++;
02333 }
02334 }
02335 }
02336 }
02337
02338 void MainWindow::on_actionLoadData_triggered()
02339 {
02340 if( _data_loader.empty())
02341 {
02342 QMessageBox::warning(this, tr("Warning"),
02343 tr("No plugin was loaded to process a data file\n") );
02344 return;
02345 }
02346
02347 QSettings settings;
02348
02349 QString file_extension_filter;
02350
02351 std::set<QString> extensions;
02352
02353 for (auto& it: _data_loader)
02354 {
02355 DataLoader* loader = it.second;
02356 for (QString extension: loader->compatibleFileExtensions() )
02357 {
02358 extensions.insert( extension.toLower() );
02359 }
02360 }
02361
02362 for (const auto& it: extensions)
02363 {
02364 file_extension_filter.append( QString(" *.") + it );
02365 }
02366
02367 QString directory_path = settings.value("MainWindow.lastDatafileDirectory", QDir::currentPath() ).toString();
02368
02369 QFileDialog loadDialog( this );
02370 loadDialog.setFileMode(QFileDialog::ExistingFiles);
02371 loadDialog.setViewMode(QFileDialog::Detail);
02372 loadDialog.setNameFilter(file_extension_filter);
02373 loadDialog.setDirectory(directory_path);
02374
02375 QStringList fileNames;
02376 if (loadDialog.exec())
02377 {
02378 fileNames = loadDialog.selectedFiles();
02379 }
02380
02381 if (fileNames.isEmpty()) {
02382 return;
02383 }
02384
02385 directory_path = QFileInfo(fileNames[0]).absolutePath();
02386 settings.setValue("MainWindow.lastDatafileDirectory", directory_path);
02387
02388 if( loadDataFromFiles(fileNames) )
02389 {
02390 updateRecentDataMenu(fileNames);
02391 }
02392 }
02393
02394 void MainWindow::on_actionLoadLayout_triggered()
02395 {
02396 QSettings settings;
02397
02398 QString directory_path = settings.value("MainWindow.lastLayoutDirectory", QDir::currentPath()).toString();
02399 QString filename = QFileDialog::getOpenFileName(this, "Open Layout",
02400 directory_path, "*.xml");
02401 if (filename.isEmpty()){
02402 return;
02403 }
02404
02405 if( loadLayoutFromFile(filename) )
02406 {
02407 updateRecentLayoutMenu( {filename} );
02408 }
02409
02410 directory_path = QFileInfo(filename).absolutePath();
02411 settings.setValue("MainWindow.lastLayoutDirectory", directory_path);
02412 }
02413
02414 void MainWindow::on_actionSaveLayout_triggered()
02415
02416 {
02417 QDomDocument doc = xmlSaveState();
02418
02419 QSettings settings;
02420
02421 QString directory_path = settings.value("MainWindow.lastLayoutDirectory",
02422 QDir::currentPath() ).toString();
02423
02424 QFileDialog saveDialog;
02425 saveDialog.setOption(QFileDialog::DontUseNativeDialog, true);
02426
02427 QGridLayout *save_layout = static_cast<QGridLayout*>(saveDialog.layout());
02428
02429 QFrame* frame = new QFrame;
02430 frame->setFrameStyle(QFrame::Box | QFrame::Plain);
02431 frame->setLineWidth(1);
02432
02433 QVBoxLayout *vbox = new QVBoxLayout;
02434 QLabel* title = new QLabel("Save Layout options");
02435 QFrame* separator = new QFrame;
02436 separator->setFrameStyle(QFrame::HLine | QFrame::Plain);
02437
02438 auto checkbox_datasource = new QCheckBox("Save data source");
02439 checkbox_datasource->setToolTip("ant the layout to remember the source of your data,\n"
02440 "i.e. the Datafile used or the Streaming Plugin loaded ?");
02441 checkbox_datasource->setFocusPolicy( Qt::NoFocus );
02442 checkbox_datasource->setChecked( settings.value("MainWindow.saveLayoutDataSource", true).toBool() );
02443
02444 auto checkbox_snippets = new QCheckBox("Save custom transformations");
02445 checkbox_snippets->setToolTip("Do you want the layout to save the custom transformations?");
02446 checkbox_snippets->setFocusPolicy( Qt::NoFocus );
02447 checkbox_snippets->setChecked( settings.value("MainWindow.saveLayoutSnippets", true).toBool() );
02448
02449 vbox->addWidget(title);
02450 vbox->addWidget(separator);
02451 vbox->addWidget(checkbox_datasource);
02452 vbox->addWidget(checkbox_snippets);
02453 frame->setLayout(vbox);
02454
02455 int rows = save_layout->rowCount();
02456 int col = save_layout->columnCount();
02457 save_layout->addWidget(frame, 0, col, rows, 1, Qt::AlignTop);
02458
02459 saveDialog.setAcceptMode(QFileDialog::AcceptSave);
02460 saveDialog.setDefaultSuffix("xml");
02461 saveDialog.setNameFilter("XML (*.xml)");
02462 saveDialog.setDirectory(directory_path);
02463 saveDialog.exec();
02464
02465 if(saveDialog.result() != QDialog::Accepted || saveDialog.selectedFiles().empty())
02466 {
02467 return;
02468 }
02469
02470 QString fileName = saveDialog.selectedFiles().first();
02471
02472 if (fileName.isEmpty()){
02473 return;
02474 }
02475
02476 directory_path = QFileInfo(fileName).absolutePath();
02477 settings.setValue("MainWindow.lastLayoutDirectory", directory_path);
02478 settings.setValue("MainWindow.saveLayoutDataSource", checkbox_datasource->isChecked() );
02479 settings.setValue("MainWindow.saveLayoutSnippets", checkbox_snippets->isChecked() );
02480
02481 QDomElement root = doc.namedItem("root").toElement();
02482 root.setAttribute("version", LAYOUT_VERSION);
02483
02484 root.appendChild( doc.createComment(" - - - - - - - - - - - - - - ") );
02485
02486 root.appendChild( doc.createComment(" - - - - - - - - - - - - - - ") );
02487
02488 root.appendChild( savePluginState(doc) );
02489
02490 root.appendChild( doc.createComment(" - - - - - - - - - - - - - - ") );
02491
02492 if( checkbox_datasource->isChecked() )
02493 {
02494 QDomElement loaded_list = doc.createElement( "previouslyLoaded_Datafiles" );
02495
02496 for(const auto& loaded: _loaded_datafiles)
02497 {
02498 QDomElement file_elem = doc.createElement( "fileInfo" );
02499 file_elem.setAttribute("filename", loaded.filename );
02500 file_elem.setAttribute("prefix", loaded.prefix );
02501
02502 QDomElement datasources_elem = doc.createElement( "selected_datasources" );
02503 QString topics_list = loaded.selected_datasources.join(";");
02504 datasources_elem.setAttribute("value", topics_list);
02505 file_elem.appendChild( datasources_elem );
02506
02507 file_elem.appendChild( loaded.plugin_config.firstChild() );
02508 loaded_list.appendChild( file_elem );
02509 }
02510 root.appendChild( loaded_list );
02511
02512 if( _current_streamer )
02513 {
02514 QDomElement loaded_streamer = doc.createElement( "previouslyLoaded_Streamer" );
02515 QString streamer_name = _current_streamer->name();
02516 streamer_name.replace(" ", "_");
02517 loaded_streamer.setAttribute("name", streamer_name );
02518 root.appendChild( loaded_streamer );
02519 }
02520 }
02521
02522 root.appendChild( doc.createComment(" - - - - - - - - - - - - - - ") );
02523 if( checkbox_snippets->isChecked() )
02524 {
02525 QDomElement custom_equations = doc.createElement("customMathEquations");
02526 for (const auto& custom_it: _custom_plots)
02527 {
02528 const auto& custom_plot = custom_it.second;
02529 custom_equations.appendChild( custom_plot->xmlSaveState(doc) );
02530 }
02531 root.appendChild(custom_equations);
02532
02533 QByteArray snippets_xml_text = settings.value("AddCustomPlotDialog.savedXML",
02534 QByteArray() ).toByteArray();
02535 auto snipped_saved = GetSnippetsFromXML(snippets_xml_text);
02536 auto snippets_root = ExportSnippets( snipped_saved, doc);
02537 root.appendChild(snippets_root);
02538 }
02539 root.appendChild( doc.createComment(" - - - - - - - - - - - - - - ") );
02540
02541 QFile file(fileName);
02542 if (file.open(QIODevice::WriteOnly)) {
02543 QTextStream stream(&file);
02544 stream << doc.toString() << endl;
02545 }
02546 }
02547
02548 void MainWindow::on_actionFullscreen_triggered()
02549 {
02550 static bool first_call = true;
02551 if( first_call && !_minimized )
02552 {
02553 first_call = false;
02554 QMessageBox::information(this, "Remember!", "Press F10 to switch back to the normal view");
02555 }
02556
02557 _minimized = !_minimized;
02558
02559 ui->leftFrame->setVisible(!_minimized);
02560 ui->widgetOptions->setVisible( !_minimized && ui->pushButtonOptions->isChecked() );
02561 ui->widgetTimescale->setVisible(!_minimized);
02562 ui->menuBar->setVisible(!_minimized);
02563
02564 for (auto& it: TabbedPlotWidget::instances() )
02565 {
02566 it.second->setControlsVisible( !_minimized );
02567 }
02568 }
02569
02570 void MainWindow::on_actionLoadDummyData_triggered()
02571 {
02572 buildDummyData();
02573 }
02574
02575 void MainWindow::on_actionFunctionEditor_triggered()
02576 {
02577 AddCustomPlotDialog dialog(_mapped_plot_data, _custom_plots, this);
02578 dialog.setEditorMode( AddCustomPlotDialog::FUNCTION_ONLY );
02579 dialog.exec();
02580 }
02581
02582 void MainWindow::on_actionClearRecentData_triggered()
02583 {
02584 QMenu* menu = ui->menuRecentData;
02585 for (QAction *action: menu->actions())
02586 {
02587 if ( action->isSeparator() ){
02588 break;
02589 }
02590 menu->removeAction( action );
02591 }
02592 menu->setEnabled(false);
02593 QSettings settings;
02594 settings.setValue("MainWindow.recentlyLoadedDatafile", {} );
02595 }
02596
02597 void MainWindow::on_actionClearRecentLayout_triggered()
02598 {
02599 QMenu* menu = ui->menuRecentLayout;
02600 for (QAction *action: menu->actions())
02601 {
02602 if ( action->isSeparator() ){
02603 break;
02604 }
02605 menu->removeAction( action );
02606 }
02607 menu->setEnabled(false);
02608 QSettings settings;
02609 settings.setValue("MainWindow.recentlyLoadedLayout", {} );
02610 }
02611
02612 void MainWindow::on_actionDeleteAllData_triggered()
02613 {
02614 QMessageBox msgBox(this);
02615 msgBox.setWindowTitle("Warning. Can't be undone.");
02616 msgBox.setText(tr("Do you want to remove the previously loaded data?\n"));
02617 msgBox.addButton(QMessageBox::No);
02618 msgBox.addButton(QMessageBox::Yes);
02619 msgBox.setDefaultButton(QMessageBox::Yes);
02620
02621 auto reply = msgBox.exec();
02622
02623 if( reply == QMessageBox::No ) {
02624 return;
02625 }
02626
02627
02628
02629
02630
02631
02632
02633
02634
02635
02636
02637
02638
02639
02640
02641
02642
02643
02644
02645 {
02646 deleteAllData();
02647 }
02648 }
02649
02650
02651
02652