Add database tree view

This commit is contained in:
2026-04-08 19:39:14 +02:00
parent 65fca14ac2
commit 6ba9be41ec
6 changed files with 603 additions and 0 deletions
+428
View File
@@ -0,0 +1,428 @@
#include "databasetree.h"
#include "database.h"
#include <QComboBox>
#include <QDialogButtonBox>
#include <QPushButton>
#include <QSettings>
#include <QSqlError>
#include <QVBoxLayout>
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<TreeNode>(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<TreeNode> &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<std::unique_ptr<TreeNode>> _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<TreeNode>();
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<TreeNode*>(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<TreeNode*>(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<TreeNode*>(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<TreeNode*>(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<TreeNode*>(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<TreeNode*>(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();
}
+69
View File
@@ -0,0 +1,69 @@
#ifndef DATABASETREE_H
#define DATABASETREE_H
#include <QAbstractItemModel>
#include <QTreeView>
#include <QSqlQuery>
#include <QFont>
#include <QDialog>
#include <QComboBox>
#include <memory>
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<QComboBox*> _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<TreeNode> _rootNode;
QVector<QSqlQuery> _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
+88
View File
@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Dialog</class>
<widget class="QDialog" name="Dialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>511</width>
<height>487</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="geometry">
<rect>
<x>120</x>
<y>390</y>
<width>341</width>
<height>32</height>
</rect>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
<widget class="QComboBox" name="comboBox">
<property name="geometry">
<rect>
<x>60</x>
<y>30</y>
<width>86</width>
<height>26</height>
</rect>
</property>
</widget>
<widget class="QLineEdit" name="lineEdit">
<property name="geometry">
<rect>
<x>180</x>
<y>30</y>
<width>113</width>
<height>26</height>
</rect>
</property>
</widget>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>Dialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>Dialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>
+12
View File
@@ -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<void (MainWindow::*)(const QString &)>(&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
+3
View File
@@ -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