Compare commits

..

13 Commits

11 changed files with 141 additions and 14 deletions
+2 -2
View File
@@ -17,7 +17,7 @@ if(SANITIZE_ADDRESS_LEAK)
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -fsanitize=address -fsanitize=leak") set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -fsanitize=address -fsanitize=leak")
endif(SANITIZE_ADDRESS_LEAK) endif(SANITIZE_ADDRESS_LEAK)
find_package(Qt6 COMPONENTS Widgets Sql OpenGLWidgets Qml Charts REQUIRED) find_package(Qt6 COMPONENTS Widgets Sql OpenGLWidgets Qml Charts Svg REQUIRED)
find_library(EXIF_LIB exif REQUIRED) find_library(EXIF_LIB exif REQUIRED)
find_library(FITS_LIB cfitsio REQUIRED) find_library(FITS_LIB cfitsio REQUIRED)
find_library(RAW_LIB NAMES raw_r REQUIRED) find_library(RAW_LIB NAMES raw_r REQUIRED)
@@ -105,7 +105,7 @@ if(STELLARSOLVER_INCLUDE AND STELLARSOLVER_LIB)
message(STATUS "Found stellarsolver ${STELLARSOLVER_INCLUDE} ${STELLARSOLVER_LIB}") message(STATUS "Found stellarsolver ${STELLARSOLVER_INCLUDE} ${STELLARSOLVER_LIB}")
endif(STELLARSOLVER_INCLUDE AND STELLARSOLVER_LIB) endif(STELLARSOLVER_INCLUDE AND STELLARSOLVER_LIB)
target_link_libraries(tenmon PRIVATE Qt6::Widgets Qt6::Sql Qt6::OpenGLWidgets Qt6::Qml Qt6::Charts ${EXIF_LIB} ${FITS_LIB} ${RAW_LIB} ${WCS_LIB} ${LCMS2_LIB} XISF) target_link_libraries(tenmon PRIVATE Qt6::Widgets Qt6::Sql Qt6::OpenGLWidgets Qt6::Qml Qt6::Charts Qt6::Svg ${EXIF_LIB} ${FITS_LIB} ${RAW_LIB} ${WCS_LIB} ${LCMS2_LIB} XISF)
if(APPLE) if(APPLE)
target_link_libraries(tenmon PRIVATE Qt6::DBus "-framework CoreFoundation") target_link_libraries(tenmon PRIVATE Qt6::DBus "-framework CoreFoundation")
elseif(UNIX) elseif(UNIX)
+5 -1
View File
@@ -2,7 +2,7 @@ FITS/XISF image viewer with multithreaded image loading
To get all dependencies install these packages To get all dependencies install these packages
sudo apt install qt6-base-dev qt6-declarative-dev libqt6opengl6-dev libraw-dev libexif-dev libcfitsio-dev wcslib-dev cmake libzstd-dev libqt6sql6-sqlite sudo apt install qt6-base-dev qt6-declarative-dev qt6-charts-dev libqt6opengl6-dev libraw-dev libexif-dev libcfitsio-dev wcslib-dev cmake libzstd-dev libqt6sql6-sqlite
on OpenSUSE on OpenSUSE
@@ -26,6 +26,10 @@ Then to build run standard cmake sequence
cmake --build build cmake --build build
./build/tenmon ./build/tenmon
To install it to system run this command as root
cmake --install build
For working plate solving you must have compiled and installed StellarSolver https://github.com/rlancaste/stellarsolver For working plate solving you must have compiled and installed StellarSolver https://github.com/rlancaste/stellarsolver
It is important that you compile StellarSolver with Qt6. By default it use Qt5 but when linked with Qt6 program it will It is important that you compile StellarSolver with Qt6. By default it use Qt5 but when linked with Qt6 program it will
crash. crash.
+9
View File
@@ -47,6 +47,7 @@
</keywords> </keywords>
<url type="homepage">https://nouspiro.space/?page_id=206</url> <url type="homepage">https://nouspiro.space/?page_id=206</url>
<url type="bugtracker">https://github.com/flathub/space.nouspiro.tenmon/issues</url> <url type="bugtracker">https://github.com/flathub/space.nouspiro.tenmon/issues</url>
<url type="vcs-browser">https://gitea.nouspiro.space/nou/tenmon</url>
<screenshots> <screenshots>
<screenshot type="default"> <screenshot type="default">
<caption>Main window with image</caption> <caption>Main window with image</caption>
@@ -59,6 +60,14 @@
</screenshots> </screenshots>
<content_rating type="oars-1.1"/> <content_rating type="oars-1.1"/>
<releases> <releases>
<release version="20260217" date="2026-02-17">
<description>
<ul>
<li>Fix potentional crash</li>
<li>Enable sorting of FITS info</li>
</ul>
</description>
</release>
<release version="20251101" date="2025-11-01"> <release version="20251101" date="2025-11-01">
<description> <description>
<ul> <ul>
+13 -1
View File
@@ -61,8 +61,15 @@ bool Database::init(const QLatin1String &connectionName)
query.exec("CREATE INDEX IF NOT EXISTS maxRa_idx ON fits_files(maxRa)"); query.exec("CREATE INDEX IF NOT EXISTS maxRa_idx ON fits_files(maxRa)");
query.exec("CREATE INDEX IF NOT EXISTS minDec_idx ON fits_files(minDec)"); query.exec("CREATE INDEX IF NOT EXISTS minDec_idx ON fits_files(minDec)");
query.exec("CREATE INDEX IF NOT EXISTS maxDec_idx ON fits_files(maxDec)"); query.exec("CREATE INDEX IF NOT EXISTS maxDec_idx ON fits_files(maxDec)");
version = 1;
} }
else if(version > 1) if(version == 1)
{
query.exec("CREATE INDEX IF NOT EXISTS id_file_key ON fits_headers(id_file, key)");
query.exec("PRAGMA user_version = 2");
version = 2;
}
if(version > 2)
{ {
qDebug() << "Database version is too new"; qDebug() << "Database version is too new";
return false; return false;
@@ -307,6 +314,11 @@ QVector<SkyObject> Database::getObjects(double minRa, double maxRa, double minDe
return objects; return objects;
} }
const QSqlDatabase &Database::db() const
{
return database;
}
bool Database::indexDir2(const QDir &dir, QProgressDialog *progress, QStringList &scannedDirs) bool Database::indexDir2(const QDir &dir, QProgressDialog *progress, QStringList &scannedDirs)
{ {
if(scannedDirs.contains(dir.canonicalPath()))return true; if(scannedDirs.contains(dir.canonicalPath()))return true;
+1
View File
@@ -42,6 +42,7 @@ public:
void reindex(QProgressDialog *progress); void reindex(QProgressDialog *progress);
QStringList getFitsKeywords(); QStringList getFitsKeywords();
QVector<SkyObject> getObjects(double minRa, double maxRa, double minDec, double maxDec); QVector<SkyObject> getObjects(double minRa, double maxRa, double minDec, double maxDec);
const QSqlDatabase& db() const;
protected: protected:
bool indexDir2(const QDir &dir, QProgressDialog *progress, QStringList &scannedDirs); bool indexDir2(const QDir &dir, QProgressDialog *progress, QStringList &scannedDirs);
bool indexFile(const QFileInfo &file); bool indexFile(const QFileInfo &file);
+87 -9
View File
@@ -9,6 +9,9 @@
#include <QMenu> #include <QMenu>
#include <QContextMenuEvent> #include <QContextMenuEvent>
#include <QRegularExpression> #include <QRegularExpression>
#include <QGuiApplication>
#include <QClipboard>
#include <QMimeData>
#include <iostream> #include <iostream>
#include "batchprocessing.h" #include "batchprocessing.h"
@@ -157,32 +160,67 @@ void FITSFileModel::filesUnmarked(const QModelIndexList &indexes)
} }
} }
void FITSFileModel::load()
{
if(!m_loaded)
{
m_loaded = true;
prepareQuery();
}
}
void FITSFileModel::prepareQuery() void FITSFileModel::prepareQuery()
{ {
if(!m_loaded)return;
QString cols; QString cols;
QString join; QString join;
QStringList where; QStringList where;
QString sql = m_columns.size() ? "SELECT f.file," : "SELECT f.file"; QString sql = m_columns.size() ? "SELECT f.file," : "SELECT f.file";
QVariantList bindValues;
QVariantList bindValuesJoin;
for(int i=0; i<m_value.size(); i++) for(int i=0; i<m_value.size(); i++)
{ {
if(m_key[i] == "file") if(m_key[i] == "file")
where.append(QString(" f.file LIKE '%1' ").arg(m_value[i])); {
where.append(" f.file LIKE ? ");
bindValues.append(m_value[i]);
}
else if(m_key[i] == "RA pos") else if(m_key[i] == "RA pos")
where.append(QString(" %1 BETWEEN f.minRa AND f.maxRa ").arg(RA(m_value[i]))); {
where.append(" ? BETWEEN f.minRa AND f.maxRa ");
bindValues.append(RA(m_value[i]));
}
else if(m_key[i] == "DEC pos") else if(m_key[i] == "DEC pos")
where.append(QString(" %1 BETWEEN f.minDec AND f.maxDec ").arg(DEC(m_value[i]))); {
where.append(" ? BETWEEN f.minDec AND f.maxDec ");
bindValues.append(DEC(m_value[i]));
}
else if(m_key[i] == "RA range") else if(m_key[i] == "RA range")
where.append(QString(" crVal1 BETWEEN %1 AND %2 ").arg(RA(m_value[i])).arg(RA(m_limit[i]))); {
where.append(" crVal1 BETWEEN ? AND ? ");
bindValues.append(RA(m_value[i]));
bindValues.append(RA(m_limit[i]));
}
else if(m_key[i] == "DEC range") else if(m_key[i] == "DEC range")
where.append(QString(" crVal2 BETWEEN %1 AND %2 ").arg(DEC(m_value[i])).arg(DEC(m_limit[i]))); {
where.append(" crVal2 BETWEEN ? AND ? ");
bindValues.append(DEC(m_value[i]));
bindValues.append(DEC(m_limit[i]));
}
else else
join += QString(" JOIN fits_headers AS s%1 ON f.id=s%1.id_file AND s%1.key='%2' AND s%1.value LIKE '%3'").arg(i).arg(m_key[i]).arg(m_value[i]); {
join += QString(" JOIN fits_headers AS s%1 ON f.id=s%1.id_file AND s%1.key=? AND s%1.value LIKE ? ").arg(i);
bindValuesJoin.append(m_key[i]);
bindValuesJoin.append(m_value[i]);
}
} }
int i=0; int i=0;
for(auto &column : m_columns) for(auto &column : m_columns)
{ {
cols += QString("GROUP_CONCAT(h%1.value) AS h%1_value,").arg(i); cols += QString("GROUP_CONCAT(h%1.value) AS h%1_value,").arg(i);
join += QString(" LEFT JOIN fits_headers AS h%1 ON f.id=h%1.id_file AND h%1.key='%2'").arg(i).arg(column); join += QString(" LEFT JOIN fits_headers AS h%1 ON f.id=h%1.id_file AND h%1.key=?").arg(i);
bindValuesJoin.append(column);
i++; i++;
} }
cols.chop(1); cols.chop(1);
@@ -191,7 +229,18 @@ void FITSFileModel::prepareQuery()
sql += join; sql += join;
if(!where.isEmpty())sql += " WHERE " + where.join("AND"); if(!where.isEmpty())sql += " WHERE " + where.join("AND");
sql += " GROUP BY f.id" + m_sort; sql += " GROUP BY f.id" + m_sort;
setQuery(sql);
QSqlQuery query(m_database->db());
query.prepare(sql);
for(auto &val : bindValuesJoin)
query.addBindValue(val);
for(auto &val : bindValues)
query.addBindValue(val);
if(!query.exec())
qWarning() << "Failed to exectute query" << query.lastQuery() << bindValuesJoin << bindValues;
setQuery(std::move(query));
setHeaderData(0, Qt::Horizontal, tr("File name")); setHeaderData(0, Qt::Horizontal, tr("File name"));
i = 1; i = 1;
for(auto &column : m_columns) for(auto &column : m_columns)
@@ -217,6 +266,7 @@ void DatabaseTableView::contextMenuEvent(QContextMenuEvent *event)
QAction *unmark = menu.addAction(tr("Unmark")); QAction *unmark = menu.addAction(tr("Unmark"));
QAction *open = menu.addAction(tr("Open")); QAction *open = menu.addAction(tr("Open"));
QAction *openDirAction = menu.addAction(tr("Open file location")); QAction *openDirAction = menu.addAction(tr("Open file location"));
QAction *copyPath = menu.addAction(tr("Copy files"));
QAction *a = menu.exec(event->globalPos()); QAction *a = menu.exec(event->globalPos());
if(a == nullptr) if(a == nullptr)
@@ -232,6 +282,22 @@ void DatabaseTableView::contextMenuEvent(QContextMenuEvent *event)
emit openFile(indexes); emit openFile(indexes);
else if(a == openDirAction) else if(a == openDirAction)
emit openDir(indexes); emit openDir(indexes);
else if(a == copyPath)
{
QStringList paths;
QList<QUrl> urls;
for(auto &index : indexes)
{
QString path = index.siblingAtColumn(0).data().toString();
paths.append(path);
urls.append(QUrl::fromLocalFile(path));
}
QMimeData *data = new QMimeData;
data->setUrls(urls);
data->setText(paths.join('\n'));
QClipboard *clipboard = QGuiApplication::clipboard();
clipboard->setMimeData(data);
}
} }
DataBaseView::DataBaseView(Database *database, QWidget *parent) : QWidget(parent) DataBaseView::DataBaseView(Database *database, QWidget *parent) : QWidget(parent)
@@ -300,12 +366,13 @@ DataBaseView::DataBaseView(Database *database, QWidget *parent) : QWidget(parent
}; };
QStringList fitsKeywords = m_database->getFitsKeywords(); QStringList fitsKeywords = m_database->getFitsKeywords();
QStringList filterKey = settings.value("databaseview/filterKey", QStringList{"file", "file", "file"}).toStringList();
for(int i=0; i<3; i++) for(int i=0; i<3; i++)
{ {
m_filterKeyword[i] = new QComboBox(this); m_filterKeyword[i] = new QComboBox(this);
m_filterKeyword[i]->setMaximumWidth(300); m_filterKeyword[i]->setMaximumWidth(300);
addFilterItems(m_filterKeyword[i], fitsKeywords); addFilterItems(m_filterKeyword[i], fitsKeywords);
m_filterKeyword[i]->setCurrentText(filterKey[i]);
m_search[i] = new QLineEdit(this); m_search[i] = new QLineEdit(this);
m_search[i]->setPlaceholderText(tr("Text to search, you can % as wildcard")); m_search[i]->setPlaceholderText(tr("Text to search, you can % as wildcard"));
@@ -339,8 +406,13 @@ DataBaseView::DataBaseView(Database *database, QWidget *parent) : QWidget(parent
DataBaseView::~DataBaseView() DataBaseView::~DataBaseView()
{ {
QStringList filterKey;
for(int i = 0; i < 3; i++)
filterKey.append(m_filterKeyword[i]->currentText());
QSettings settings; QSettings settings;
settings.setValue("databaseview/header", m_tableView->horizontalHeader()->saveState()); settings.setValue("databaseview/header", m_tableView->horizontalHeader()->saveState());
settings.setValue("databaseview/filterKey", filterKey);
} }
void DataBaseView::selectColumns() void DataBaseView::selectColumns()
@@ -392,6 +464,7 @@ bool DataBaseView::exportCSV(const QString &path)
if(!csv.open(QIODevice::WriteOnly | QIODevice::Text)) if(!csv.open(QIODevice::WriteOnly | QIODevice::Text))
return false; return false;
m_model->load();
QSqlQuery sql(m_model->query().lastQuery()); QSqlQuery sql(m_model->query().lastQuery());
int colCount = m_model->columnCount(); int colCount = m_model->columnCount();
QStringList header; QStringList header;
@@ -420,3 +493,8 @@ bool DataBaseView::exportCSV(const QString &path)
} }
return true; return true;
} }
void DataBaseView::visible(bool visible)
{
if(visible)m_model->load();
}
+3
View File
@@ -30,6 +30,7 @@ class FITSFileModel : public QSqlQueryModel
QStringList m_limit; QStringList m_limit;
QSet<QString> m_markedFiles; QSet<QString> m_markedFiles;
Database *m_database; Database *m_database;
bool m_loaded = false;
public: public:
explicit FITSFileModel(Database *database, QObject *parent = nullptr); explicit FITSFileModel(Database *database, QObject *parent = nullptr);
void sort(int column, Qt::SortOrder order) override; void sort(int column, Qt::SortOrder order) override;
@@ -38,6 +39,7 @@ public:
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
void filesMarked(const QModelIndexList &indexes); void filesMarked(const QModelIndexList &indexes);
void filesUnmarked(const QModelIndexList &indexes); void filesUnmarked(const QModelIndexList &indexes);
void load();
protected: protected:
void prepareQuery(); void prepareQuery();
}; };
@@ -74,6 +76,7 @@ public slots:
void itemActivated(const QModelIndex &index); void itemActivated(const QModelIndex &index);
void applyFilter(); void applyFilter();
bool exportCSV(const QString &path); bool exportCSV(const QString &path);
void visible(bool visible);
signals: signals:
void loadFile(QString file); void loadFile(QString file);
}; };
+2
View File
@@ -11,6 +11,8 @@ ImageInfo::ImageInfo(QWidget *parent) : QTreeWidget(parent)
setIndentation(5); setIndentation(5);
QSettings settings; QSettings settings;
header()->restoreState(settings.value("imageinfo/headerstate").toByteArray()); header()->restoreState(settings.value("imageinfo/headerstate").toByteArray());
setSortingEnabled(true);
header()->setSortIndicatorClearable(true);
} }
ImageInfo::~ImageInfo() ImageInfo::~ImageInfo()
+1
View File
@@ -30,6 +30,7 @@ FITSRecord::FITSRecord(const LibXISF::FITSKeyword &record)
string.chop(1); string.chop(1);
string.remove(0, 1); string.remove(0, 1);
} }
string = string.trimmed();
bool isint; bool isint;
bool isdouble; bool isdouble;
double vald = string.toDouble(&isdouble); double vald = string.toDouble(&isdouble);
+11
View File
@@ -6,6 +6,9 @@
#include <QSettings> #include <QSettings>
#include <stdlib.h> #include <stdlib.h>
#include "../thumbnailer/genthumbnail.h" #include "../thumbnailer/genthumbnail.h"
#ifdef Q_OS_WIN64
#include <windows.h>
#endif
int main(int argc, char *argv[]) int main(int argc, char *argv[])
{ {
@@ -19,6 +22,14 @@ int main(int argc, char *argv[])
bool useGLES = true; bool useGLES = true;
#endif #endif
#ifdef Q_OS_WIN64
if(AttachConsole(ATTACH_PARENT_PROCESS))
{
freopen("CONOUT$", "w", stdout);
freopen("CONOUT$", "w", stderr);
}
#endif
QCommandLineParser cmd; QCommandLineParser cmd;
cmd.addOption({"gl", "Use desktop OpenGL. This is default on x86 and MacOS platform."}); cmd.addOption({"gl", "Use desktop OpenGL. This is default on x86 and MacOS platform."});
cmd.addOption({"gles", "Use OpenGL ES. This is default on ARM platform."}); cmd.addOption({"gles", "Use OpenGL ES. This is default on ARM platform."});
+7 -1
View File
@@ -153,6 +153,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
databaseViewDock->setWidget(m_databaseView); databaseViewDock->setWidget(m_databaseView);
databaseViewDock->setObjectName("databaseViewDock"); databaseViewDock->setObjectName("databaseViewDock");
databaseViewDock->hide(); databaseViewDock->hide();
connect(databaseViewDock, &QDockWidget::visibilityChanged, m_databaseView, &DataBaseView::visible);
addDockWidget(Qt::BottomDockWidgetArea, databaseViewDock); addDockWidget(Qt::BottomDockWidgetArea, databaseViewDock);
QDockWidget *filetreeDock = nullptr; QDockWidget *filetreeDock = nullptr;
@@ -196,7 +197,12 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
#endif #endif
fileMenu->addAction(tr("Copy marked files"), Qt::Key_F5, this, &MainWindow::copyMarked); fileMenu->addAction(tr("Copy marked files"), Qt::Key_F5, this, &MainWindow::copyMarked);
fileMenu->addAction(tr("Move marked files"), Qt::Key_F6, this, &MainWindow::moveMarked); fileMenu->addAction(tr("Move marked files"), Qt::Key_F6, this, &MainWindow::moveMarked);
fileMenu->addAction(tr("Move marked files to trash"), QKeySequence::Delete, this, &MainWindow::deleteMarked); QAction *deleteAction = fileMenu->addAction(tr("Move marked files to trash"), QKeySequence::Delete, this, &MainWindow::deleteMarked);
#ifdef Q_OS_MACOS
deleteAction->setShortcuts(QList<QKeySequence>({Qt::Key_Backspace, QKeySequence::Delete}));
#else
deleteAction->setShortcuts(QKeySequence::Delete);
#endif
fileMenu->addSeparator(); fileMenu->addSeparator();
fileMenu->addAction(tr("Index directory"), this, static_cast<void (MainWindow::*)()>(&MainWindow::indexDir)); fileMenu->addAction(tr("Index directory"), this, static_cast<void (MainWindow::*)()>(&MainWindow::indexDir));
fileMenu->addAction(tr("Reindex files"), this, &MainWindow::reindex); fileMenu->addAction(tr("Reindex files"), this, &MainWindow::reindex);