diff --git a/CMakeLists.txt b/CMakeLists.txt index 993fe5264d5e7b09528188eef249fd8eb8988dbe..782ece679bf4cb4e6afec10a5fb1b882911e6f95 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,7 +9,7 @@ cmake_minimum_required( VERSION 3.1 FATAL_ERROR ) # visimpl project and version -project( visimpl VERSION 1.2.1 ) +project( visimpl VERSION 1.3.0 ) set( visimpl_VERSION_ABI 6 ) SET( VISIMPL_LICENSE "GPL") diff --git a/visimpl/DomainManager.cpp b/visimpl/DomainManager.cpp index 0e88bcc10c5da8e8bb2f21e1f1cde075b59cda09..17284074b88458205441d8a9f2c4094beed1bef9 100644 --- a/visimpl/DomainManager.cpp +++ b/visimpl/DomainManager.cpp @@ -422,7 +422,6 @@ namespace visimpl group->source( )->active( state ); } - void DomainManager::_updateGroupsModels( void ) { for( auto group : _groups ) @@ -626,7 +625,7 @@ namespace visimpl { for( auto group : _groups ) { - if( !group->dirty( )) + if( !group->dirty( ) || !group->active()) continue; if( group->cached( )) diff --git a/visimpl/MainWindow.cpp b/visimpl/MainWindow.cpp index 1681deb1c43b4bc24ea2f9c215bdde70d9b4b28d..a0dfebece0027059e41a109cfcbd70a8f9a33f96 100644 --- a/visimpl/MainWindow.cpp +++ b/visimpl/MainWindow.cpp @@ -59,6 +59,9 @@ #include <QPushButton> #include <QToolBox> #include <QtGlobal> +#include <QJsonDocument> +#include <QJsonObject> +#include <QJsonArray> #include <thread> @@ -115,6 +118,8 @@ namespace visimpl , _circuitScaleZ( nullptr ) , _buttonImportGroups( nullptr ) , _buttonClearGroups( nullptr ) + , _buttonLoadGroups{nullptr} + , _buttonSaveGroups{nullptr} , _buttonAddGroup( nullptr ) , _buttonClearSelection( nullptr ) , _selectionSizeLabel( nullptr ) @@ -917,6 +922,11 @@ namespace visimpl _buttonImportGroups = new QPushButton( "Import from..." ); _buttonClearGroups = new QPushButton( "Clear" ); _buttonClearGroups->setEnabled( false ); + _buttonLoadGroups = new QPushButton("Load"); + _buttonLoadGroups->setToolTip(tr("Load Groups from disk")); + _buttonSaveGroups = new QPushButton("Save"); + _buttonSaveGroups->setToolTip(tr("Save Groups to disk")); + _buttonSaveGroups->setEnabled(false); { QWidget* groupContainer = new QWidget( ); @@ -934,8 +944,10 @@ namespace visimpl groupOuterLayout->setMargin( 0 ); groupOuterLayout->addWidget( _buttonImportGroups, 0, 0, 1, 1 ); groupOuterLayout->addWidget( _buttonClearGroups, 0, 1, 1, 1 ); + groupOuterLayout->addWidget( _buttonLoadGroups, 1, 0, 1, 1 ); + groupOuterLayout->addWidget( _buttonSaveGroups, 1, 1, 1, 1 ); - groupOuterLayout->addWidget( scrollGroups, 1, 0, 1, 2 ); + groupOuterLayout->addWidget( scrollGroups, 2, 0, 1, 2 ); groupBoxGroups->setLayout( groupOuterLayout ); } @@ -1130,6 +1142,9 @@ namespace visimpl connect( _buttonClearGroups, SIGNAL( clicked( void ) ), this, SLOT( clearGroups( void ) ) ); + connect( _buttonLoadGroups, SIGNAL( clicked(void)), this, SLOT(loadGroups())); + connect( _buttonSaveGroups, SIGNAL( clicked(void)), this, SLOT(saveGroups())); + _alphaNormalButton->setChecked( true ); } @@ -1665,7 +1680,7 @@ namespace visimpl this, SLOT(onGroupPreview())); QCheckBox* visibilityCheckbox = new QCheckBox( "active" ); - visibilityCheckbox->setChecked( true ); + visibilityCheckbox->setChecked( group->active() ); connect( visibilityCheckbox, SIGNAL( clicked( ) ), this, SLOT( checkGroupsVisibility( ) ) ); @@ -1693,6 +1708,7 @@ namespace visimpl _groupLayout->addWidget( container ); _buttonClearGroups->setEnabled( true ); + _buttonSaveGroups->setEnabled(true); } void MainWindow::Stop( bool notify ) @@ -1735,6 +1751,7 @@ void MainWindow::clearGroups( void ) _buttonImportGroups->setEnabled( true ); _buttonClearGroups->setEnabled( false ); + _buttonSaveGroups->setEnabled(false); } void MainWindow::importVisualGroups( void ) @@ -2083,6 +2100,7 @@ void MainWindow::clearGroups( void ) m_type = type; m_subsetEventFile = subsetEventFile; _openGLWidget->loadData( arg_1, type, simType, arg_2 ); + _lastOpenedNetworkFileName = QString::fromStdString(arg_1); } catch(const std::exception &e) { @@ -2156,6 +2174,307 @@ void MainWindow::clearGroups( void ) } } + void MainWindow::loadGroups() + { + const auto title = tr("Load Groups"); + + if(!_domainManager->groups().empty()) + { + const auto message = tr("Loading groups from disk will erase the " + "current groups. Do you want to continue?"); + + QMessageBox msgbox(this); + msgbox.setWindowTitle(title); + msgbox.setText(message); + msgbox.setIcon(QMessageBox::Icon::Question); + msgbox.setWindowIcon(QIcon(":/visimpl.png")); + msgbox.setStandardButtons(QMessageBox::Cancel|QMessageBox::Ok); + msgbox.setDefaultButton(QMessageBox::Button::Ok); + + if(QMessageBox::Ok != msgbox.exec()) + return; + } + + QFileInfo lastFile{_lastOpenedNetworkFileName}; + const auto fileName = QFileDialog::getOpenFileName(this, title, lastFile.path(), + tr("Json files (*.json)"), + nullptr, QFileDialog::ReadOnly|QFileDialog::DontUseNativeDialog); + if(fileName.isEmpty()) return; + + QFile file{fileName}; + if(!file.open(QIODevice::ReadOnly)) + { + const auto message = tr("Couldn't open file %1").arg(fileName); + + QMessageBox msgbox(this); + msgbox.setWindowTitle(title); + msgbox.setIcon(QMessageBox::Icon::Critical); + msgbox.setText(message); + msgbox.setWindowIcon(QIcon(":/visimpl.png")); + msgbox.setStandardButtons(QMessageBox::Ok); + msgbox.exec(); + return; + } + + const auto contents = file.readAll(); + QJsonParseError jsonError; + const auto jsonDoc = QJsonDocument::fromJson(contents, &jsonError);; + if(jsonDoc.isNull() || !jsonDoc.isObject()) + { + const auto message = tr("Couldn't read the contents of %1 or error at parsing.").arg(fileName); + + QMessageBox msgbox{this}; + msgbox.setWindowTitle(title); + msgbox.setIcon(QMessageBox::Icon::Critical); + msgbox.setText(message); + msgbox.setWindowIcon(QIcon(":/visimpl.png")); + msgbox.setStandardButtons(QMessageBox::Ok); + msgbox.setDetailedText(jsonError.errorString()); + msgbox.exec(); + return; + } + + const auto jsonObj = jsonDoc.object(); + if(jsonObj.isEmpty()) + { + const auto message = tr("Error at parsing.").arg(fileName); + + QMessageBox msgbox{this}; + msgbox.setWindowTitle(title); + msgbox.setIcon(QMessageBox::Icon::Critical); + msgbox.setText(message); + msgbox.setWindowIcon(QIcon(":/visimpl.png")); + msgbox.setStandardButtons(QMessageBox::Ok); + msgbox.exec(); + return; + } + + const QFileInfo currentFile{_lastOpenedNetworkFileName}; + const QString jsonGroupsFile = jsonObj.value("filename").toString(); + if(jsonGroupsFile.compare(currentFile.fileName(), Qt::CaseInsensitive) != 0) + { + const auto message = tr("This groups definitions are from file %1. Current file" + " is %2. Do you want to continue?").arg(jsonGroupsFile).arg(currentFile.fileName()); + + QMessageBox msgbox{this}; + msgbox.setWindowTitle(title); + msgbox.setIcon(QMessageBox::Icon::Question); + msgbox.setText(message); + msgbox.setWindowIcon(QIcon(":/visimpl.png")); + msgbox.setStandardButtons(QMessageBox::Cancel|QMessageBox::Ok); + msgbox.setDefaultButton(QMessageBox::Ok); + + if(QMessageBox::Ok != msgbox.exec()) + return; + } + + QApplication::setOverrideCursor(Qt::WaitCursor); + + clearGroups(); + const auto jsonGroups = jsonObj.value("groups").toArray(); + + std::vector<VisualGroup *> groupsList; + const auto createGroup = [&groupsList, this](const QJsonValue &v) + { + const auto o = v.toObject(); + + const auto name = o.value("name").toString(); + const auto overrideGIDS = o.value("override").toBool(false); + const auto gidsStrings = o.value("gids").toString().split(","); + + GIDUSet gids; + auto addGids = [&gids](const QString s) + { + if(s.contains(":")) + { + auto limits = s.split(":"); + for(unsigned int id = limits.first().toUInt(); id <= limits.last().toUInt(); ++id) + gids.insert(id); + } + else + { + gids.insert(s.toUInt()); + } + }; + std::for_each(gidsStrings.cbegin(), gidsStrings.cend(), addGids); + + auto group = _domainManager->addVisualGroup(gids, name.toStdString(), overrideGIDS); + auto idx = _domainManager->groups().size()-1; + addGroupControls(group, idx, gids.size()); + const auto active = o.value("active").toBool(true); + auto checkbox = std::get< gr_checkbox >(_groupsVisButtons.at(idx)); + checkbox->setChecked(active); + + _domainManager->setVisualGroupState( idx, active ); + + const auto functionPairs = o.value("function").toString().split(";"); + TTransferFunction function; + auto addFunctionPair = [&function](const QString &s) + { + const auto parts = s.split(","); + Q_ASSERT(parts.size() == 2); + const auto value = parts.first().toFloat(); + const auto color = QColor(parts.last()); + function.emplace_back(value, color); + }; + std::for_each(functionPairs.cbegin(), functionPairs.cend(), addFunctionPair); + + const auto sizePairs = o.value("sizes").toString().split(";"); + TSizeFunction sizes; + auto addSizes = [&sizes](const QString &s) + { + const auto parts = s.split(","); + Q_ASSERT(parts.size() == 2); + const auto a = parts.first().toFloat(); + const auto b = parts.last().toFloat(); + sizes.emplace_back(a, b); + }; + std::for_each(sizePairs.cbegin(), sizePairs.cend(), addSizes); + + updateGroupColors(idx, function, sizes); + auto container = std::get< gr_container >(_groupsVisButtons.at(idx)); + auto tfw = qobject_cast<TransferFunctionWidget*>(container->layout()->itemAt(0)->widget()); + if(tfw) + { + tfw->setColorPoints(function, true); + tfw->setSizeFunction(sizes); + tfw->colorChanged(); + tfw->sizeChanged(); + } + }; + std::for_each(jsonGroups.constBegin(), jsonGroups.constEnd(), createGroup); + + _groupLayout->update(); + checkGroupsVisibility(); + + const auto groups = _domainManager->groups(); + _buttonClearGroups->setEnabled( !groups.empty() ); + _buttonSaveGroups->setEnabled( !groups.empty() ); + + QApplication::restoreOverrideCursor(); + } + + void MainWindow::saveGroups() + { + const auto &groups = _domainManager->groups(); + if(groups.empty()) return; + + const auto dateTime = QDateTime::currentDateTime(); + QFileInfo lastFile{_lastOpenedNetworkFileName}; + QString filename = lastFile.dir().absoluteFilePath(lastFile.baseName() + "_groups_" + dateTime.toString("yyyy-MM-dd-hh-mm") + ".json"); + filename = QFileDialog::getSaveFileName(this, tr("Save Groups"), filename, tr("Json files (*.json)"), nullptr, QFileDialog::DontUseNativeDialog); + + if(filename.isEmpty()) return; + + QFile wFile{filename}; + if(!wFile.open(QIODevice::WriteOnly|QIODevice::Text|QIODevice::Truncate)) + { + const auto message = tr("Unable to open file %1 for writing.").arg(filename); + + QMessageBox msgbox{this}; + msgbox.setWindowTitle(tr("Save Groups")); + msgbox.setIcon(QMessageBox::Icon::Critical); + msgbox.setText(message); + msgbox.setWindowIcon(QIcon(":/visimpl.png")); + msgbox.setDefaultButton(QMessageBox::Ok); + msgbox.exec(); + return; + } + + QApplication::setOverrideCursor(Qt::WaitCursor); + + QJsonObject obj; + obj.insert("filename", QFileInfo{_lastOpenedNetworkFileName}.fileName()); + obj.insert("date", dateTime.toString()); + + QJsonArray groupsObjs; + + auto insertGroup = [&groupsObjs, this](const VisualGroup *g) + { + QJsonObject groupObj; + groupObj.insert("name", QString::fromStdString(g->name())); + groupObj.insert("active", g->active()); + + QStringList tfList; + const auto tf = g->colorMapping(); + auto addColors = [&tfList](const TTFColor &c) + { + tfList << QString("%1,%2").arg(c.first).arg(c.second.name(QColor::HexArgb)); + }; + std::for_each(tf.cbegin(), tf.cend(), addColors); + groupObj.insert("function", tfList.join(";")); + + QStringList sizesList; + const auto sizes = g->sizeFunction(); + auto addSizes = [&sizesList](const TSize &s) + { + sizesList << QString("%1,%2").arg(s.first).arg(s.second); + }; + std::for_each(sizes.cbegin(), sizes.cend(), addSizes); + groupObj.insert("sizes", sizesList.join(";")); + + const auto &gids = g->gids(); + std::vector<unsigned int> gidsVec; + std::for_each(gids.cbegin(), gids.cend(), [&gidsVec](unsigned int v){ gidsVec.push_back(v); }); + std::sort(gidsVec.begin(), gidsVec.end()); + QStringList gidsStrings; + std::pair<unsigned int, unsigned int> range = std::make_pair(std::numeric_limits<unsigned int>::max() - 1,std::numeric_limits<unsigned int>::max() - 1); + auto enterNumber = [&range, &gidsStrings]() + { + if(range.first == range.second) + gidsStrings << QString::number(range.first); + else + gidsStrings << QString("%1:%2").arg(range.first).arg(range.second); + }; + + for(auto i = gidsVec.begin(); i != gidsVec.end(); ++i) + { + auto num = *i; + if(num != range.second + 1) + { + if(range.first != std::numeric_limits<unsigned int>::max() - 1) + { + enterNumber(); + } + range.first = num; + } + + range.second = num; + } + enterNumber(); + + groupObj.insert("gids", gidsStrings.join(",")); + + groupsObjs << groupObj; + }; + std::for_each(groups.cbegin(), groups.cend(), insertGroup); + + obj.insert("groups", groupsObjs); + + QJsonDocument doc{obj}; + const auto temp = doc.toJson().toStdString(); + wFile.write(doc.toJson()); + + QApplication::restoreOverrideCursor(); + + if(wFile.error() != QFile::NoError) + { + const auto message = tr("Error saving file %1.").arg(filename); + + QMessageBox msgbox{this}; + msgbox.setWindowTitle(tr("Save Groups")); + msgbox.setIcon(QMessageBox::Icon::Critical); + msgbox.setText(message); + msgbox.setDetailedText(wFile.errorString()); + msgbox.setWindowIcon(QIcon(":/visimpl.png")); + msgbox.setDefaultButton(QMessageBox::Ok); + msgbox.exec(); + } + + wFile.flush(); + wFile.close(); + } + void MainWindow::sendZeroEQPlaybackOperation(const unsigned int op) { #ifdef SIMIL_USE_ZEROEQ diff --git a/visimpl/MainWindow.h b/visimpl/MainWindow.h index baee250be5384fd0e8ef635cd3dbe14206273d08..8945f62bd112406a8484611daa295c9db4b3dcc9 100644 --- a/visimpl/MainWindow.h +++ b/visimpl/MainWindow.h @@ -194,6 +194,16 @@ namespace visimpl */ void onDataLoaded(); + /** \brief Loads groups and its properties from a file on disk. + * + */ + void loadGroups(); + + /** \brief Saves current groups and its properties to a file on disk. + * + */ + void saveGroups(); + protected: void _initSimControlDock( void ); void _initPlaybackDock( void ); @@ -293,6 +303,8 @@ namespace visimpl QPushButton* _buttonImportGroups; QPushButton* _buttonClearGroups; + QPushButton* _buttonLoadGroups; + QPushButton* _buttonSaveGroups; QPushButton* _buttonAddGroup; QPushButton* _buttonClearSelection; QLabel* _selectionSizeLabel; diff --git a/visimpl/SelectionManagerWidget.cpp b/visimpl/SelectionManagerWidget.cpp index 5793d21c4242b572e629be94ca0fccb27fb751c6..9e3175e7b7f27bd853ea33524e092bbd83086cff 100644 --- a/visimpl/SelectionManagerWidget.cpp +++ b/visimpl/SelectionManagerWidget.cpp @@ -51,12 +51,12 @@ namespace visimpl void SelectionManagerWidget::init( void ) { - QVBoxLayout* layoutTop = new QVBoxLayout( ); + auto layoutTop = new QVBoxLayout( ); this->setLayout( layoutTop ); _tabWidget = new QTabWidget( ); QWidget* containerFoot = new QWidget( ); - QGridLayout* layoutFoot = new QGridLayout( ); + auto layoutFoot = new QGridLayout( ); containerFoot->setLayout( layoutFoot ); layoutTop->addWidget( _tabWidget ); @@ -84,10 +84,9 @@ namespace visimpl void SelectionManagerWidget::_initTabSelection( void ) { + auto containerSelection = new QWidget( ); - QWidget* containerSelection = new QWidget( ); - - QGridLayout* layoutSelection = new QGridLayout( ); + auto layoutSelection = new QGridLayout( ); containerSelection->setLayout( layoutSelection ); _labelAvailable = new QLabel( "Available GIDs: 0" ); @@ -132,8 +131,8 @@ namespace visimpl void SelectionManagerWidget::_initTabExport( void ) { - QWidget* containerExport = new QWidget( ); - QGridLayout* layoutExport = new QGridLayout( ); + auto containerExport = new QWidget( ); + auto layoutExport = new QGridLayout( ); containerExport->setLayout( layoutExport ); _pathExportDefault = QDir::currentPath(); @@ -151,16 +150,16 @@ namespace visimpl _buttonBrowse = new QPushButton( "Browse..." ); _buttonSave = new QPushButton( "Save" ); - QGroupBox* groupBoxPrefix = new QGroupBox( "Prefix/Suffix" ); - QGridLayout* layoutPrefix = new QGridLayout( ); + auto groupBoxPrefix = new QGroupBox( "Prefix/Suffix" ); + auto layoutPrefix = new QGridLayout( ); groupBoxPrefix->setLayout( layoutPrefix ); layoutPrefix->addWidget( new QLabel( "Prefix:" ), 0, 0, 1, 1 ); layoutPrefix->addWidget( _lineEditPrefix, 0, 1, 1, 1 ); layoutPrefix->addWidget( new QLabel( "Suffix:" ), 1, 0, 1, 1 ); layoutPrefix->addWidget( _lineEditSuffix, 1, 1, 1, 1 ); - QGroupBox* groupBoxSeparator = new QGroupBox( "Separator" ); - QGridLayout* layoutSeparator = new QGridLayout( ); + auto groupBoxSeparator = new QGroupBox( "Separator" ); + auto layoutSeparator = new QGridLayout( ); layoutSeparator->addWidget( _radioNewLine, 0, 0, 1, 2 ); layoutSeparator->addWidget( _radioSpace, 1, 0, 1, 2 ); layoutSeparator->addWidget( _radioTab, 2, 0, 1, 2 ); @@ -171,8 +170,6 @@ namespace visimpl groupBoxSeparator->setLayout( layoutSeparator ); - - layoutExport->addWidget( new QLabel( "File path:"), 0, 0, 1, 1 ); layoutExport->addWidget( _lineEditFilePath, 0, 1, 1, 4 ); layoutExport->addWidget( _buttonBrowse, 0, 5, 1, 1 ); @@ -180,8 +177,6 @@ namespace visimpl layoutExport->addWidget( groupBoxSeparator, 1, 2, 4, 3 ); layoutExport->addWidget( _buttonSave, 4, 5, 1, 1 ); - - connect( _buttonBrowse, SIGNAL( clicked( void )), this, SLOT( _buttonBrowseClicked( void ))); @@ -352,11 +347,22 @@ namespace visimpl const QString& prefix, const QString& suffix ) { - QFile file; + if(prefix.contains(separator) || suffix.contains(separator)) + { + QMessageBox msgBox( this ); + msgBox.setWindowTitle("Selection save"); + msgBox.setText( "The prefix and the suffix cannot contain the separator character." ); + msgBox.setStandardButtons( QMessageBox::Ok ); + msgBox.exec( ); + + return; + } + QFile file; if( file.exists( filePath )) { QMessageBox msgBox( this ); + msgBox.setWindowTitle("Selection save"); msgBox.setText( "The selected file already exists." ); msgBox.setInformativeText( "Do you want to overwrite?" ); msgBox.setStandardButtons( QMessageBox::Save | QMessageBox::Cancel ); @@ -375,7 +381,7 @@ namespace visimpl std::copy( _gidsSelected.begin( ), _gidsSelected.end( ), gids.begin( )); std::sort( gids.begin( ), gids.end( )); - for( auto gid : gids ) + for( auto &gid : gids ) { outStream << prefix << gid << suffix << separator; }