diff --git a/CMakeLists.txt b/CMakeLists.txt index d7df653c4cf5c8deb13154eaaa1df552efb201b1..a8adbab03252d28ea14a2edf67cd4ce1e4f8e2df 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.5.1 ) +project( visimpl VERSION 1.5.2 ) set( visimpl_VERSION_ABI 6 ) SET( VISIMPL_LICENSE "GPL") diff --git a/visimpl/MainWindow.cpp b/visimpl/MainWindow.cpp index 4214721b734431a41ebd172c38cd0420693da959..2f10157ae8ccdf4f00c302b32be675a54202fa9e 100644 --- a/visimpl/MainWindow.cpp +++ b/visimpl/MainWindow.cpp @@ -857,6 +857,7 @@ namespace visimpl _selectionManager->setWindowModality( Qt::WindowModal ); _selectionManager->setMinimumHeight( 300 ); _selectionManager->setMinimumWidth( 500 ); + _selectionManager->setWindowIcon(QIcon( ":/visimpl.png" )); connect( _selectionManager, SIGNAL( selectionChanged( void ) ), this, SLOT( selectionManagerChanged( void ) ) ); diff --git a/visimpl/SelectionManagerWidget.cpp b/visimpl/SelectionManagerWidget.cpp index 9e3175e7b7f27bd853ea33524e092bbd83086cff..821dec4d2d52edf10296b4440ddf26dfc43855f8 100644 --- a/visimpl/SelectionManagerWidget.cpp +++ b/visimpl/SelectionManagerWidget.cpp @@ -21,16 +21,40 @@ */ // ViSimpl -#include "SelectionManagerWidget.h" - -// Qt -#include <QGridLayout> -#include <QGroupBox> -#include <QLabel> -#include <QFileDialog> -#include <QMessageBox> -#include <QTextStream> -#include <QShortcut> +#include <qabstractitemmodel.h> +#include <qabstractitemview.h> +#include <qalgorithms.h> +#include <qboxlayout.h> +#include <qchar.h> +#include <qdir.h> +#include <qfile.h> +#include <qfiledialog.h> +#include <qflags.h> +#include <qgridlayout.h> +#include <qgroupbox.h> +#include <qicon.h> +#include <qiodevice.h> +#include <qitemselectionmodel.h> +#include <qkeysequence.h> +#include <qlabel.h> +#include <qlineedit.h> +#include <qlist.h> +#include <qlistview.h> +#include <qmessagebox.h> +#include <qnamespace.h> +#include <qobject.h> +#include <qobjectdefs.h> +#include <qpushbutton.h> +#include <qradiobutton.h> +#include <qshortcut.h> +#include <qstandarditemmodel.h> +#include <qstring.h> +#include <qstringlist.h> +#include <qtabwidget.h> +#include <qtextstream.h> +#include <qvariant.h> +#include <sumrice/types.h> +#include <visimpl/SelectionManagerWidget.h> namespace visimpl { @@ -109,16 +133,36 @@ namespace visimpl _listViewAvailable->setModel( _modelAvailable ); _listViewSelected->setModel( _modelSelected ); - _buttonAddToSelection = new QPushButton( "-->" ); + _buttonAddToSelection = new QPushButton(QIcon(":/icons/right.svg"), ""); _buttonAddToSelection->setToolTip( tr( "Add to selected GIDs" )); - _buttonRemoveFromSelection = new QPushButton( "<--" ); + _buttonRemoveFromSelection = new QPushButton(QIcon(":/icons/left.svg"), ""); _buttonRemoveFromSelection->setToolTip( tr( "Remove from selected GIDs" )); layoutSelection->addWidget( _listViewAvailable, 1, 0, 5, 1 ); layoutSelection->addWidget( _buttonAddToSelection, 2, 1, 1, 1 ); layoutSelection->addWidget( _buttonRemoveFromSelection, 4, 1, 1, 1 ); layoutSelection->addWidget( _listViewSelected, 1, 2, 5, 1 ); + layoutSelection->addWidget( new QLabel("Selected:"), 6,0,1,1); + + QRegExp rx("[0-9 \\-\\,]+"); + auto validator = new QRegExpValidator(rx, this); + + _rangeEdit = new QLineEdit(); + _rangeEdit->setPlaceholderText("Nothing selected"); + _rangeEdit->setValidator(validator); + + auto rangeLayout = new QHBoxLayout(); + rangeLayout->addWidget(new QLabel("Selected:"), 0); + rangeLayout->addWidget(_rangeEdit, 1); + + layoutSelection->addLayout(rangeLayout, 6, 0, 1, 4); + + connect(_rangeEdit, SIGNAL(editingFinished()), + this, SLOT(_updateRangeEdit())); + + connect(_listViewAvailable->selectionModel(), SIGNAL(selectionChanged(const QItemSelection &, const QItemSelection&)), + this, SLOT(_updateRangeEdit())); connect( _buttonAddToSelection, SIGNAL( clicked( void )), this, SLOT( _addToSelected( void ))); @@ -244,8 +288,10 @@ namespace visimpl for( auto gid : gids ) { const auto text = QString::number(gid); + auto idx = new QStandardItem(text); + idx->setData(gid); - available << new QStandardItem(text); + available << idx; selected << new QStandardItem(text); _gidIndex.insert( std::make_pair( gid, index )); @@ -279,11 +325,13 @@ namespace visimpl { _labelAvailable->setText( QString( "Available GIDs: ") + QString::number( _gidsAvailable.size( ))); _labelSelection->setText( QString( "Selected GIDs: ") + QString::number( _gidsSelected.size( ))); + + _buttonRemoveFromSelection->setEnabled(!_gidsSelected.empty()); } void SelectionManagerWidget::_addToSelected( void ) { - auto selectedIndices = + const auto selectedIndices = _listViewAvailable->selectionModel( )->selectedIndexes( ); if( selectedIndices.size( ) <= 0 ) @@ -294,7 +342,7 @@ namespace visimpl if( index.row( ) < 0 ) continue; - auto item = _modelAvailable->itemFromIndex( index ); + const auto item = _modelAvailable->itemFromIndex( index ); bool ok; unsigned int gid = item->data( Qt::DisplayRole ).toUInt( &ok ); @@ -302,7 +350,7 @@ namespace visimpl _gidsAvailable.erase( gid ); _gidsSelected.insert( gid ); - auto gidIndex = _gidIndex.find( gid ); + const auto gidIndex = _gidIndex.find( gid ); assert( gidIndex != _gidIndex.end( )); _listViewAvailable->setRowHidden( gidIndex->second, true ); @@ -310,6 +358,7 @@ namespace visimpl } _listViewAvailable->selectionModel( )->clearSelection( ); + _rangeEdit->clear(); _updateListsLabelNumbers( ); } @@ -342,6 +391,120 @@ namespace visimpl _updateListsLabelNumbers( ); } + void SelectionManagerWidget::_updateRangeEdit() + { + auto lEdit = qobject_cast<QLineEdit*>(sender()); + if(lEdit) + { + const auto parts = _rangeEdit->text().split(','); + const auto model = _listViewAvailable->model(); + QItemSelection selection; + + auto processPart = [&model, &selection, this](const QString &s) + { + if(s.isEmpty()) return true; + const auto trim = s.trimmed(); + bool ok = false; + + if(trim.contains("-")) + { + const auto values = trim.split("-"); + if(values.size() > 2) return false; + const auto min = values.first().trimmed().toULongLong(&ok); + if(!ok) return false; + const auto max = values.last().trimmed().toULongLong(&ok); + if(!ok || max < min) return false; + const auto minIt = _gidIndex.find(min); + const auto maxIt = _gidIndex.find(max); + if(minIt == _gidIndex.end() || maxIt == _gidIndex.end()) return false; + + const auto range = QItemSelectionRange(model->index((*minIt).second, 0), model->index((*maxIt).second, 0)); + selection.append(range); + return true; + } + + // not a range + const auto value = trim.toULongLong(&ok); + if(!ok) return false; + const auto it = _gidIndex.find(value); + if(it == _gidIndex.end()) return false; + const auto item = model->index((*it).second, 0); + selection.append(QItemSelectionRange(item, item)); + return true; + }; + + auto it = std::find_if_not(parts.cbegin(), parts.cend(), processPart); + if(it != parts.cend()) + { + lEdit->setStyleSheet("QLineEdit { background: rgb(255, 160, 160); selection-background-color: rgb(0, 200, 200); }"); + } + else + { + lEdit->setStyleSheet(""); + } + + if(!selection.isEmpty()) + { + auto sModel = _listViewAvailable->selectionModel(); + sModel->blockSignals(true); + sModel->select(selection, QItemSelectionModel::ClearAndSelect); + sModel->blockSignals(false); + _listViewAvailable->scrollTo(selection.last().bottomRight(), QAbstractItemView::EnsureVisible); + _listViewAvailable->repaint(); + } + } + else + { + auto sModel = qobject_cast<QItemSelectionModel*>(sender()); + if(sModel) + { + const auto selection = sModel->selection(); + std::vector<Range> ranges; + + auto processRange = [&ranges](const QItemSelectionRange &r) + { + bool ok = false; + const auto min = r.topLeft(); + if(!min.isValid()) return false; + const auto minData = min.data(Qt::UserRole + 1); + if(!minData.isValid()) return false; + const auto minValue = minData.toULongLong(&ok); + if(!ok) return false; + + const auto max = r.bottomRight(); + if(!max.isValid()) return false; + if(min != max) + { + const auto maxData = max.data(Qt::UserRole + 1); + if(!maxData.isValid()) return false; + const auto maxValue = maxData.toULongLong(&ok); + if(!ok) return false; + + ranges.emplace_back(minValue, maxValue); + return true; + } + + ranges.emplace_back(minValue, minValue); + return true; + }; + auto it = std::find_if_not(selection.cbegin(), selection.cend(), processRange); + if(it != selection.cend() || ranges.empty()) return; + + ranges = mergeRanges(ranges); + + _rangeEdit->setStyleSheet(""); + _rangeEdit->blockSignals(true); + _rangeEdit->setText(printRanges(ranges)); + _rangeEdit->blockSignals(false); + _rangeEdit->repaint(); + } + else + { + std::cerr << "Unable to indentify sender! " << __FILE__ << "," << __LINE__ << std::endl; + } + } + } + void SelectionManagerWidget::_saveToFile( const QString& filePath, const QString& separator, const QString& prefix, @@ -353,6 +516,7 @@ namespace visimpl msgBox.setWindowTitle("Selection save"); msgBox.setText( "The prefix and the suffix cannot contain the separator character." ); msgBox.setStandardButtons( QMessageBox::Ok ); + msgBox.setWindowIcon(QIcon(":/visimpl.png")); msgBox.exec( ); return; @@ -364,6 +528,7 @@ namespace visimpl QMessageBox msgBox( this ); msgBox.setWindowTitle("Selection save"); msgBox.setText( "The selected file already exists." ); + msgBox.setWindowIcon(QIcon(":/visimpl.png")); msgBox.setInformativeText( "Do you want to overwrite?" ); msgBox.setStandardButtons( QMessageBox::Save | QMessageBox::Cancel ); msgBox.setDefaultButton( QMessageBox::Save ); @@ -429,4 +594,48 @@ namespace visimpl _saveToFile( _lineEditFilePath->text( ), separator, _lineEditPrefix->text( ), _lineEditSuffix->text( )); } + + SelectionManagerWidget::Ranges SelectionManagerWidget::mergeRanges( + SelectionManagerWidget::Ranges &r) + { + Ranges result; + + auto sortRanges = [](const Range &lhs, const Range &rhs) + { + // ranges dont overlap + return lhs.min < rhs.min; + }; + std::sort(r.begin(), r.end(), sortRanges); + + // join sorted ranges + for(auto it = r.cbegin(); it != r.cend(); ++it) + { + if(result.empty()) + { + result.emplace_back(*it); + continue; + } + + if(result.back().canMerge(*it)) + { + result.back() = result.back() + (*it); + } + else + { + result.emplace_back(*it); + } + } + + return result; + } + + QString SelectionManagerWidget::printRanges(const SelectionManagerWidget::Ranges &r) + { + QStringList texts; + auto printRange = [&texts](const Range &item){ texts << item.print(); }; + std::for_each(r.cbegin(), r.cend(), printRange); + + return texts.join(','); + } + } diff --git a/visimpl/SelectionManagerWidget.h b/visimpl/SelectionManagerWidget.h index 9741647e7552d4d5ce85b371b2cb126e5f79905f..115e9c73814e0ceb16538ca654abb19cf93efe66 100644 --- a/visimpl/SelectionManagerWidget.h +++ b/visimpl/SelectionManagerWidget.h @@ -78,6 +78,8 @@ namespace visimpl void _buttonCancelClicked( void ); void _buttonAcceptClicked( void ); + void _updateRangeEdit(); + protected: void _initTabSelection( void ); void _initTabExport( void ); @@ -110,6 +112,7 @@ namespace visimpl QLabel* _labelAvailable; QLabel* _labelSelection; + QLineEdit *_rangeEdit; TUIntUintMap _gidIndex; @@ -128,6 +131,70 @@ namespace visimpl QPushButton* _buttonSave; QString _pathExportDefault; + + private: + /** \struct Range + * \brief Minimal class to represent a range of uint64_t and its basic operations. + * + */ + struct Range + { + unsigned long long min; + unsigned long long max; + + /** \brief Range class constructor. + * \param[in] a Range minimum + * \param[in] b Range maximim + */ + Range(unsigned long a, unsigned long b) + : min { a }, max { b } + {}; + + /** \brief Returns true if the given Range can be merged. + * \param[in] r Range reference. + */ + bool canMerge(const Range &r) + { + // dont overcomplicate it, Qt returns QItemSelections without overlaps. + return (max + 1 == r.min) || (min - 1 == r.max); + }; + + /** \brief Modifies the range struct adding with the given one. + * \param[in] r Range reference. + */ + Range& operator+(const Range &r) + { + if (max + 1 == r.min) + max = r.max; + else + min = r.min; + + return *this; + }; + + /** \brief Returns a QString with the range. + * + */ + QString print() const + { + if(min != max) return QString("%1-%2").arg(min).arg(max); + return QString("%1").arg(min); + } + }; + + using Ranges = std::vector<Range>; + + /** \brief Returns the minimal vector of ranges representing the given one. + * \param[inout] r Vector of ranges. Returned sorted. + * + */ + Ranges mergeRanges(Ranges &r); + + /** \brief Returns a QString with the given ranges separated by commas. + * + */ + QString printRanges(const Ranges &r); + }; } diff --git a/visimpl/icons/left.svg b/visimpl/icons/left.svg new file mode 100644 index 0000000000000000000000000000000000000000..98f73b025281e546ce7745032e3630105cf6b8b0 --- /dev/null +++ b/visimpl/icons/left.svg @@ -0,0 +1,3 @@ +<svg width="48px" height="48px" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M24 5L6 24L24 43L24 31L42 31V17H24V5Z" fill="#2F88FF" stroke="black" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/> +</svg> diff --git a/visimpl/icons/right.svg b/visimpl/icons/right.svg new file mode 100644 index 0000000000000000000000000000000000000000..035cffb1c17314a6afc425902410d1aeb3e7194b --- /dev/null +++ b/visimpl/icons/right.svg @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="48px" + height="48px" + viewBox="0 0 48 48" + fill="none" + version="1.1" + id="svg4" + sodipodi:docname="right.svg" + inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"> + <metadata + id="metadata10"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + </cc:Work> + </rdf:RDF> + </metadata> + <defs + id="defs8" /> + <sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="736" + inkscape:window-height="480" + id="namedview6" + showgrid="false" + inkscape:zoom="4.9166667" + inkscape:cx="24" + inkscape:cy="24" + inkscape:window-x="561" + inkscape:window-y="148" + inkscape:window-maximized="0" + inkscape:current-layer="svg4" /> + <path + d="m 24.000075,5 18,19 -18,19 V 31 H 6.0000755 V 17 H 24.000075 Z" + id="path2" + inkscape:connector-curvature="0" + style="fill:#2f88ff;stroke:#000000;stroke-width:4;stroke-linecap:round;stroke-linejoin:round" /> +</svg> diff --git a/visimpl/resources.qrc b/visimpl/resources.qrc index efa370bf25914c07a975b40cd6a8b841a4830ea8..950eb7a705119479dd75028f4258d9b925713415 100644 --- a/visimpl/resources.qrc +++ b/visimpl/resources.qrc @@ -28,6 +28,8 @@ <file>icons/stackviz.svg</file> <file>icons/recorder.svg</file> <file>icons/toolconfig.svg</file> + <file>icons/left.svg</file> + <file>icons/right.svg</file> <file>visimpl.png</file> </qresource> </RCC>