diff --git a/CMakeLists.txt b/CMakeLists.txt index 34a4686..6762cdc 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,6 +32,8 @@ set(TENMON_SRC src/batchprocessing.cpp src/batchprocessing.h src/batchprocessing.ui src/chartgraph.h src/chartgraph.cpp src/database.cpp src/database.h + src/databasetree.cpp src/databasetree.h + src/databasetreekeys.ui src/databaseview.cpp src/databaseview.h src/delete.cpp src/filemanager.h src/filemanager.cpp src/filemanager.ui @@ -92,6 +94,7 @@ if(STELLARSOLVER_INCLUDE AND STELLARSOLVER_LIB) if(MXE) find_library(GSL_LIB gsl REQUIRED) find_library(GSLCBLAS_LIB gslcblas REQUIRED) + target_compile_definitions(tenmon PRIVATE "stellarsolver_STATIC") target_link_libraries(tenmon PRIVATE ${STELLARSOLVER_LIB} ${GSL_LIB} ${GSLCBLAS_LIB} boost_regex-mt-x64) else(MXE) target_link_libraries(tenmon PRIVATE ${STELLARSOLVER_LIB}) diff --git a/src/databasetree.cpp b/src/databasetree.cpp new file mode 100644 index 0000000..30070c3 --- /dev/null +++ b/src/databasetree.cpp @@ -0,0 +1,428 @@ +#include "databasetree.h" + +#include "database.h" +#include +#include +#include +#include +#include +#include + +DatabaseTreeSettings::DatabaseTreeSettings(const QString &filter, const QStringList &keywords, QWidget *parent) : QDialog(parent) +{ + setWindowTitle(tr("Add tree filter")); + QVBoxLayout *vlayout = new QVBoxLayout(this); + setLayout(vlayout); + + QStringList key = filter.split('/'); + + for(int i = 0; i < 10; i++) + { + QComboBox *comboxBox = new QComboBox(this); + comboxBox->addItem(""); + comboxBox->addItems(keywords); + vlayout->addWidget(comboxBox); + _keywordsSelect.append(comboxBox); + if(i < key.size() && keywords.contains(key[i])) + comboxBox->setCurrentText(key[i]); + } + + QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + connect(buttonBox, &QDialogButtonBox::accepted, this, &DatabaseTreeSettings::accept); + connect(buttonBox, &QDialogButtonBox::rejected, this, &DatabaseTreeSettings::reject); + vlayout->addWidget(buttonBox); +} + +QString DatabaseTreeSettings::keywords() const +{ + QStringList keywords; + for(QComboBox *box : _keywordsSelect) + { + if(box->currentIndex() > 0) + keywords.append(box->currentText()); + } + return keywords.join('/'); +} + +class TreeNode +{ +public: + TreeNode() = default; + TreeNode(TreeNode *parent, const QVariant value, int level) + :_parent(parent) + ,_value(value) + ,_level(level) + {} + const TreeNode* child(size_t idx) const + { + if(idx >= 0 && idx < _children.size()) + return _children[idx].get(); + return nullptr; + } + TreeNode* child(size_t idx) + { + if(idx >= 0 && idx < _children.size()) + return _children[idx].get(); + return nullptr; + } + TreeNode* parent() const + { + return _parent; + } + int row() const + { + if(_parent) + return _parent->indexOf(this); + return 0; + } + int childCount() const + { + if(!_init)return 1; + return _children.size(); + } + const QVariant& value() const + { + return _value; + } + void fill(const QVariantList &list) + { + _init = true; + for(auto &item : list) + _children.push_back(std::make_unique(this, item, _level + 1)); + } + bool filled() const + { + return _init; + } + int level() const + { + return _level; + } +private: + int indexOf(const TreeNode *child) const + { + auto f = [child](const std::unique_ptr &i){ return i.get() == child; }; + auto it = std::find_if(_children.begin(), _children.end(), f); + if(it != _children.end())return std::distance(_children.begin(), it); + return -1; + } + TreeNode *_parent = nullptr; + QVariant _value; + std::vector> _children; + bool _init = false; + int _level = 0; +}; + +DatabaseTree::DatabaseTree(Database *database, QObject *parent) : QAbstractItemModel(parent) + ,_database(database) +{ + _italicFont.setItalic(true); +} + +void DatabaseTree::setKeys(const QStringList &keys) +{ + _keys = keys; + if(!_loaded)return; + beginResetModel(); + prepareQueries(); + _rootNode = std::make_unique(); + fillNode(_rootNode.get()); + endResetModel(); +} + +QModelIndex DatabaseTree::index(int row, int column, const QModelIndex &parent) const +{ + if(!hasIndex(row, column, parent)) + return QModelIndex(); + + TreeNode *node; + + if(!parent.isValid()) + node = _rootNode.get(); + else + node = static_cast(parent.internalPointer()); + + if(node) + { + TreeNode *child = node->child(row); + if(child)return createIndex(row, column, child); + } + + return QModelIndex(); +} + +QModelIndex DatabaseTree::parent(const QModelIndex &index) const +{ + if(!index.isValid()) + return QModelIndex(); + + TreeNode *childNode = static_cast(index.internalPointer()); + const TreeNode *parentNode = childNode->parent(); + + if (parentNode == _rootNode.get()) + return QModelIndex(); + + return createIndex(parentNode->row(), 0, parentNode); +} + +int DatabaseTree::rowCount(const QModelIndex &index) const +{ + if(index.column() > 0)return 0; + + TreeNode *node; + if(!index.isValid()) + node = _rootNode.get(); + else + node = static_cast(index.internalPointer()); + + if(node && node->level() <= _keys.size()) + return node->childCount(); + + return 0; +} + +int DatabaseTree::columnCount(const QModelIndex &index) const +{ + Q_UNUSED(index); + return 1; +} + +QVariant DatabaseTree::data(const QModelIndex &index, int role) const +{ + if(!index.isValid()) + return QVariant(); + + TreeNode *node = static_cast(index.internalPointer()); + if(node == nullptr) + return QVariant(); + + switch(role) + { + case Qt::FontRole: + { + if(node->value().toString().isNull()) + return _italicFont; + return QVariant(); + } + case Qt::DisplayRole: + { + QString str = node->value().toString(); + if(str.isNull())return "NULL"; + else return str; + } + default: + return QVariant(); + } +} + +bool DatabaseTree::canFetchMore(const QModelIndex &parent) const +{ + if(!parent.isValid()) + return false; + + TreeNode *node = static_cast(parent.internalPointer()); + //qDebug() << "Can Fetch more" << node->value(); + if(node) + return !node->filled(); + return false; +} + +void DatabaseTree::fetchMore(const QModelIndex &parent) +{ + if(!parent.isValid()) + return; + + TreeNode *node = static_cast(parent.internalPointer()); + //qDebug() << "Fetch more" << node->value(); + if(node) + { + fillNode(node); + + if(node->childCount() > 0) + { + beginInsertRows(parent, 0, node->childCount() - 1); + endInsertRows(); + } + } +} + +QVariant DatabaseTree::headerData(int section, Qt::Orientation orientation, int role) const +{ + if(orientation == Qt::Horizontal && role == Qt::DisplayRole && section == 0) + return _keys.join('/'); + return QVariant(); +} + +void DatabaseTree::load() +{ + if(!_loaded) + { + _loaded = true; + setKeys(_keys); + } +} + +void DatabaseTree::prepareQueries() +{ + if(!_loaded)return; + + _queries.clear(); + + QString join; + QString where; + + for(int i = 0; i < _keys.size(); i++) + join += QString(" LEFT JOIN fits_headers AS h%1 ON f.id = h%1.id_file AND h%1.key = ?").arg(i); + + for(int i = 0; i < _keys.size(); i++) + { + QString sql = QString("SELECT h%1.value FROM fits_files AS f").arg(i) + join + where + QString(" GROUP BY h%1.value ORDER BY h%1.value").arg(i); + qDebug() << _keys[i] << sql; + QSqlQuery query(sql, _database->db()); + for(auto &val : _keys) + query.addBindValue(val); + + if(where.isEmpty()) + where += QString(" WHERE h%1.value IS ?").arg(i); + else + where += QString(" AND h%1.value IS ?").arg(i); + _queries.append(std::move(query)); + } + + QSqlQuery files("SELECT f.file FROM fits_files AS f " + join + where + " GROUP BY f.id ORDER BY f.file", _database->db()); + for(auto &val : _keys) + files.addBindValue(val); + qDebug() << files.lastQuery(); + _queries.append(std::move(files)); +} + +void DatabaseTree::fillNode(TreeNode *node) +{ + if(node->filled()) + return; + + TreeNode *n = node; + QVariantList vals; + while(n->parent()) + { + vals.prepend(n->value()); + n = n->parent(); + } + + int level = vals.size(); + if(level >= _queries.size()) + { + qWarning() << "Level is too deep"; + node->fill({}); + return; + } + QSqlQuery &q = _queries[level]; + + for(int i = 0; i < level; i++) + q.bindValue(i + _keys.size(), vals[i]); + if(!q.exec()) + { + qWarning() << "Failed to execute query" << q.lastError(); + node->fill({}); + return; + } + + QVariantList list; + while(q.next()) + list.append(q.value(0)); + + node->fill(list); +} + +DatabaseTreeView::DatabaseTreeView(Database *database, QWidget *parent) : QWidget(parent) + ,_database(database) +{ + QVBoxLayout *vlayout = new QVBoxLayout(this); + QHBoxLayout *hlayout = new QHBoxLayout(this); + + _model = new DatabaseTree(database, this); + _treeView = new QTreeView(this); + _treeView->setModel(_model); + _treeView->setHeaderHidden(true); + + QSettings settings; + QStringList filters = settings.value("databasetreeview/filters", QStringList{"OBJECT", "OBJECT/IMAGETYP", "OBJECT/IMAGETYP/FILTER", "OBJECT/IMAGETYP/FILTER/EXPTIME"}).toStringList(); + int selectedFilter = settings.value("databasetreeview/selectedFilter", 2).toInt(); + + _filters = new QComboBox(this); + _filters->addItems(filters); + _filters->setCurrentIndex(selectedFilter); + connect(_filters, &QComboBox::currentTextChanged, this, &DatabaseTreeView::filterChanged); + filterChanged(_filters->currentText()); + + QPushButton *addButton = new QPushButton(tr("Add"), this); + QPushButton *removeButton = new QPushButton(tr("Remove"), this); + + hlayout->addWidget(_filters, 1); + hlayout->addWidget(addButton); + hlayout->addWidget(removeButton); + + vlayout->addLayout(hlayout); + vlayout->addWidget(_treeView); + + connect(_treeView, &QTreeView::activated, [this](const QModelIndex &index){ + if(!_model->hasChildren(index)) + { + QString path = _model->data(index).toString(); + emit loadFile(path); + } + }); + + connect(addButton, &QPushButton::clicked, this, &DatabaseTreeView::addFilter); + connect(removeButton, &QPushButton::clicked, this, &DatabaseTreeView::removeFilter); +} + +DatabaseTreeView::~DatabaseTreeView() +{ + QStringList filters; + for(int i = 0; i < _filters->count(); i++) + filters.append(_filters->itemText(i)); + + QSettings settings; + settings.setValue("databasetreeview/filters", filters); + settings.setValue("databasetreeview/selectedFilter", _filters->currentIndex()); +} + +void DatabaseTreeView::addFilter() +{ + QStringList keywords = _database->getFitsKeywords(); + DatabaseTreeSettings settings(_filters->currentText(), keywords, this); + int result = settings.exec(); + if(result == QDialog::Accepted) + { + QString keywords = settings.keywords(); + int idx = _filters->findText(keywords); + if(idx == -1) + { + _filters->addItem(keywords); + _filters->setCurrentText(keywords); + } + else + { + _filters->setCurrentIndex(idx); + } + } +} + +void DatabaseTreeView::removeFilter() +{ + if(_filters->count() > 1) + _filters->removeItem(_filters->currentIndex()); +} + +void DatabaseTreeView::filterChanged(const QString &filter) +{ + QStringList keys = filter.split('/'); + _model->setKeys(keys); +} + +void DatabaseTreeView::visible(bool visible) +{ + if(visible) + _model->load(); +} diff --git a/src/databasetree.h b/src/databasetree.h new file mode 100644 index 0000000..f515d13 --- /dev/null +++ b/src/databasetree.h @@ -0,0 +1,69 @@ +#ifndef DATABASETREE_H +#define DATABASETREE_H + +#include +#include +#include +#include +#include +#include +#include + +class Database; +class TreeNode; + +class DatabaseTreeSettings : public QDialog +{ +public: + explicit DatabaseTreeSettings(const QString &filter, const QStringList &keywords, QWidget *parent = nullptr); + QString keywords() const; +private: + QVector _keywordsSelect; +}; + +class DatabaseTree : public QAbstractItemModel +{ +public: + explicit DatabaseTree(Database *database, QObject *parent = nullptr); + void setKeys(const QStringList &keys); + QModelIndex index(int row, int column, const QModelIndex &parent) const override; + QModelIndex parent(const QModelIndex &index) const override; + int rowCount(const QModelIndex &index) const override; + int columnCount(const QModelIndex &index) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + bool canFetchMore(const QModelIndex &parent) const override; + void fetchMore(const QModelIndex &parent) override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + void load(); +private: + void prepareQueries(); + void fillNode(TreeNode *node); + Database *_database = nullptr; + std::unique_ptr _rootNode; + QVector _queries; + QStringList _keys; + QFont _italicFont; + bool _loaded = false; +}; + +class DatabaseTreeView : public QWidget +{ + Q_OBJECT +public: + explicit DatabaseTreeView(Database *database, QWidget *parent = nullptr); + virtual ~DatabaseTreeView(); +public slots: + void addFilter(); + void removeFilter(); + void filterChanged(const QString &filter); + void visible(bool visible); +signals: + void loadFile(const QString &file); +private: + QComboBox *_filters = nullptr; + QTreeView *_treeView = nullptr; + DatabaseTree *_model = nullptr; + Database *_database = nullptr; +}; + +#endif // DATABASETREE_H diff --git a/src/databasetreekeys.ui b/src/databasetreekeys.ui new file mode 100644 index 0000000..9ae73de --- /dev/null +++ b/src/databasetreekeys.ui @@ -0,0 +1,88 @@ + + + Dialog + + + + 0 + 0 + 511 + 487 + + + + Dialog + + + + + 120 + 390 + 341 + 32 + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + 60 + 30 + 86 + 26 + + + + + + + 180 + 30 + 113 + 26 + + + + + + + + buttonBox + accepted() + Dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 542c272..3d02459 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -128,6 +128,8 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) _plateSolving->hide(); #endif + _databaseTree = new DatabaseTree(m_database, this); + QToolBar *navigationToolbar = new QToolBar(tr("Navigation toolbar"), this); navigationToolbar->setObjectName("navigationtoolbar"); navigationToolbar->hide(); @@ -173,6 +175,15 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) histogramDock->hide(); addDockWidget(Qt::LeftDockWidgetArea, histogramDock); + DatabaseTreeView *databaseTreeView = new DatabaseTreeView(m_database, this); + QDockWidget *databaseTreeDock = new QDockWidget(tr("Database Tree"), this); + databaseTreeDock->setObjectName("databasetreeDock"); + databaseTreeDock->setWidget(databaseTreeView); + databaseTreeDock->hide(); + connect(databaseTreeDock, &QDockWidget::visibilityChanged, databaseTreeView, &DatabaseTreeView::visible); + connect(databaseTreeView, &DatabaseTreeView::loadFile, this, static_cast(&MainWindow::loadFile)); + addDockWidget(Qt::BottomDockWidgetArea, databaseTreeDock); + setWindowTitle(tr("Tenmon")); connect(m_ringList, &ImageRingList::pixmapLoaded, m_image, &ImageScrollArea::imageLoaded); @@ -337,6 +348,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) dockMenu->addAction(navigationToolbar->toggleViewAction()); dockMenu->addAction(filesystemDock->toggleViewAction()); dockMenu->addAction(databaseViewDock->toggleViewAction()); + dockMenu->addAction(databaseTreeDock->toggleViewAction()); if(filetreeDock)dockMenu->addAction(filetreeDock->toggleViewAction()); dockMenu->addAction(histogramDock->toggleViewAction()); #ifdef PLATESOLVER diff --git a/src/mainwindow.h b/src/mainwindow.h index ad9124a..2855171 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -11,6 +11,7 @@ #include "stretchtoolbar.h" #include "databaseview.h" #include "platesolving.h" +#include "databasetree.h" class MainWindow : public QMainWindow { @@ -24,6 +25,7 @@ class MainWindow : public QMainWindow Filetree *m_filetree; DataBaseView *m_databaseView; PlateSolving *_plateSolving = nullptr; + DatabaseTree *_databaseTree = nullptr; static int socketPair[2]; QSocketNotifier *socketNotifier; QString _lastDir; @@ -69,6 +71,7 @@ public slots: void exportCSV(); void checkNewVersion(); void openFileManager(); + void runScript(const QString &script, const QString &outdir, const QStringList &paths, bool exit); }; #endif // MAINWINDOW_H