Compare commits

...

40 Commits

Author SHA1 Message Date
nou 974d482d5b Update metainfo 2026-04-12 18:32:15 +02:00
nou d45bf37e50 Update translation 2026-04-12 18:23:48 +02:00
nou 56a8a0e789 Special handling of COUNT aggregate function 2026-04-12 18:23:39 +02:00
nou 27afb2ea5f Add table view to database tree 2026-04-12 13:41:29 +02:00
nou 28016ada8d Improve logging 2026-04-12 10:26:03 +02:00
nou 885a5b4c6d Add support copy to clipboard for table 2026-04-12 10:20:43 +02:00
nou 63149745ed Handle return value of QFile::open 2026-04-12 10:19:40 +02:00
nou ef8b3d7668 Add scriptarg to cli options 2026-04-10 21:55:36 +02:00
nou 8d2a0a28cc Update tranlations 2026-04-10 21:39:37 +02:00
nou 6ba9be41ec Add database tree view 2026-04-08 19:55:26 +02:00
nou 65fca14ac2 Add running script as CLI option 2026-04-08 19:38:52 +02:00
nou 3818fd4625 Update copyright year 2026-03-30 22:31:51 +02:00
nou 3f88e5fe83 Reopen console on windows when started from cmd.exe 2026-03-26 14:46:23 +01:00
nou 6a537642ab Add copy files to database view 2026-03-22 21:10:04 +01:00
nou b7f1a0abc9 Add id_file_key index to database 2026-03-22 09:55:19 +01:00
nou 33c976d3c9 Remember a selected filter keyword in database view 2026-03-22 09:54:57 +01:00
nou a17001cdf9 Trim whitespace string from XISF 2026-03-21 22:21:35 +01:00
nou 305c1d1f55 Deffered SQL query when database is visible 2026-03-21 20:33:47 +01:00
nou 95808b094d Fix buidling query 2026-03-21 20:31:44 +01:00
nou 2b56af27fe Add explicit link to Svg module to solve some issues with SVG icon 2026-03-15 17:47:51 +01:00
nou 8edf746827 Use bindvalue in DatabaseTableView 2026-03-15 17:47:24 +01:00
nou 729a330e6c Add backspace as move to trash shortcut for MacOS 2026-03-15 17:45:01 +01:00
nou 1ac5a4e42a Update metainfo 2026-02-16 22:52:28 +01:00
nou 83d212aa91 Enable sorting of FITS header 2026-02-16 22:29:25 +01:00
nou bd24fba407 Update README 2026-02-11 21:26:33 +01:00
nou 3448f62f31 Try to fix crash in ImageRingList 2026-01-19 20:57:13 +01:00
nou 567e66acb5 Update libXISF 2025-11-02 23:17:10 +01:00
nou 9e79133464 Fix compile error 2025-11-01 12:11:53 +01:00
nou e08107aa13 Improve Save as 2025-11-01 12:06:24 +01:00
nou 6eda2c4e48 Remove some code 2025-10-20 23:48:47 +02:00
nou b16ae3a9ee Add language setting 2025-10-20 21:29:59 +02:00
nou 56bba27ae3 Give save filter only formats that are supported 2025-10-20 00:23:11 +02:00
nou 1070dc32c1 Update metainfo 2025-09-15 15:55:00 +02:00
nou f61cf12f0a Update help files 2025-09-15 15:49:30 +02:00
nou 530b0c62c3 Open parent directory if it doesn't exist 2025-09-15 15:49:16 +02:00
nou 7e95440dd6 Update translations 2025-09-15 10:58:29 +02:00
nou 03492972cb Include all types in completion 2025-09-14 20:44:36 +02:00
nou 9cca183677 Working copy/move operation 2025-09-14 20:44:07 +02:00
nou afd059b36b Add some margins when retriving objects 2025-09-14 13:43:20 +02:00
nou 1b9f218acb Console line with simple auto completion 2025-08-17 17:39:01 +02:00
50 changed files with 4117 additions and 1957 deletions
+5 -2
View File
@@ -17,7 +17,7 @@ if(SANITIZE_ADDRESS_LEAK)
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -fsanitize=address -fsanitize=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(FITS_LIB cfitsio REQUIRED)
find_library(RAW_LIB NAMES raw_r REQUIRED)
@@ -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})
@@ -105,7 +108,7 @@ if(STELLARSOLVER_INCLUDE AND STELLARSOLVER_LIB)
message(STATUS "Found stellarsolver ${STELLARSOLVER_INCLUDE} ${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)
target_link_libraries(tenmon PRIVATE Qt6::DBus "-framework CoreFoundation")
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
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
@@ -26,6 +26,10 @@ Then to build run standard cmake sequence
cmake --build build
./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
It is important that you compile StellarSolver with Qt6. By default it use Qt5 but when linked with Qt6 program it will
crash.
+43 -3
View File
@@ -61,7 +61,7 @@ This image should be 256 pixel wide. Each row of image will be used as separate
<li>mid point - defines the value to be stretched to 50% intensity</li>
<li>white point - all pixels with higher value (brighter) than this will be clipped white</li>
</ul>
<p>Following the slider are 7 buttons for automatic stretching:</p>
<p>Following the slider are 8 buttons to control image display:</p>
<ul>
<li><i>Linked channels</i> toggle stretching each RGB channel individually.</li>
<li><i>Auto Stretch</i> automatically apply black and mid points to render the image with optimal brightness.</li>
@@ -70,6 +70,7 @@ This image should be 256 pixel wide. Each row of image will be used as separate
<li><i>False colors</i> show black and white in false colour palette.</li>
<li><i>Debayer CFA</i> Demosaicing black and white image from CFA sensor to color one.</li>
<li><i>Apply Auto stretch on load</i> toggle automatically applying Autostretch for each image when loaded.</li>
<li><i>Draw equatorial grid</i> toggle drawing equatorial coordinate grid and catalogue objects. Needs that file have WCS data.</li>
</ul>
<h3>Marking images</h3>
@@ -121,6 +122,13 @@ Pressing Enter or clicking on <i>Filter</i> button will filter out database reco
This example filters for files where: "Bias" is in the file name, the OBJECT property is "M_42" (where the underscore can be any single character), and the DATE property begins with "2022".
</p>
<h3>Database tree</h3>
<p>This is another view that show indexed database as tree. You can add or remove tree grouping that construct a tree structure from FITS keywords. Each level of tree
will be based on this grouping. You can specify one keywords multiple times. When adding a grouping you can also specify aggregation function that is applied to last level.
SUM will add up all numbers in that group usefull for example for EXPTIME. COUNT will show how many files are in that group. AVG will show average value, MIN,MAX,MEDIAN will calculate
minimum, maximum and media value in that group. So if you select CCD-TEMP as last with AVG it will calculate average temperature. Double click on file in tree view will open
that file.</p>
<h3>Plate Solving</h3>
<p>This module can plate solve images and update FITS header with solution for FITS and XISF images.
<b>Profile</b> this set various parameters that affect star extraction and solving.
@@ -134,12 +142,43 @@ solver.
<p>Then finally there are various action button. Settings button show dialog where you can set path to existing index files or auto download some.
Extract button will just extract stars from image and it will show their count, HFR and eccentricity. This action doesn't need index files.
Solve button will try to find coordinates of images. Abort button will stop extraction or solving. Update FITS header will update FITS fits keywords
with found solution.
with found solution.</p>
<p>In settings dialog you can set path to index files which is by default custom internal one. It also try to locate commonly used path from other
programs like KStars for astrometry.net index files.
</p>
<h3>File Manager</h3>
<p>
This is simple double panel file manager. It can show columns with selected FITS keywords. Each panel have tabs where it easily switch between
multiple directories. You can copy or move files and directories either inside one panel or between two panels by selecting and then dragging.
By default files are copies. To move then press Shift key before start dragging. Double click on file will open it in main window if it is image
or other file it will open default program that is associated with it this file type. You can also drag file to other programs like from default
file explorer programs that are in system or from file explorer to this program.
</p>
<p>
In menu you can select which FITS keywords will be showed. Temporarilly disable loading FITS header or copy file paths of selected files as text.
</p>
<h3>Command line options</h3>
<p>
Tenmon can be executed from command line. It support these command line options.
<ul>
<li><i>--thumb, --thumbnail &lt;path&gt;</i> Generate thumbnail and save it to path. It generate it from first file provided as argument.</li>
<li><i>-s, --size <size></i> size of generated thumbnail. Aspect ratio of input image is preserved.</li>
<li><i>--script &lt;script&gt;</i> execute a script from file path same manner as executed from GUI.</li>
<li><i>--scriptarg &lt;arg&gt>;</i> pass this string as variable scriptarg to a running script.</li>
<li><i>--outdir &lt;dir&gt;</i> output dir for script execution. By default current working directory is used.</li>
<li><i>--noexit</i> by default application exit when execution of script specified with --script ends. This prefent that.</li>
<li><i>-h, --help</i> show help end exit.</li>
Any other arguments are taken as input paths. If only one file path is specified then that image is open and image list is populated by directory
containing that image. If directory is specified then it is same as selecting that directory in "Open directory recusivelly". If multiple files are
specified then image list will contain just these speicified images.
When exuecting script with --script then these paths are used as input files and directories as in "Batch processing"
</ul>
</p>
<h3>Batch processing</h3>
<p>This module allow to write scripts in JavaScript that process image files. Batch Processing window consist from three main parts. On top is list of input files and directories.
You can add directories or individual files to this list. Directories are scanned recursively to find all files even non image files. This list of files is then passed to script in array named <b>files</b>.
@@ -159,7 +198,8 @@ this output directory is ignored.</p>
<p>Bellow that is list of scripts. These scripts are located in application data directory. Select script which you want to run by clicking on it. Clicking on <i>Open scripts</i> will open directory with these scripts where you create new and edit them.
Location of this directory is on Windows: "C:/Users/&lt;USER&gt;/AppData/Roaming/nou/Tenmon/scripts" Linux: "~/.local/share/nou/Tenmon/scripts" MacOS: "~/Library/Application Support/nou/Tenmon/scripts"</p>
<p>Next is Log windows that contain any messages that come from scripts. Mainly calls to <code>core.log()</code> At bottom there buttons that can start or stop execution of selected scripts.</p>
<p>Next is Log windows that contain any messages that come from scripts. Mainly calls to <code>core.log()</code> At bottom there is console that enable run simple script commands and
buttons that can start or stop execution of selected scripts.</p>
<h4>core</h4>
<p>There is global object called <b>core</b> that have these methods.</p>
+57
View File
@@ -102,6 +102,63 @@ En appuyant sur la touche Enter ou en cliquant sur le bouton <i>Filtre</i>, les
Cet exemple filtre les fichiers où : "Bias" figure dans le nom de fichier, la propriété OBJECT est "M_42" (où le trait de soulignement peut être n'importe quel caractère) et la propriété DATE commence par "2022".
</p>
<h3>Database tree</h3>
<p>This is another view that show indexed database as tree. You can add or remove tree grouping that construct a tree structure from FITS keywords. Each level of tree
will be based on this grouping. You can specify one keywords multiple times. When adding a grouping you can also specify aggregation function that is applied to last level.
SUM will add up all numbers in that group usefull for example for EXPTIME. COUNT will show how many files are in that group. AVG will show average value, MIN,MAX,MEDIAN will calculate
minimum, maximum and media value in that group. So if you select CCD-TEMP as last with AVG it will calculate average temperature. Double click on file in tree view will open
that file.</p>
<h3>Plate Solving</h3>
<p>This module can plate solve images and update FITS header with solution for FITS and XISF images.
<b>Profile</b> this set various parameters that affect star extraction and solving.
<b>Starting point</b> program will try to automatically determine optimal starting point which helps to speed up solving.
You can leave one or both unchecked then it will attempt to do blind solving. If the position or scale is wrong it can actually
fail to solve.
<b>Solution</b> this section contain resulting solution like RA,DEC coordinates center of image, image field of view, orientation as degrees E of N,
image scale in arcseconds per pixel, number of stars extracted and HFR fitting and eccentricity. Then there is log window for debug information from
solver.
</p>
<p>Then finally there are various action button. Settings button show dialog where you can set path to existing index files or auto download some.
Extract button will just extract stars from image and it will show their count, HFR and eccentricity. This action doesn't need index files.
Solve button will try to find coordinates of images. Abort button will stop extraction or solving. Update FITS header will update FITS fits keywords
with found solution.</p>
<p>In settings dialog you can set path to index files which is by default custom internal one. It also try to locate commonly used path from other
programs like KStars for astrometry.net index files.
</p>
<h3>File Manager</h3>
<p>
This is simple double panel file manager. It can show columns with selected FITS keywords. Each panel have tabs where it easily switch between
multiple directories. You can copy or move files and directories either inside one panel or between two panels by selecting and then dragging.
By default files are copies. To move then press Shift key before start dragging. Double click on file will open it in main window if it is image
or other file it will open default program that is associated with it this file type. You can also drag file to other programs like from default
file explorer programs that are in system or from file explorer to this program.
</p>
<p>
In menu you can select which FITS keywords will be showed. Temporarilly disable loading FITS header or copy file paths of selected files as text.
</p>
<h3>Command line options</h3>
<p>
Tenmon can be executed from command line. It support these command line options.
<ul>
<li><i>--thumb, --thumbnail &lt;path&gt;</i> Generate thumbnail and save it to path. It generate it from first file provided as argument.</li>
<li><i>-s, --size <size></i> size of generated thumbnail. Aspect ratio of input image is preserved.</li>
<li><i>--script &lt;script&gt;</i> execute a script from file path same manner as executed from GUI.</li>
<li><i>--scriptarg &lt;arg&gt>;</i> pass this string as variable scriptarg to a running script.</li>
<li><i>--outdir &lt;dir&gt;</i> output dir for script execution. By default current working directory is used.</li>
<li><i>--noexit</i> by default application exit when execution of script specified with --script ends. This prefent that.</li>
<li><i>-h, --help</i> show help end exit.</li>
Any other arguments are taken as input paths. If only one file path is specified then that image is open and image list is populated by directory
containing that image. If directory is specified then it is same as selecting that directory in "Open directory recusivelly". If multiple files are
specified then image list will contain just these speicified images.
When exuecting script with --script then these paths are used as input files and directories as in "Batch processing"
</ul>
</p>
<h3>Traitement par lot</h3>
Ce module permet d'écrire des scripts en JavaScript qui traitent des fichiers images. La fenêtre de traitement par lots se compose de trois parties principales. En haut se trouve la liste des fichiers et répertoires d'entrée.
+71
View File
@@ -51,6 +51,17 @@ na ktorej sa dajú nastaviť tri body.
<li>stredný bod - pixeli s touto hodnotou budú zobrazené ako 50% šedá</li>
<li>biely bod - pixeli nad touto hodnotou budú zobrazené ako biele</li>
</ul>
<p>Nasleduje 8 tlačidiel pre nastavenie zobrazenie obrázka:</p>
<ul>
<li><i>Prepojené kanály</i> prepína medzi nataihnutím jasových urovní pre každý RGB kanál zvlášť alebo jednotné pre všetky kanály.</li>
<li><i>Automatické natiahnutie</i> automaticky nastavý čierny, šedý a biely bod pre optimálne zobrazenie..</li>
<li><i>Resetuj funkciu prevodu na obrazovku</i> nastavý hodnoty čierneho, šedého a bieleho bodu na východzie hodnoty.</li>
<li><i>Invertuj farby</i> invertuje zobrazené farby a zobrazý obrázok ako negatív.</li>
<li><i>Falošné farby</i> pre zobrazenie čiernobielych obrázkov sa použije farebná paleta.</li>
<li><i>Preved CFA na farbu</i> Demosaicing black and white image from CFA sensor to color one.</li>
<li><i>Aplikuj automatické natiahnutie pri načítaní</i> pri zapnutí </li>
<li><i>Vykresli equatoriálnu mriežku</i> zapína zobrazenie equatoriálnej mriežky. Je poitrebné aby súbor obsahoval WCS dáta.</li>
</ul>
<p>Prvé tlačidlo prepína prepojenie nastavenia čierneho, stretdného a bieleho bodu. Po prepnutí sa dá každý farebný kanál nastaviť
samostatne.
Nasleduje tlačidlo ktoré nastaví hodnoty čierneho a stredného bodu tak aby bol obrázok zobrazený optimálnym jasom.
@@ -94,6 +105,60 @@ V nasledovnom príklade sa vyhľadajú súbory ktoré majú v mene súboru "Bias
zástupný znak za hocijaký reťazec znakov aj žiadny. Znak _ je tiež zástupný znak zastupujúci práve jeden znak.
Bez použitia zástupných znakov sa vyhľadá iba presný výskyt.</p>
<h3>Database tree</h3>
<p>This is another view that show indexed database as tree. You can add or remove tree grouping that construct a tree structure from FITS keywords. Each level of tree
will be based on this grouping. You can specify one keywords multiple times. When adding a grouping you can also specify aggregation function that is applied to last level.
SUM will add up all numbers in that group usefull for example for EXPTIME. COUNT will show how many files are in that group. AVG will show average value, MIN,MAX,MEDIAN will calculate
minimum, maximum and media value in that group. So if you select CCD-TEMP as last with AVG it will calculate average temperature. Double click on file in tree view will open
that file.</p>
<h3>Plate Solving</h3>
<p>Tento modul umožnuje vyriešiť obrázok a určiť RA, DEC koordináty a aktualizovať FITS a XISF súbory s WCS dátami.
<b>Profil</b> toto nastavuje rôzne parametre ovplivňujúce extrahovanie hviezd a hľadanie koordinátov.
<b>Štartovný bod</b> program sa pokúsi automaticky určiť bod začiatku hľadania koordinátov pre zrýchlenie hľadania.
Možete nechať jednu alebo obydve voľbi nezaškrtnuté kedy sa bude hľadať riešenie naslepo. Ak je pozícia alebo škála nesprávna hľadanie môže zlyhať.
<b>Riešenie</b> táto časť obsahuje výsledné riešenie hľadania ako sú RA,DEC koordináty stredu obrázku, veľkosť zorného poľa, uhol natočenia v stupňoch,
škála obrázku v arcsekundách na pixel, počet nájdených hviezd a veľkosť hviezd HFR a excentricita. Potom je tu ešte okno so záznamom z riešenia.
</p>
<p>Pod tým je tlačidlo pre nastavenie cesty k indexovým súborom a ich stiahnutie. Je možné buď stiahnuť indexové súbory alebo použiť už existujúce súbory.
Tlačidlo Extrahovať nájde hviezdy a zobrazý ich počet veľkosť HFR a excentricitu. Na toto nie sú potrebné indexové súbory.
Tlačidlo Vyriešiť sa pokúsi nájsť koordináty obrázka. Zrušiť preruší hľadanie riešenia. Aktualizovať FITS hlavičku zapíše najdené riešenie do súboru.</p>
<p>In settings dialog you can set path to index files which is by default custom internal one. It also try to locate commonly used path from other
programs like KStars for astrometry.net index files.
</p>
<h3>File Manager</h3>
<p>
This is simple double panel file manager. It can show columns with selected FITS keywords. Each panel have tabs where it easily switch between
multiple directories. You can copy or move files and directories either inside one panel or between two panels by selecting and then dragging.
By default files are copies. To move then press Shift key before start dragging. Double click on file will open it in main window if it is image
or other file it will open default program that is associated with it this file type. You can also drag file to other programs like from default
file explorer programs that are in system or from file explorer to this program.
</p>
<p>
In menu you can select which FITS keywords will be showed. Temporarilly disable loading FITS header or copy file paths of selected files as text.
</p>
<h3>Command line options</h3>
<p>
Tenmon can be executed from command line. It support these command line options.
<ul>
<li><i>--thumb, --thumbnail &lt;path&gt;</i> Generate thumbnail and save it to path. It generate it from first file provided as argument.</li>
<li><i>-s, --size <size></i> size of generated thumbnail. Aspect ratio of input image is preserved.</li>
<li><i>--script &lt;script&gt;</i> execute a script from file path same manner as executed from GUI.</li>
<li><i>--scriptarg &lt;arg&gt>;</i> pass this string as variable scriptarg to a running script.</li>
<li><i>--outdir &lt;dir&gt;</i> output dir for script execution. By default current working directory is used.</li>
<li><i>--noexit</i> by default application exit when execution of script specified with --script ends. This prefent that.</li>
<li><i>-h, --help</i> show help end exit.</li>
Any other arguments are taken as input paths. If only one file path is specified then that image is open and image list is populated by directory
containing that image. If directory is specified then it is same as selecting that directory in "Open directory recusivelly". If multiple files are
specified then image list will contain just these speicified images.
When exuecting script with --script then these paths are used as input files and directories as in "Batch processing"
</ul>
</p>
<h3>Hromadné spracovanie</h3>
Tento modul umožnuje písanie skriptov v JavaScripte ktoré spracujú súbory obrázkov. Okno Hromadného spracovanie pozostáva z troch častí. Navrchu je zoznam vstupných súborov a adresárov.
@@ -109,6 +174,12 @@ V skripte potom cez toto pole iteruje nasledovne.
}
</pre>
<p>Pod týmto zoznamom je výstupný adresár. Všetky relatívne cesty predané do metod ako sú copy(), move() alebo convert() budú relatívne voči tomuto adresáru.
Ak je ako argument použitá absolútna cesta tak je tento vystupný adresár ignorovaný.</p>
<p>Nasleduje logovacie okno kde sú zapisováné všetký výpisy z behu scriptu. Hlavne volania z <code>core.log()</code> Na spodu je konzola kde je možné vkladať jednoduché príkazy a nakoniec ešte tlačítka
ktoré spúštať alebo zastavovať vybraný skript.</p>
<h4>core</h4>
V skripte je dostupný globálny objekt nazvaný <b>core</b> ktorý má nasledovné metódy.
<ul>
+1 -1
View File
@@ -2,7 +2,7 @@
<td style="padding-right:10px"><img src=":/space.nouspiro.tenmon.png"></td>
<td><h3>Tenmon</h3>
Tenmon is FITS/XISF image viewer and converter. It also index FITS keywords.<br>
v@GITVERSION@ Copyright © 2022 Dušan Poizl<br><br>
v@GITVERSION@ Copyright © 2026 Dušan Poizl<br><br>
This program is free software: you can redistribute it and/or modify<br>
it under the terms of the GNU General Public License as published by<br>
+1 -1
Submodule libXISF updated: c6581e1122...7b70b6a081
+40
View File
@@ -33,6 +33,7 @@
<li>Color space aware</li>
<li>Histogram</li>
<li>Scripting</li>
<li>Plate solving</li>
</ul>
</description>
<categories>
@@ -46,6 +47,7 @@
</keywords>
<url type="homepage">https://nouspiro.space/?page_id=206</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>
<screenshot type="default">
<caption>Main window with image</caption>
@@ -58,6 +60,44 @@
</screenshots>
<content_rating type="oars-1.1"/>
<releases>
<release version="20260412" date="2026-04-12">
<description>
<ul>
<li>Add database tree to show database entries grouped in tree structure</li>
<li>Add database summary table. This can show total exposure time for OBJECT and FILTER for example</li>
<li>Copy files to clipboard from database view</li>
<li>Copy to clipboard text from database tables so they can be pasted to Excel</li>
<li>Add CLI option to run scripts and generate thumbnails</li>
</ul>
</description>
</release>
<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">
<description>
<ul>
<li>Better image Save as</li>
<li>Fix xisf file corruption when platesolving</li>
<li>Add selecting language</li>
</ul>
</description>
</release>
<release version="20250915" date="2025-09-15">
<description>
<ul>
<li>Draw equatorial grid and objects overlay</li>
<li>File Manager</li>
<li>Support for PCL:AstrometricSolution</li>
<li>Script console</li>
</ul>
</description>
</release>
<release version="20250429" date="2025-04-29">
<description>
<ul>
+9 -7
View File
@@ -15,11 +15,13 @@ About::About(QWidget *parent) : QDialog(parent)
QLabel *label = new QLabel(this);
QFile tenmonText(":/about/tenmon");
tenmonText.open(QIODevice::ReadOnly);
QByteArray text = tenmonText.readAll();
text.replace("@GITVERSION@", GITVERSION);
label->setText(text);
label->setOpenExternalLinks(true);
if(tenmonText.open(QIODevice::ReadOnly))
{
QByteArray text = tenmonText.readAll();
text.replace("@GITVERSION@", GITVERSION);
label->setText(text);
label->setOpenExternalLinks(true);
}
QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok);
connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
@@ -41,8 +43,8 @@ HelpDialog::HelpDialog(QWidget *parent) : QDialog(parent)
layout->addWidget(helpText);
QFile tenmonText(":/help");
tenmonText.open(QIODevice::ReadOnly);
helpText->setHtml(tenmonText.readAll());
if(tenmonText.open(QIODevice::ReadOnly))
helpText->setHtml(tenmonText.readAll());
}
QString getVersion()
+134 -8
View File
@@ -13,11 +13,11 @@
#include <QChart>
#include <QChartView>
#include <QLineSeries>
#include <QCompleter>
#include "scriptengine.h"
#include "chartgraph.h"
#ifdef Q_OS_LINUX
#include <QCloseEvent>
#include <QDBusConnection>
#include <QDBusMessage>
#endif
@@ -98,18 +98,43 @@ BatchProcessing::BatchProcessing(Database *database, QWidget *parent) : QDialog(
qWarning() << "Failed to get app data location";
}
connect(_ui->addFilesButton, &QPushButton::released, this, &BatchProcessing::addFiles);
connect(_ui->addDirButton, &QPushButton::released, this, &BatchProcessing::addDir);
connect(_ui->addFilesButton, &QPushButton::released, this, static_cast<void (BatchProcessing::*)()>(&BatchProcessing::addFiles));
connect(_ui->addDirButton, &QPushButton::released, this, static_cast<void (BatchProcessing::*)()>(&BatchProcessing::addDir));
connect(_ui->addMarkedButton, &QPushButton::released, this, &BatchProcessing::addMarked);
connect(_ui->removeButton, &QPushButton::released, this, &BatchProcessing::removePath);
connect(_ui->removeAllButton, &QPushButton::released, this, &BatchProcessing::removeAllPaths);
connect(_ui->startButton, &QPushButton::released, this, &BatchProcessing::runScript);
connect(_ui->startButton, &QPushButton::released, this, static_cast<void (BatchProcessing::*)()>(&BatchProcessing::runScript));
connect(_ui->stopButton, &QPushButton::released, this, &BatchProcessing::stopScript);
connect(_ui->browseButton, &QPushButton::released, this, &BatchProcessing::browse);
connect(_ui->openScriptsButton, &QPushButton::released, this, &BatchProcessing::openScriptDir);
_textColor = _ui->log->palette().text().color();
_engine = new Script::ScriptEngine(_database, this);
connect(_engine, &Script::ScriptEngine::newMessage, this, &BatchProcessing::newMessage);
_completerModel = new QStringListModel(this);
_completer = new QCompleter(_completerModel, this);
_ui->consoleLineEdit->setCompleter(_completer);
connect(_ui->executeButton, &QPushButton::clicked, _ui->consoleLineEdit, &QLineEdit::returnPressed);
connect(_ui->consoleLineEdit, &QLineEdit::returnPressed, [this](){
if(!_completer->popup()->isVisible())
{
QString program = _ui->consoleLineEdit->text();
QJSValue val = _engine->eval(program);
_ui->consoleLineEdit->addLine();
//qDebug() << val.toString();
}
});
connect(_ui->consoleLineEdit, &QLineEdit::textEdited, [this](const QString &text){
QStringList comp = _engine->complete(text);
//qDebug() << comp;
_completerModel->setStringList(comp);
});
_ui->addFilesButton->setAutoDefault(false);
QSettings settings;
_ui->outputPath->setText(settings.value("batchprocessing/outputpath", QStandardPaths::standardLocations(QStandardPaths::PicturesLocation).first()).toString());
}
@@ -122,6 +147,17 @@ BatchProcessing::~BatchProcessing()
delete _ui;
}
void BatchProcessing::setOutputDir(const QString &output)
{
_ui->outputPath->setText(output);
}
void BatchProcessing::setPaths(const QStringList &paths)
{
_ui->pathsList->addItems(paths);
refreshPaths();
}
void BatchProcessing::closeEvent(QCloseEvent *event)
{
if(_engineThread)
@@ -143,6 +179,15 @@ void BatchProcessing::closeEvent(QCloseEvent *event)
}
}
void BatchProcessing::refreshPaths()
{
QStringList paths;
for(int i=0; i<_ui->pathsList->count(); i++)
paths.append(_ui->pathsList->item(i)->text());
_paths = scanDirectories(paths);
_engine->setParams("", _paths, _ui->outputPath->text(), QString());
}
void BatchProcessing::addFiles()
{
QSettings settings;
@@ -152,6 +197,7 @@ void BatchProcessing::addFiles()
_ui->pathsList->addItems(files);
settings.setValue("batchprocessing/inputpath", QFileInfo(files.first()).absolutePath());
}
refreshPaths();
}
void BatchProcessing::addDir()
@@ -163,6 +209,7 @@ void BatchProcessing::addDir()
_ui->pathsList->addItem(dir);
settings.setValue("batchprocessing/inputpath", dir);
}
refreshPaths();
}
void BatchProcessing::addMarked()
@@ -174,17 +221,20 @@ void BatchProcessing::addMarked()
if(info.exists() && info.isReadable())
_ui->pathsList->addItem(file);
};
refreshPaths();
}
void BatchProcessing::removePath()
{
for(auto &item : _ui->pathsList->selectedItems())
delete item;
refreshPaths();
}
void BatchProcessing::removeAllPaths()
{
_ui->pathsList->clear();
refreshPaths();
}
void BatchProcessing::browse()
@@ -208,9 +258,6 @@ void BatchProcessing::runScript()
_engineThread = new Script::ScriptEngineThread(_database, this);
connect(_engineThread, &Script::ScriptEngineThread::newMessage, this, &BatchProcessing::newMessage);
connect(_engineThread, &Script::ScriptEngineThread::finished, this, &BatchProcessing::scriptFinished);
QStringList paths;
for(int i=0; i<_ui->pathsList->count(); i++)
paths.append(_ui->pathsList->item(i)->text());
QFileInfo outDir(_ui->outputPath->text());
if(outDir.exists() && outDir.isWritable())
@@ -221,7 +268,7 @@ void BatchProcessing::runScript()
else
script = ":/scripts/" + script;
_engineThread->setParams(script, scanDirectories(paths), _ui->outputPath->text());
_engineThread->setParams(script, _paths, _ui->outputPath->text(), QString());
_engineThread->start();
_ui->startButton->setEnabled(false);
_ui->stopButton->setEnabled(true);
@@ -229,6 +276,35 @@ void BatchProcessing::runScript()
else
{
QMessageBox::warning(this, tr("Invalid output directory"), tr("Output directory path doesn't exist or is not writable"));
delete _engineThread;
_engineThread = nullptr;
}
}
}
void BatchProcessing::runScript(const QString &script, const QString &arg, bool exit)
{
_ui->log->clear();
{
_engineThread = new Script::ScriptEngineThread(_database, this);
connect(_engineThread, &Script::ScriptEngineThread::newMessage, this, &BatchProcessing::newMessage);
connect(_engineThread, &Script::ScriptEngineThread::newMessage, this, &BatchProcessing::newMessageCli);
connect(_engineThread, &Script::ScriptEngineThread::finished, this, &BatchProcessing::scriptFinished);
if(exit)connect(_engineThread, &Script::ScriptEngineThread::finished, this, &BatchProcessing::accept);
QFileInfo outDir(_ui->outputPath->text());
if(outDir.exists() && outDir.isWritable())
{
_engineThread->setParams(script, _paths, _ui->outputPath->text(), arg);
_engineThread->start();
_ui->startButton->setEnabled(false);
_ui->stopButton->setEnabled(true);
}
else
{
QMessageBox::warning(this, tr("Invalid output directory"), tr("Output directory path doesn't exist or is not writable"));
delete _engineThread;
_engineThread = nullptr;
}
}
}
@@ -256,6 +332,14 @@ void BatchProcessing::newMessage(const QString &message, bool error)
_ui->log->append(message);
}
void BatchProcessing::newMessageCli(const QString &message, bool error)
{
if(error)
qWarning() << message;
else
qDebug() << message;
}
QJSValue BatchProcessing::getString(const QString &label, const QString &text)
{
bool ok = false;
@@ -329,6 +413,48 @@ void BatchProcessing::plot(const QVariant &graph)
chart->plot(graph);
}
ConsoleLine::ConsoleLine(QWidget *parent) : QLineEdit(parent)
{
}
void ConsoleLine::addLine()
{
QString line = text();
clear();
if(_history.size() && _history.last() == line)return;
_history.append(line);
if(_history.size() > 100)_history.removeFirst();
_currentLine = _history.size();
}
void ConsoleLine::keyReleaseEvent(QKeyEvent *event)
{
if(event->key() == Qt::Key_Up)
{
_currentLine--;
if(_currentLine < 0)
{
_currentLine = -1;
clear();
return;
}
setText(_history.at(_currentLine));
}
else if(event->key() == Qt::Key_Down)
{
_currentLine++;
if(_currentLine >= _history.size())
{
_currentLine = _history.size();
clear();
return;
}
setText(_history.at(_currentLine));
}
else QLineEdit::keyReleaseEvent(event);
}
void openDir(const QString &path)
{
#ifdef Q_OS_LINUX
+24
View File
@@ -3,6 +3,9 @@
#include <QDialog>
#include <QFileSystemWatcher>
#include <QStringListModel>
#include <QCompleter>
#include <QLineEdit>
#include "scriptengine.h"
namespace Ui { class BatchProcessing; }
@@ -16,15 +19,22 @@ class BatchProcessing : public QDialog
QString _scriptBasePath;
QFileSystemWatcher _fileWatcher;
Script::ScriptEngineThread *_engineThread = nullptr;
Script::ScriptEngine *_engine = nullptr;
QColor _textColor;
Database *_database;
QStringListModel *_completerModel = nullptr;
QCompleter *_completer = nullptr;
QList<QPair<QString, QString>> _paths;
private slots:
void scanScriptDir();
public:
explicit BatchProcessing(Database *database, QWidget *parent = nullptr);
~BatchProcessing();
void setOutputDir(const QString &output);
void setPaths(const QStringList &paths);
protected:
void closeEvent(QCloseEvent *event);
void refreshPaths();
public slots:
void addFiles();
void addDir();
@@ -34,9 +44,11 @@ public slots:
void browse();
void openScriptDir();
void runScript();
void runScript(const QString &script, const QString &arg, bool exit);
void stopScript();
void scriptFinished();
void newMessage(const QString &message, bool error);
void newMessageCli(const QString &message, bool error);
QJSValue getString(const QString &label, const QString &text);
QJSValue getInt(const QString &label, int value);
@@ -47,6 +59,18 @@ public slots:
void plot(const QVariant &graph);
};
class ConsoleLine : public QLineEdit
{
Q_OBJECT
public:
explicit ConsoleLine(QWidget *parent = nullptr);
void addLine();
void keyReleaseEvent(QKeyEvent *event) override;
private:
int _currentLine = 0;
QStringList _history;
};
void openDir(const QString &path);
#endif // BATCHPROCESSING_H
+63 -33
View File
@@ -43,6 +43,9 @@
<property name="text">
<string>Add files</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
<item>
@@ -50,6 +53,9 @@
<property name="text">
<string>Add directories</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
<item>
@@ -57,6 +63,9 @@
<property name="text">
<string>Add marked</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
<item>
@@ -64,6 +73,9 @@
<property name="text">
<string>Remove</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
<item>
@@ -71,6 +83,9 @@
<property name="text">
<string>Remove all</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
@@ -98,6 +113,9 @@
<property name="text">
<string>Browse</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
@@ -129,6 +147,9 @@
<property name="text">
<string>Open scripts</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
@@ -171,23 +192,30 @@
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<widget class="ConsoleLine" name="consoleLineEdit">
<property name="placeholderText">
<string>Console</string>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</widget>
</item>
<item>
<widget class="QPushButton" name="executeButton">
<property name="text">
<string>Execute</string>
</property>
</spacer>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="startButton">
<property name="text">
<string>Start script</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
<item>
@@ -198,12 +226,8 @@
<property name="text">
<string>Stop script</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="closeButton">
<property name="text">
<string>Close</string>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
@@ -211,23 +235,29 @@
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>ConsoleLine</class>
<extends>QLineEdit</extends>
<header>batchprocessing.h</header>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>pathsList</tabstop>
<tabstop>addFilesButton</tabstop>
<tabstop>addDirButton</tabstop>
<tabstop>addMarkedButton</tabstop>
<tabstop>removeButton</tabstop>
<tabstop>removeAllButton</tabstop>
<tabstop>browseButton</tabstop>
<tabstop>openScriptsButton</tabstop>
<tabstop>scriptsList</tabstop>
<tabstop>consoleLineEdit</tabstop>
<tabstop>startButton</tabstop>
<tabstop>stopButton</tabstop>
<tabstop>outputPath</tabstop>
<tabstop>log</tabstop>
</tabstops>
<resources/>
<connections>
<connection>
<sender>closeButton</sender>
<signal>released()</signal>
<receiver>BatchProcessing</receiver>
<slot>close()</slot>
<hints>
<hint type="sourcelabel">
<x>973</x>
<y>745</y>
</hint>
<hint type="destinationlabel">
<x>511</x>
<y>383</y>
</hint>
</hints>
</connection>
</connections>
<connections/>
</ui>
+22 -10
View File
@@ -35,7 +35,7 @@ bool Database::init(const QLatin1String &connectionName)
}
else
{
qDebug() << "Could not open NGC database";
qWarning() << "Could not open NGC database";
}
}
@@ -61,10 +61,17 @@ 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 minDec_idx ON fits_files(minDec)");
query.exec("CREATE INDEX IF NOT EXISTS maxDec_idx ON fits_files(maxDec)");
version = 1;
}
else if(version > 1)
if(version == 1)
{
qDebug() << "Database version is too new";
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)
{
qWarning() << "Database version is too new";
return false;
}
@@ -93,16 +100,16 @@ bool Database::init(const QLatin1String &connectionName)
m_deleteFile.prepare("DELETE FROM fits_files WHERE id=?");
return true;
}
qDebug() << error.text();
qWarning() << error.text();
}
else
{
qDebug() << "Failed to open database" << connectionName;
qWarning() << "Failed to open database" << connectionName;
}
}
else
{
qDebug() << "Database is invalid";
qWarning() << "Database is invalid";
}
return false;
}
@@ -168,7 +175,7 @@ bool Database::checkError(QSqlQuery &query)
return true;
else
{
qDebug() << error.text();
qWarning() << error.text();
return false;
}
}
@@ -307,6 +314,11 @@ QVector<SkyObject> Database::getObjects(double minRa, double maxRa, double minDe
return objects;
}
const QSqlDatabase &Database::db() const
{
return database;
}
bool Database::indexDir2(const QDir &dir, QProgressDialog *progress, QStringList &scannedDirs)
{
if(scannedDirs.contains(dir.canonicalPath()))return true;
@@ -371,7 +383,7 @@ bool Database::indexFile(const QFileInfo &file)
m_insertFileWcs.bindValue(7, crVal2);
if(!m_insertFileWcs.exec())
{
qDebug() << "Database error" << m_insertFileWcs.lastError();
qWarning() << "Database error" << m_insertFileWcs.lastError();
return false;
}
last_id = m_insertFileWcs.lastInsertId().toLongLong();
@@ -382,7 +394,7 @@ bool Database::indexFile(const QFileInfo &file)
m_insertFile.bindValue(1, mtime);
if(!m_insertFile.exec())
{
qDebug() << "Database error" << m_insertFile.lastError();
qWarning() << "Database error" << m_insertFile.lastError();
return false;
}
last_id = m_insertFile.lastInsertId().toLongLong();
@@ -403,7 +415,7 @@ bool Database::indexFile(const QFileInfo &file)
m_insertFitsHeader.bindValue(3, comments);
if(!m_insertFitsHeader.execBatch())
{
qDebug() << "Database error" << m_insertFitsHeader.lastError();
qWarning() << "Database error" << m_insertFitsHeader.lastError();
return false;
}
}
+1
View File
@@ -42,6 +42,7 @@ public:
void reindex(QProgressDialog *progress);
QStringList getFitsKeywords();
QVector<SkyObject> getObjects(double minRa, double maxRa, double minDec, double maxDec);
const QSqlDatabase& db() const;
protected:
bool indexDir2(const QDir &dir, QProgressDialog *progress, QStringList &scannedDirs);
bool indexFile(const QFileInfo &file);
+591
View File
@@ -0,0 +1,591 @@
#include "databasetree.h"
#include "database.h"
#include "databaseview.h"
#include <QComboBox>
#include <QDialogButtonBox>
#include <QLabel>
#include <QPushButton>
#include <QSettings>
#include <QSqlError>
#include <QStackedWidget>
#include <QVBoxLayout>
DatabaseTreeSettings::DatabaseTreeSettings(const QStringList &data, QStringList keywords, QWidget *parent) : QDialog(parent)
{
setWindowTitle(tr("Add tree grouping"));
QVBoxLayout *vlayout = new QVBoxLayout(this);
setLayout(vlayout);
QStringList key = data[0].split('/');
qsizetype dateobsindex = keywords.indexOf("DATE-OBS");
if(dateobsindex != -1)
{
keywords.insert(dateobsindex + 1, "DATE-OBS_YEAR-MONTH-DAY");
keywords.insert(dateobsindex + 1, "DATE-OBS_YEAR-MONTH");
keywords.insert(dateobsindex + 1, "DATE-OBS_YEAR");
}
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]);
}
vlayout->addWidget(new QLabel(tr("Aggregate function"), this));
_aggregateFunction = new QComboBox(this);
_aggregateFunction->addItems({"", "SUM", "COUNT", "AVG", "MIN", "MAX", "MEDIAN"});
vlayout->addWidget(_aggregateFunction);
_aggregateFunction->setToolTip(tr("This aggregate function will be applied to last level of grouping"));
_aggregateFunction->setCurrentText(data[1]);
QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
connect(buttonBox, &QDialogButtonBox::accepted, this, &DatabaseTreeSettings::acceptButton);
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('/');
}
QString DatabaseTreeSettings::aggregrationFunc() const
{
return _aggregateFunction->currentText();
}
void DatabaseTreeSettings::acceptButton()
{
for(QComboBox *box : _keywordsSelect)
{
if(box->currentIndex() > 0)
{
QDialog::accept();
return;
}
}
QDialog::reject();
}
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();
}
QStringList DatabaseTree::keys() const
{
return _keys;
}
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);
}
}
QSqlQuery DatabaseTree::getGroupQuery(const QString &aggregateFunc) const
{
QStringList cols;
QString join;
QString sum;
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);
if(_keys[i] == "DATE-OBS_YEAR")
cols.append(QString("STRFTIME('%Y', h%1.value)").arg(i));
else if(_keys[i] == "DATE-OBS_YEAR-MONTH")
cols.append(QString("STRFTIME('%Y-%m', h%1.value)").arg(i));
else if(_keys[i] == "DATE-OBS_YEAR-MONTH-DAY")
cols.append(QString("STRFTIME('%Y-%m-%d', h%1.value)").arg(i));
else
cols.append(QString("h%1.value").arg(i));
if(i == _keys.size() - 1)
{
QString tmp = aggregateFunc + "(" + cols.last() + ")";
if(aggregateFunc == "COUNT")
cols.append("COUNT(*)");
else
cols.last() = tmp;
}
}
QStringList group = cols;
group.removeLast();
QString sql = "SELECT " + cols.join(',') + " FROM fits_files AS f" + join + " GROUP BY " + group.join(',') + " ORDER BY " + cols.join(" NULLS LAST,") + " NULLS LAST";
QSqlQuery query(sql, _database->db());
for(auto &val : _keys)
{
if(val.startsWith("DATE-OBS_"))
query.addBindValue("DATE-OBS");
else
query.addBindValue(val);
}
qDebug() << "Group query" << sql;
if(!query.exec())
qWarning() << "Group query failed" << query.lastError();
return query;
}
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 col = QString("h%1.value").arg(i);
if(_keys[i] == "DATE-OBS_YEAR")
col = QString("STRFTIME('%Y', h%1.value)").arg(i);
else if(_keys[i] == "DATE-OBS_YEAR-MONTH")
col = QString("STRFTIME('%Y-%m', h%1.value)").arg(i);
else if(_keys[i] == "DATE-OBS_YEAR-MONTH-DAY")
col = QString("STRFTIME('%Y-%m-%d', h%1.value)").arg(i);
sql = QString("SELECT %1 FROM fits_files AS f").arg(col) + join + where + QString(" GROUP BY %1 ORDER BY %1 NULLS LAST").arg(col);
qDebug() << "Tree query for" << _keys[i] << sql;
QSqlQuery query(sql, _database->db());
for(auto &val : _keys)
{
if(val.startsWith("DATE-OBS_"))
query.addBindValue("DATE-OBS");
else
query.addBindValue(val);
}
if(where.isEmpty())
where += QString(" WHERE %1 IS ?").arg(col);
else
where += QString(" AND %1 IS ?").arg(col);
_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)
{
if(val.startsWith("DATE-OBS_"))
files.addBindValue("DATE-OBS");
else
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() << q.lastQuery() << q.boundValues();
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);
_tableView = new CopyTableView(this);
_sqlModel = new QSqlQueryModel(this);
_tableView->setModel(_sqlModel);
QSettings settings;
QStringList filters = settings.value("databasetreeview/filters", QStringList{"OBJECT", "OBJECT/IMAGETYP", "OBJECT/IMAGETYP/FILTER", "OBJECT/IMAGETYP/FILTER/EXPTIME",
"IMAGETYP/OBJECT/IMAGETYP/FILTER/EXPTIME", "IMAGETYP/DATE-OBS_YEAR/EXPTIME"}).toStringList();
QStringList aggrFuncs = settings.value("databasetreeview/aggrFuncs", QStringList{"", "", "", "SUM", "SUM", "SUM"}).toStringList();
int selectedFilter = settings.value("databasetreeview/selectedFilter", 2).toInt();
_filters = new QComboBox(this);
for(int i = 0; i < std::min(filters.size(), aggrFuncs.size()); i++)
{
_filters->addItem(filters[i] + " " + aggrFuncs[i], QStringList{filters[i], aggrFuncs[i]});
}
_filters->setCurrentIndex(selectedFilter);
connect(_filters, &QComboBox::currentIndexChanged, this, &DatabaseTreeView::filterChanged);
filterChanged(_filters->currentIndex());
QStackedWidget *stackedWidget = new QStackedWidget;
stackedWidget->addWidget(_treeView);
stackedWidget->addWidget(_tableView);
QPushButton *addButton = new QPushButton(tr("Add"), this);
QPushButton *removeButton = new QPushButton(tr("Remove"), this);
QPushButton *treeTableButton = new QPushButton(tr("Tree/Table"), this);
treeTableButton->setCheckable(true);
connect(treeTableButton, &QPushButton::clicked, [stackedWidget](bool checked){
stackedWidget->setCurrentIndex(checked ? 1 : 0);
});
hlayout->addWidget(_filters, 1);
hlayout->addWidget(addButton);
hlayout->addWidget(removeButton);
hlayout->addWidget(treeTableButton);
vlayout->addLayout(hlayout);
vlayout->addWidget(stackedWidget);
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;
QStringList aggrFuncs;
for(int i = 0; i < _filters->count(); i++)
{
QStringList data = _filters->itemData(i).toStringList();
filters.append(data[0]);
aggrFuncs.append(data[1]);
}
QSettings settings;
settings.setValue("databasetreeview/filters", filters);
settings.setValue("databasetreeview/aggrFuncs", aggrFuncs);
settings.setValue("databasetreeview/selectedFilter", _filters->currentIndex());
}
void DatabaseTreeView::addFilter()
{
QStringList keywords = _database->getFitsKeywords();
QStringList data = _filters->currentData().toStringList();
DatabaseTreeSettings settings(data, keywords, this);
int result = settings.exec();
if(result == QDialog::Accepted)
{
QString keywords = settings.keywords();
QString aggrFunc = settings.aggregrationFunc();
QString text = keywords + " " + aggrFunc;
int idx = _filters->findText(text);
if(idx == -1)
{
_filters->addItem(text, QStringList{keywords, aggrFunc});
_filters->setCurrentText(text);
}
else
{
_filters->setCurrentIndex(idx);
}
}
}
void DatabaseTreeView::removeFilter()
{
if(_filters->count() > 1)
_filters->removeItem(_filters->currentIndex());
}
void DatabaseTreeView::filterChanged(int index)
{
QStringList data = _filters->itemData(index).toStringList();
QStringList keys = data[0].split('/');
_model->setKeys(keys);
setQuery(data[1]);
}
void DatabaseTreeView::visible(bool visible)
{
if(visible && !_loaded)
{
_loaded = true;
_model->load();
QStringList data = _filters->currentData().toStringList();
setQuery(data[1]);
}
}
void DatabaseTreeView::setQuery(const QString &func)
{
QStringList keys = _model->keys();
int i = 0;
_sqlModel->setQuery(_model->getGroupQuery(func));
if(!func.isEmpty())
{
QString tmp = func + "(" + keys.last() + ")";
if(func == "COUNT")
keys.append("COUNT");
else
keys.last() = tmp;
}
for(auto &key : keys)
_sqlModel->setHeaderData(i++, Qt::Horizontal, key);
_tableView->resizeColumnsToContents();
}
+83
View File
@@ -0,0 +1,83 @@
#ifndef DATABASETREE_H
#define DATABASETREE_H
#include <QAbstractItemModel>
#include <QComboBox>
#include <QDialog>
#include <QFont>
#include <QSqlQuery>
#include <QSqlQueryModel>
#include <QTableView>
#include <QTreeView>
#include <memory>
class Database;
class TreeNode;
class DatabaseTreeSettings : public QDialog
{
Q_OBJECT
public:
explicit DatabaseTreeSettings(const QStringList &data, QStringList keywords, QWidget *parent = nullptr);
QString keywords() const;
QString aggregrationFunc() const;
public slots:
void acceptButton();
private:
QVector<QComboBox*> _keywordsSelect;
QComboBox *_aggregateFunction;
};
class DatabaseTree : public QAbstractItemModel
{
public:
explicit DatabaseTree(Database *database, QObject *parent = nullptr);
void setKeys(const QStringList &keys);
QStringList keys() const;
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();
QSqlQuery getGroupQuery(const QString &aggregateFunc) const;
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(int index);
void visible(bool visible);
private:
void setQuery(const QString &func);
signals:
void loadFile(const QString &file);
private:
QComboBox *_filters = nullptr;
QTreeView *_treeView = nullptr;
QTableView *_tableView = nullptr;
DatabaseTree *_model = nullptr;
QSqlQueryModel *_sqlModel = nullptr;
Database *_database = nullptr;
bool _loaded = false;
};
#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>
+123 -10
View File
@@ -9,6 +9,9 @@
#include <QMenu>
#include <QContextMenuEvent>
#include <QRegularExpression>
#include <QGuiApplication>
#include <QClipboard>
#include <QMimeData>
#include <iostream>
#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()
{
if(!m_loaded)return;
QString cols;
QString join;
QStringList where;
QString sql = m_columns.size() ? "SELECT f.file," : "SELECT f.file";
QVariantList bindValues;
QVariantList bindValuesJoin;
for(int i=0; i<m_value.size(); i++)
{
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")
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")
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")
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")
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
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;
for(auto &column : m_columns)
{
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++;
}
cols.chop(1);
@@ -191,7 +229,19 @@ void FITSFileModel::prepareQuery()
sql += join;
if(!where.isEmpty())sql += " WHERE " + where.join("AND");
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;
else
setQuery(std::move(query));
setHeaderData(0, Qt::Horizontal, tr("File name"));
i = 1;
for(auto &column : m_columns)
@@ -206,7 +256,7 @@ void FITSFileModel::prepareQuery()
m_markedFiles = QSet<QString>(list.begin(), list.end());
}
DatabaseTableView::DatabaseTableView(QWidget *parent) : QTableView(parent)
DatabaseTableView::DatabaseTableView(QWidget *parent) : CopyTableView(parent)
{
}
@@ -217,6 +267,7 @@ void DatabaseTableView::contextMenuEvent(QContextMenuEvent *event)
QAction *unmark = menu.addAction(tr("Unmark"));
QAction *open = menu.addAction(tr("Open"));
QAction *openDirAction = menu.addAction(tr("Open file location"));
QAction *copyPath = menu.addAction(tr("Copy files"));
QAction *a = menu.exec(event->globalPos());
if(a == nullptr)
@@ -232,6 +283,22 @@ void DatabaseTableView::contextMenuEvent(QContextMenuEvent *event)
emit openFile(indexes);
else if(a == openDirAction)
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)
@@ -300,12 +367,13 @@ DataBaseView::DataBaseView(Database *database, QWidget *parent) : QWidget(parent
};
QStringList fitsKeywords = m_database->getFitsKeywords();
QStringList filterKey = settings.value("databaseview/filterKey", QStringList{"file", "file", "file"}).toStringList();
for(int i=0; i<3; i++)
{
m_filterKeyword[i] = new QComboBox(this);
m_filterKeyword[i]->setMaximumWidth(300);
addFilterItems(m_filterKeyword[i], fitsKeywords);
m_filterKeyword[i]->setCurrentText(filterKey[i]);
m_search[i] = new QLineEdit(this);
m_search[i]->setPlaceholderText(tr("Text to search, you can % as wildcard"));
@@ -339,8 +407,13 @@ DataBaseView::DataBaseView(Database *database, QWidget *parent) : QWidget(parent
DataBaseView::~DataBaseView()
{
QStringList filterKey;
for(int i = 0; i < 3; i++)
filterKey.append(m_filterKeyword[i]->currentText());
QSettings settings;
settings.setValue("databaseview/header", m_tableView->horizontalHeader()->saveState());
settings.setValue("databaseview/filterKey", filterKey);
}
void DataBaseView::selectColumns()
@@ -392,6 +465,7 @@ bool DataBaseView::exportCSV(const QString &path)
if(!csv.open(QIODevice::WriteOnly | QIODevice::Text))
return false;
m_model->load();
QSqlQuery sql(m_model->query().lastQuery());
int colCount = m_model->columnCount();
QStringList header;
@@ -420,3 +494,42 @@ bool DataBaseView::exportCSV(const QString &path)
}
return true;
}
void DataBaseView::visible(bool visible)
{
if(visible)m_model->load();
}
CopyTableView::CopyTableView(QWidget *parent) : QTableView(parent)
{
}
void CopyTableView::keyPressEvent(QKeyEvent *event)
{
if(event->matches(QKeySequence::Copy))
{
QModelIndexList list = selectedIndexes();
QString table;
if(list.size() == 0)return;
int row = list.first().row();
int col = list.first().column();
for(auto &index : list)
{
if(row != index.row())
table.append('\n');
else if(col != index.column())
table.append('\t');
table.append(index.data().toString());
row = index.row();
col = index.column();
}
qApp->clipboard()->setText(table);
event->accept();
}
else
{
QTableView::keyPressEvent(event);
}
}
+12 -1
View File
@@ -30,6 +30,7 @@ class FITSFileModel : public QSqlQueryModel
QStringList m_limit;
QSet<QString> m_markedFiles;
Database *m_database;
bool m_loaded = false;
public:
explicit FITSFileModel(Database *database, QObject *parent = nullptr);
void sort(int column, Qt::SortOrder order) override;
@@ -38,11 +39,20 @@ public:
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
void filesMarked(const QModelIndexList &indexes);
void filesUnmarked(const QModelIndexList &indexes);
void load();
protected:
void prepareQuery();
};
class DatabaseTableView : public QTableView
class CopyTableView : public QTableView
{
Q_OBJECT
public:
explicit CopyTableView(QWidget *parent = nullptr);
void keyPressEvent(QKeyEvent *event);
};
class DatabaseTableView : public CopyTableView
{
Q_OBJECT
public:
@@ -74,6 +84,7 @@ public slots:
void itemActivated(const QModelIndex &index);
void applyFilter();
bool exportCSV(const QString &path);
void visible(bool visible);
signals:
void loadFile(QString file);
};
+334 -5
View File
@@ -5,8 +5,214 @@
#include <QSettings>
#include <QStandardPaths>
#include <QDesktopServices>
#include <QMimeData>
#include <QClipboard>
#include <QThread>
#include <QDirIterator>
#include "loadimage.h"
class FileTimes
{
public:
explicit FileTimes(const QString &path)
{
QFile file(path);
#ifndef Q_OS_WIN
birthTime = file.fileTime(QFileDevice::FileBirthTime);
#endif
modificationTime = file.fileTime(QFileDevice::FileModificationTime);
accessTime = file.fileTime(QFileDevice::FileAccessTime);
}
void apply(const QString &path)
{
QFile file(path);
if(file.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::ExistingOnly))
{
#ifndef Q_OS_WIN // Only windows allow changing birth time
file.setFileTime(birthTime, QFileDevice::FileBirthTime);
#endif
file.setFileTime(accessTime, QFileDevice::FileAccessTime);
file.setFileTime(modificationTime, QFileDevice::FileModificationTime);
}
}
private:
QDateTime birthTime;
QDateTime modificationTime;
QDateTime accessTime;
};
FileTransfer::FileTransfer(FileManager *fm) :
_fm(fm)
{
}
FileTransfer::~FileTransfer()
{
_run = false;
}
void FileTransfer::copy(const QStringList &src, const QString &dst)
{
_run = true;
perform(src, dst, true);
emit finished();
}
void FileTransfer::move(const QStringList &src, const QString &dst)
{
_run = true;
perform(src, dst, false);
emit finished();
}
void FileTransfer::cancel()
{
_run = false;
}
void FileTransfer::perform(const QStringList &src, const QString &dst, bool copy)
{
QDir dstDir(dst);
if(!dstDir.exists())
{
emit error(tr("Error"), tr("Destination directory %1 doesn't exists").arg(dstDir.absolutePath()));
return;
}
QList<Action> actions;
QStringList dirs;
emit progress(0);
for(const QString &i : src)
{
QFileInfo srcInfo(i);
if(srcInfo.absolutePath() == dst || dst.startsWith(srcInfo.absoluteFilePath()))
return;
if(srcInfo.isDir())
{
QDir srcDir(i);
//qDebug() << "dir" << srcInfo.absoluteFilePath() << srcInfo.fileName();
if(!copy && !dstDir.exists(srcInfo.fileName()))
{
if(QFile::rename(srcInfo.absoluteFilePath(), dstDir.absoluteFilePath(srcInfo.fileName())))
continue;
}
actions.append({srcInfo.absoluteFilePath(), srcInfo.fileName(), true});
if(!copy)dirs.prepend(srcInfo.absoluteFilePath());
QDirIterator it(i, QDirIterator::Subdirectories | QDirIterator::FollowSymlinks);
while(it.hasNext())
{
QFileInfo info = it.nextFileInfo();
if(info.fileName() == "." || info.fileName() == "..")
continue;
QString relativePath = srcDir.dirName() + "/" + srcDir.relativeFilePath(info.absoluteFilePath());
if(info.isDir())
{
actions.append({"", relativePath, true});
if(!copy)dirs.prepend(info.absoluteFilePath());
//qDebug() << "dir" << info.absoluteFilePath() << relativePath;
}
else
{
actions.append({info.absoluteFilePath(), dstDir.absoluteFilePath(relativePath), false});
//qDebug() << "file" << info.absoluteFilePath() << dstDir.absoluteFilePath(relativePath);
}
}
}
else
{
actions.append({srcInfo.absoluteFilePath(), dstDir.absoluteFilePath(srcInfo.fileName())});
//qDebug() << "file" << srcInfo.absoluteFilePath() << dstDir.absoluteFilePath(srcInfo.fileName());
}
}
bool overwriteAll = false;
bool skipAll = false;
int total = actions.size();
int i = 0;
for(auto &a : actions)
{
if(!_run)
return;
if(a.dir)
{
dstDir.mkpath(a.dst);
}
else
{
QFileInfo dstInfo(a.dst);
if(dstInfo.exists())
{
if(overwriteAll)
{
QFile::remove(dstInfo.absoluteFilePath());
}
else if(skipAll)
{
emit progress(i++ * 100 / total);
continue;
}
else
{
QMessageBox::StandardButton ret;
QMetaObject::invokeMethod(_fm, "overwrite", Qt::BlockingQueuedConnection, Q_RETURN_ARG(QMessageBox::StandardButton, ret), Q_ARG(QString, dstInfo.fileName()));
switch(ret)
{
case QMessageBox::YesToAll:
overwriteAll = true;//break; is intentionally missing
case QMessageBox::Yes:
QFile::remove(dstInfo.absoluteFilePath());
break;
case QMessageBox::NoToAll:
skipAll = true;//break; is intentionally missing
case QMessageBox::No:
emit progress(i++ * 100 / total);
continue;
break;
case QMessageBox::Cancel:
return;
default:
break;
}
}
}
FileTimes t(a.src);
if(copy)
{
if(!QFile::copy(a.src, a.dst))
{
emit error(tr("Copy failed"), tr("Failed to copy file %1 to %2").arg(a.src).arg(a.dst));
return;
}
}
else
{
if(!QFile::rename(a.src, a.dst))
{
emit error(tr("Move failed"), tr("Failed to move file %1 to %2").arg(a.src).arg(a.dst));
return;
}
}
t.apply(a.dst);
}
emit progress(i++ * 100 / total);
}
if(!copy)
{
for(const QString &d : dirs)
{
QDir dir(d);
if(dir.isEmpty())
dir.removeRecursively();
}
}
}
PathTabBar::PathTabBar(const QStringList &tabs) :
_tabs(tabs)
{
@@ -147,6 +353,8 @@ FileManager::FileManager(const QSet<QString> &openFilter, QWidget *parent) : QMa
connect(ui->rightTab, &DirView::dirChanged, _rightTabBar, &PathTabBar::pathChanged);
connect(ui->leftTab, &DirView::openFile, this, &FileManager::openFile);
connect(ui->rightTab, &DirView::openFile, this, &FileManager::openFile);
connect(ui->leftTab, &DirView::filesAction, this, &FileManager::copyMoveFiles, Qt::QueuedConnection);
connect(ui->rightTab, &DirView::filesAction, this, &FileManager::copyMoveFiles, Qt::QueuedConnection);
connect(ui->actionLoad_FITS_keywordsLeft, &QAction::toggled, ui->leftTab, &DirView::loadFitsKeywords);
connect(ui->actionLoad_FITS_keywordsRight, &QAction::toggled, ui->rightTab, &DirView::loadFitsKeywords);
@@ -165,6 +373,8 @@ FileManager::FileManager(const QSet<QString> &openFilter, QWidget *parent) : QMa
connect(ui->actionSelect_columnsLeft, &QAction::triggered, this, &FileManager::selectFITSKeywords);
connect(ui->actionSelect_columnsRight, &QAction::triggered, this, &FileManager::selectFITSKeywords);
connect(ui->actionCopySelectedFilesPathsLeft, &QAction::triggered, this, &FileManager::copySelectedFilesPaths);
connect(ui->actionCopySelectedFilesPathsRight, &QAction::triggered, this, &FileManager::copySelectedFilesPaths);
connect(ui->leftPath, &QLineEdit::returnPressed, this, &FileManager::pathEdited);
connect(ui->rightPath, &QLineEdit::returnPressed, this, &FileManager::pathEdited);
@@ -175,6 +385,25 @@ FileManager::FileManager(const QSet<QString> &openFilter, QWidget *parent) : QMa
ui->menuLeft_Tab->addAction(drive.absoluteFilePath(), [path, this](){ ui->leftTab->setDir(path); });
ui->menuRight_Tab->addAction(drive.absoluteFilePath(), [path, this](){ ui->rightTab->setDir(path); });
}
ui->progressBar->hide();
ui->cancelButton->hide();
_thread = new QThread(this);
_thread->start();
_fileTransfer = new FileTransfer(this);
_fileTransfer->moveToThread(_thread);
connect(_fileTransfer, &FileTransfer::progress, ui->progressBar, &QProgressBar::setValue);
connect(_fileTransfer, &FileTransfer::error, this, &FileManager::errorMessage);
connect(_fileTransfer, &FileTransfer::finished, [this](){
ui->leftTab->setDragEnabled(true);
ui->rightTab->setDragEnabled(true);
ui->progressBar->hide();
ui->cancelButton->hide();
});
connect(this, &FileManager::copy, _fileTransfer, &FileTransfer::copy);
connect(this, &FileManager::move, _fileTransfer, &FileTransfer::move);
connect(ui->cancelButton, &QPushButton::clicked, [this](){ _fileTransfer->cancel(); });
}
FileManager::~FileManager()
@@ -190,6 +419,12 @@ FileManager::~FileManager()
settings.setValue("filemanager/rightLoadFitsKeywords", ui->actionLoad_FITS_keywordsRight->isChecked());
settings.setValue("filemanager/geometry", saveGeometry());
delete ui;
_fileTransfer->cancel();
_thread->quit();
_thread->wait();
delete _fileTransfer;
}
void FileManager::selectFITSKeywords()
@@ -213,6 +448,14 @@ void FileManager::selectFITSKeywords()
}
}
void FileManager::copySelectedFilesPaths()
{
if(sender() == ui->actionCopySelectedFilesPathsLeft)
ui->leftTab->copySelectedFilesPathsToClipboard();
if(sender() == ui->actionCopySelectedFilesPathsRight)
ui->rightTab->copySelectedFilesPathsToClipboard();
}
void FileManager::pathEdited()
{
if(sender() == ui->leftPath)
@@ -229,6 +472,40 @@ void FileManager::pathEdited()
}
}
void FileManager::copyMoveFiles(Qt::DropAction action, const QStringList &src, const QString &dst)
{
ui->leftTab->setDragEnabled(false);
ui->rightTab->setDragEnabled(false);
ui->progressBar->show();
ui->cancelButton->show();
switch(action)
{
case Qt::CopyAction:
emit copy(src, dst);
break;
case Qt::MoveAction:
emit move(src, dst);
break;
case Qt::LinkAction:
default:
break;
}
}
QMessageBox::StandardButton FileManager::overwrite(const QString &dst)
{
QMessageBox::StandardButton button = QMessageBox::question(this, tr("Overwrite file?"), tr("Destination file %1 already exists. Overwrite?").arg(dst),
QMessageBox::Yes | QMessageBox::YesToAll | QMessageBox::No | QMessageBox::NoToAll | QMessageBox::Cancel);
return button;
}
void FileManager::errorMessage(const QString &title, const QString &text)
{
QMessageBox::critical(this, title, text);
}
QCache<QString, ImageInfoData>* DirFileSystemModel::getCacheInstance()
{
static bool init = true;
@@ -241,7 +518,8 @@ QCache<QString, ImageInfoData>* DirFileSystemModel::getCacheInstance()
return &cache;
}
DirFileSystemModel::DirFileSystemModel(QObject *parent) : QFileSystemModel(parent)
DirFileSystemModel::DirFileSystemModel(QWidget *parentWidget) : QFileSystemModel(parentWidget)
,_parentWidget(parentWidget)
{
_cache = getCacheInstance();
setFilter(QDir::AllEntries | QDir::NoDot);
@@ -272,7 +550,9 @@ const QStringList &DirFileSystemModel::FITSKeywords() const
Qt::ItemFlags DirFileSystemModel::flags(const QModelIndex &index) const
{
return QFileSystemModel::flags(index) & ~Qt::ItemIsEditable;
Qt::ItemFlags ret = QFileSystemModel::flags(index) & ~Qt::ItemIsEditable;
if(index.row() == 0)ret &= ~Qt::ItemIsDragEnabled;
return ret;
}
int DirFileSystemModel::columnCount(const QModelIndex &parent) const
@@ -335,6 +615,21 @@ bool DirFileSystemModel::hasChildren(const QModelIndex &parent) const
return QFileSystemModel::hasChildren(parent);
}
bool DirFileSystemModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent)
{
Q_UNUSED(row);
Q_UNUSED(column);
if(data->hasUrls())
{
QStringList srcPaths;
for(auto &url : data->urls())
srcPaths.append(url.toLocalFile());
emit filesAction(action, srcPaths, filePath(parent));
}
return false;
}
void DirFileSystemModel::loadFitsKeywords(bool enable)
{
_loadFitsKeywords = enable;
@@ -348,6 +643,8 @@ DirView::DirView(QWidget *parent) : QTreeView(parent)
setDragEnabled(true);
setAcceptDrops(true);
connect(_dirFileSystemModel, &DirFileSystemModel::filesAction, this, &DirView::filesAction);
setModel(_dirFileSystemModel);
setSelectionMode(QAbstractItemView::ExtendedSelection);
@@ -382,10 +679,21 @@ void DirView::setDir(const QString &path)
if(oldPath.left(ROOT_LEN) != path.left(ROOT_LEN))
_dirFileSystemModel->setRootPath(path.left(ROOT_LEN));
_dirFileSystemModel->setDir(path);
setRootIndex(_dirFileSystemModel->index(path, 0));
QString newPath = path;
if(!QFileInfo::exists(path))
{
QDir dir(path);
do
{
dir.setPath(QDir::cleanPath(dir.filePath("..")));
}while(!dir.exists() && !dir.isRoot());
newPath = dir.path();
}
_dirFileSystemModel->setDir(newPath);
setRootIndex(_dirFileSystemModel->index(newPath, 0));
clearSelection();
if(oldPath != path)emit dirChanged(path);
if(oldPath != newPath)emit dirChanged(newPath);
}
QString DirView::dir() const
@@ -435,3 +743,24 @@ void DirView::loadFitsKeywords(bool enable)
{
_dirFileSystemModel->loadFitsKeywords(enable);
}
void DirView::copySelectedFilesPathsToClipboard() const
{
QList<QUrl> urls;
QString text;
auto selected = selectionModel()->selectedRows();
for(auto &item : selected)
{
if(item.column() == 0)
{
QString path = _dirFileSystemModel->filePath(item);
text.append(path); text.append('\n');
urls.append(QUrl::fromLocalFile(path));
}
}
QClipboard *clipboard = QApplication::clipboard();
QMimeData *mimeData = new QMimeData();
mimeData->setUrls(urls);
mimeData->setText(text);
clipboard->setMimeData(mimeData);
}
+50 -6
View File
@@ -8,6 +8,7 @@
#include <QDialog>
#include <QTabBar>
#include <QHBoxLayout>
#include <QMessageBox>
#include "imageinfodata.h"
namespace Ui {
@@ -15,6 +16,34 @@ class FileManager;
class FITSKeyword;
}
class FileManager;
class FileTransfer: public QObject
{
Q_OBJECT
public:
explicit FileTransfer(FileManager *fm);
~FileTransfer();
public slots:
void copy(const QStringList &src, const QString &dst);
void move(const QStringList &src, const QString &dst);
void cancel();
signals:
void progress(int percent);
void finished();
void error(const QString &title, const QString &text);
private:
void perform(const QStringList &src, const QString &dst, bool copy);
struct Action
{
QString src;
QString dst;
bool dir = false;
};
FileManager *_fm;
bool _run = true;
};
class PathTabBar : public QTabBar
{
Q_OBJECT
@@ -51,25 +80,28 @@ public:
~FileManager();
public slots:
void selectFITSKeywords();
void copySelectedFilesPaths();
void pathEdited();
void copyMoveFiles(Qt::DropAction action, const QStringList &src, const QString &dst);
QMessageBox::StandardButton overwrite(const QString &dst);
void errorMessage(const QString &title, const QString &text);
signals:
void openFile(const QString &path);
void copy(const QStringList &src, const QString &dst);
void move(const QStringList &src, const QString &dst);
private:
Ui::FileManager *ui;
PathTabBar *_leftTabBar;
PathTabBar *_rightTabBar;
QThread *_thread;
FileTransfer *_fileTransfer;
};
class DirFileSystemModel : public QFileSystemModel
{
Q_OBJECT
mutable QCache<QString, ImageInfoData> *_cache = nullptr;
static QCache<QString, ImageInfoData>* getCacheInstance();
QModelIndex _dir;
QStringList _fitsKeywords;
bool _loadFitsKeywords = true;
public:
explicit DirFileSystemModel(QObject *parent = nullptr);
explicit DirFileSystemModel(QWidget *parentWidget);
void setDir(const QString &path);
QString dir() const;
void setFITSKeywords(const QStringList &keywords);
@@ -79,8 +111,18 @@ public:
QVariant data(const QModelIndex &index, int role) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
bool hasChildren(const QModelIndex &parent) const override;
bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) override;
public slots:
void loadFitsKeywords(bool enable);
signals:
void filesAction(Qt::DropAction action, const QStringList &src, const QString &dst);
private:
mutable QCache<QString, ImageInfoData> *_cache = nullptr;
static QCache<QString, ImageInfoData>* getCacheInstance();
QModelIndex _dir;
QStringList _fitsKeywords;
bool _loadFitsKeywords = true;
QWidget *_parentWidget = nullptr;
};
class DirView : public QTreeView
@@ -98,9 +140,11 @@ public:
public slots:
void headerContextMenu(const QPoint &pos);
void loadFitsKeywords(bool enable);
void copySelectedFilesPathsToClipboard() const;
signals:
void dirChanged(const QString &path);
void openFile(const QString &path);
void filesAction(Qt::DropAction action, const QStringList &src, const QString &dst);
};
#endif // FILEMANAGER_H
+47 -7
View File
@@ -14,24 +14,52 @@
<string>File Manager</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QHBoxLayout" name="horizontalLayout">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QVBoxLayout" name="leftLayout">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLineEdit" name="leftPath"/>
<layout class="QVBoxLayout" name="leftLayout">
<item>
<widget class="QLineEdit" name="leftPath"/>
</item>
<item>
<widget class="DirView" name="leftTab"/>
</item>
</layout>
</item>
<item>
<widget class="DirView" name="leftTab"/>
<layout class="QVBoxLayout" name="rightLayout">
<item>
<widget class="QLineEdit" name="rightPath"/>
</item>
<item>
<widget class="DirView" name="rightTab"/>
</item>
</layout>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="rightLayout">
<layout class="QHBoxLayout" name="progressLayout">
<item>
<widget class="QLineEdit" name="rightPath"/>
<widget class="QProgressBar" name="progressBar">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="value">
<number>0</number>
</property>
</widget>
</item>
<item>
<widget class="DirView" name="rightTab"/>
<widget class="QPushButton" name="cancelButton">
<property name="text">
<string>Cancel</string>
</property>
</widget>
</item>
</layout>
</item>
@@ -52,6 +80,7 @@
</property>
<addaction name="actionLoad_FITS_keywordsLeft"/>
<addaction name="actionSelect_columnsLeft"/>
<addaction name="actionCopySelectedFilesPathsLeft"/>
<addaction name="separator"/>
</widget>
<widget class="QMenu" name="menuRight_Tab">
@@ -60,6 +89,7 @@
</property>
<addaction name="actionLoad_FITS_keywordsRight"/>
<addaction name="actionSelect_columnsRight"/>
<addaction name="actionCopySelectedFilesPathsRight"/>
<addaction name="separator"/>
</widget>
<addaction name="menuLeft_Tab"/>
@@ -97,6 +127,16 @@
<string>Select columns</string>
</property>
</action>
<action name="actionCopySelectedFilesPathsLeft">
<property name="text">
<string>Copy selected files paths</string>
</property>
</action>
<action name="actionCopySelectedFilesPathsRight">
<property name="text">
<string>Copy selected files paths</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>
+10 -6
View File
@@ -345,18 +345,22 @@ Download::Download(QNetworkReply *reply, const QString indexPath, QObject *paren
filename.remove(QRegularExpression("\\.zst$"));
_fw.setFileName(indexPath + "/" + filename);
_fw.open(QIODevice::WriteOnly | QIODevice::Truncate);
if(_fw.isOpen())
if(_fw.open(QIODevice::WriteOnly | QIODevice::Truncate))
{
qDebug() << "open file" << _fw.fileName();
_dstream = ZSTD_createDStream();
}
else
{
qWarning() << "Failed to open file" << _fw.fileName();
abort();
}
_dstream = ZSTD_createDStream();
}
Download::~Download()
{
ZSTD_freeDStream(_dstream);
if(_dstream)
ZSTD_freeDStream(_dstream);
}
void Download::abort()
@@ -402,7 +406,7 @@ void Download::finished()
void Download::decompress(QByteArray &data)
{
if(data.isEmpty())return;
if(data.isEmpty() || _dstream == nullptr)return;
_hash.addData(data);
+2 -2
View File
@@ -11,8 +11,8 @@
class Download : public QObject
{
Q_OBJECT
QNetworkReply *_reply;
ZSTD_DStream *_dstream;
QNetworkReply *_reply = nullptr;
ZSTD_DStream *_dstream = nullptr;
QFile _fw;
QCryptographicHash _hash;
public:
+2
View File
@@ -11,6 +11,8 @@ ImageInfo::ImageInfo(QWidget *parent) : QTreeWidget(parent)
setIndentation(5);
QSettings settings;
header()->restoreState(settings.value("imageinfo/headerstate").toByteArray());
setSortingEnabled(true);
header()->setSortIndicatorClearable(true);
}
ImageInfo::~ImageInfo()
+3 -1
View File
@@ -30,6 +30,7 @@ FITSRecord::FITSRecord(const LibXISF::FITSKeyword &record)
string.chop(1);
string.remove(0, 1);
}
string = string.trimmed();
bool isint;
bool isdouble;
double vald = string.toDouble(&isdouble);
@@ -453,7 +454,8 @@ SkyGrid WCSDataT::prepareGrid(uint32_t w, uint32_t h, Database *database)
if(database)
{
skyGrid.objects = database->getObjects(minRa, maxRa, minDec, maxDec);
double size = std::max(maxRa - minRa, maxDec - minDec);
skyGrid.objects = database->getObjects(minRa - size, maxRa + size, minDec - size, maxDec + size);
for(auto &object : skyGrid.objects)
{
QPointF p;
+24 -20
View File
@@ -111,7 +111,7 @@ void Image::thumbnailLoadFinish(std::shared_ptr<RawImage> rawImage)
emit thumbnailLoaded(this);
}
ImageRingList::ImageRingList(Database *database, const QStringList &nameFilter, QObject *parent) : QAbstractItemModel(parent)
ImageRingList::ImageRingList(Database *database, const QStringList &nameFilter, QObject *parent) : QAbstractListModel(parent)
, m_liveMode(false)
, m_analyzeLevel(None)
, m_database(database)
@@ -412,7 +412,7 @@ void ImageRingList::clearThumbnails()
img->clearThumbnail();
}
QModelIndex ImageRingList::index(int row, int column, const QModelIndex &parent) const
/*QModelIndex ImageRingList::index(int row, int column, const QModelIndex &parent) const
{
Q_UNUSED(parent);
return createIndex(row, column, m_images.at(row).get());
@@ -422,7 +422,7 @@ QModelIndex ImageRingList::parent(const QModelIndex &child) const
{
Q_UNUSED(child);
return QModelIndex();
}
}*/
int ImageRingList::rowCount(const QModelIndex &parent) const
{
@@ -432,31 +432,35 @@ int ImageRingList::rowCount(const QModelIndex &parent) const
return 0;
}
int ImageRingList::columnCount(const QModelIndex &parent) const
/*int ImageRingList::columnCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return 1;
}
}*/
QVariant ImageRingList::data(const QModelIndex &index, int role) const
{
switch(role)
if(index.isValid() && index.row() >= 0 && index.row() < m_images.size())
{
case Qt::DisplayRole:
{
QFileInfo info(m_images.at(index.row())->name());
return info.fileName();
}
case Qt::FontRole:
{
bool marked = m_database->isMarked(m_images.at(index.row())->name());
QFont font;
font.setBold(marked);
return font;
}
default:
return QVariant();
switch(role)
{
case Qt::DisplayRole:
{
QFileInfo info(m_images.at(index.row())->name());
return info.fileName();
}
case Qt::FontRole:
{
bool marked = m_database->isMarked(m_images.at(index.row())->name());
QFont font;
font.setBold(marked);
return font;
}
default:
return QVariant();
}
}
return QVariant();
}
QVariant ImageRingList::headerData(int section, Qt::Orientation orientation, int role) const
+4 -4
View File
@@ -51,7 +51,7 @@ typedef std::shared_ptr<Image> ImagePtr;
class Database;
class ImageRingList : public QAbstractItemModel
class ImageRingList : public QAbstractListModel
{
Q_OBJECT
int m_width;
@@ -93,10 +93,10 @@ public:
void updateMark();
void clearThumbnails();
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
QModelIndex parent(const QModelIndex &child) const override;
//QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
//QModelIndex parent(const QModelIndex &child) const override;
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
//int columnCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
public slots:
+6 -6
View File
@@ -154,7 +154,7 @@ void ImageWidgetGL::setImage(std::shared_ptr<RawImage> image, int index)
m_image->bind();
f->glPixelStorei(GL_UNPACK_ALIGNMENT, 4);
f->glGenerateMipmap(GL_TEXTURE_2D);
qDebug() << "setImage" << timer.elapsed();
qDebug() << "ImageWidgetGL::setImage" << timer.elapsed() << "ms";
m_swPaint = f->glGetError() != GL_NO_ERROR;
}
@@ -721,19 +721,19 @@ void ImageWidgetGL::initializeGL()
logger->startLogging();
connect(logger, &QOpenGLDebugLogger::messageLogged, [](const QOpenGLDebugMessage &message)
{
qDebug() << message;
qDebug() << "OpenGL debug" << message;
});
qDebug() << "Vendor:" << (char*)f->glGetString(GL_VENDOR);
qDebug() << "Renderer:" << (char*)f->glGetString(GL_RENDERER);
qDebug() << "Version:" << (char*)f->glGetString(GL_VERSION);
qDebug() << "OpenGL Vendor:" << (char*)f->glGetString(GL_VENDOR);
qDebug() << "OpenGL Renderer:" << (char*)f->glGetString(GL_RENDERER);
qDebug() << "OpenGL Version:" << (char*)f->glGetString(GL_VERSION);
f->glGetIntegerv(GL_MAX_TEXTURE_SIZE, &m_maxTextureSize);
f->glGetIntegerv(GL_MAX_ARRAY_TEXTURE_LAYERS, &m_maxArrayLayers);
qDebug() << "Max texture size:" << m_maxTextureSize << "max layers:" << m_maxArrayLayers;
//MANUAL_MIPMAP_GEN = QString((const char*)f->glGetString(GL_VENDOR)).startsWith("ATI Technologies Inc", Qt::CaseInsensitive);
qDebug() << context()->format();
qDebug() << "OpenGL context format" << context()->format();
// each vertex is x,y 2D position and s,t texture coordinates
float vertexs[] = {-1.0f, -1.0f, 0.0f, 1.0f,
+7 -7
View File
@@ -95,7 +95,7 @@ bool loadFITS(const QString path, ImageInfoData &info, std::shared_ptr<RawImage>
char err[100];
fits_get_errstatus(status, err);
info.info.append({QObject::tr("Error"), QString(err)});
qDebug() << "Failed to load FITS file" << err;
qWarning() << "Failed to load FITS file" << err;
return false;
};
@@ -335,7 +335,7 @@ bool loadXISF(const QString &path, ImageInfoData &info, std::shared_ptr<RawImage
catch (LibXISF::Error &err)
{
info.info.append(QPair<QString, QString>("Error", err.what()));
qDebug() << "Failed to load XISF" << err.what();
qWarning() << "Failed to load XISF" << err.what();
return false;
}
info.info.append({QObject::tr("Error"), QObject::tr("Unsupported sample format")});
@@ -382,7 +382,7 @@ bool readXISFHeader(const QString &path, ImageInfoData &info)
}
catch (LibXISF::Error &err)
{
qDebug() << err.what();
qWarning() << "LibXISF error" << err.what();
return false;
}
return true;
@@ -455,17 +455,17 @@ bool loadImage(const QString &path, ImageInfoData &info, std::shared_ptr<RawImag
if(path.endsWith(".CR2", Qt::CaseInsensitive) || path.endsWith(".CR3", Qt::CaseInsensitive) || path.endsWith(".NEF", Qt::CaseInsensitive) || path.endsWith(".DNG", Qt::CaseInsensitive))
{
ret = loadRAW(path, info, rawImage);
qDebug() << "LoadRAW" << timer.elapsed();
qDebug() << "LoadRAW" << timer.elapsed() << "ms";
}
else if(isFITS(fileInfo.suffix()))
{
ret = loadFITS(path, info, rawImage, planar, index);
qDebug() << "LoadFITS" << timer.elapsed();
qDebug() << "LoadFITS" << timer.elapsed() << "ms";
}
else if(isXISF(fileInfo.suffix()))
{
ret = loadXISF(path, info, rawImage, planar, index);
qDebug() << "LoadXISF" << timer.elapsed();
qDebug() << "LoadXISF" << timer.elapsed() << "ms";
}
else
{
@@ -481,7 +481,7 @@ bool loadImage(const QString &path, ImageInfoData &info, std::shared_ptr<RawImag
exif_data_free(exif);
}
rawImage = std::make_shared<RawImage>(img);
qDebug() << "LoadQImage" << timer.elapsed();
qDebug() << "LoadQImage" << timer.elapsed() << "ms";
ret = !img.isNull();
}
return ret;
+5 -1
View File
@@ -193,7 +193,11 @@ void ConvertRunable::run()
QFileInfo info(m_outfile);
info.dir().mkpath(".");
if(m_params.autostretch)
if(m_params.stretch)
{
rawimage->applySTF(m_params.mtf);
}
else if(m_params.autostretch)
{
rawimage->calcStats();
MTFParam mtfParam = rawimage->calcMTFParams();
+3
View File
@@ -6,6 +6,7 @@
#include <QSemaphore>
#include <QSize>
#include "imageinfodata.h"
#include "mtfparam.h"
class Image;
@@ -33,6 +34,8 @@ public:
QSize resize;
Qt::AspectRatioMode aspect = Qt::KeepAspectRatio;
bool autostretch = false;
bool stretch = false;
MTFParam mtf;
ConvertParams(){}
ConvertParams(const QVariantMap &map);
};
+44 -4
View File
@@ -3,8 +3,12 @@
#include <QSurfaceFormat>
#include <QTranslator>
#include <QCommandLineParser>
#include <QSettings>
#include <stdlib.h>
#include "../thumbnailer/genthumbnail.h"
#ifdef Q_OS_WIN64
#include <windows.h>
#endif
int main(int argc, char *argv[])
{
@@ -18,12 +22,24 @@ int main(int argc, char *argv[])
bool useGLES = true;
#endif
#ifdef Q_OS_WIN64
if(AttachConsole(ATTACH_PARENT_PROCESS))
{
freopen("CONOUT$", "w", stdout);
freopen("CONOUT$", "w", stderr);
}
#endif
QCommandLineParser cmd;
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({{"thumb", "thumbnail"}, "Generate thumbnail and save it to path.", "path"});
cmd.addOption({{"s", "size"}, "Size of the thumbnails in pixels (default: 128)", "size", "128"});
cmd.addPositionalArgument("file", "File to open");
cmd.addPositionalArgument("file", "Files or paths to open");
cmd.addOption({"script", "Execute script", "script"});
cmd.addOption({"scriptarg", "String that will be passed to script as variable \"scriparg\"", "arg"});
cmd.addOption({"outdir", "Output dir for script (default: CWD)", "dir", "."});
cmd.addOption({"noexit", "Do not exit application when script finish"});
cmd.addHelpOption();
QStringList cmdArgs;
for(int i = 0; i < argc; i++)
@@ -76,15 +92,26 @@ int main(int argc, char *argv[])
QTranslator translator;
QTranslator translator2;
if(translator.load(QLocale(), "tenmon", "_", ":/translations"))
a.installTranslator(&translator);
QSettings settings;
QString lang = settings.value("settings/lang").toString();
if(lang.isEmpty())
{
if(translator.load(QLocale(), "tenmon", "_", ":/translations"))
a.installTranslator(&translator);
}
else
{
if(translator.load("tenmon_" + lang, ":/translations"))
a.installTranslator(&translator);
}
if(translator2.load(QLocale(), "tenmon", "_", a.applicationDirPath()))
a.installTranslator(&translator2);
MainWindow w;
w.show();
if(!cmd.positionalArguments().isEmpty())
if(!cmd.positionalArguments().isEmpty() && !cmd.isSet("script"))
{
QStringList files = cmd.positionalArguments();
QStringList paths;
@@ -101,5 +128,18 @@ int main(int argc, char *argv[])
w.loadFiles(paths);
}
if(cmd.isSet("script"))
{
QStringList paths = cmd.positionalArguments();
QString script = cmd.value("script");
QString outdir = cmd.value("outdir");
QString arg = cmd.value("scriptarg");
if(!QDir::isAbsolutePath(script))script = QDir::currentPath() + "/" + script;
if(!QDir::isAbsolutePath(outdir))outdir = QDir::currentPath() + "/" + outdir;
bool noexit = cmd.isSet("noexit");
if(!noexit)w.hide();
w.runScript(script, outdir, paths, arg, !noexit);
}
return a.exec();
}
+62 -17
View File
@@ -18,10 +18,12 @@
#include <QThreadPool>
#include <QStatusBar>
#include <QImageReader>
#include <QImageWriter>
#include <QMimeDatabase>
#include <QDesktopServices>
#include <QJsonDocument>
#include <QNetworkReply>
#include <QTimer>
#include "loadrunable.h"
#include "markedfiles.h"
#include "about.h"
@@ -57,12 +59,18 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
for(auto format : supportedFormats)
{
QMimeType mimeType = db.mimeTypeForName(format);
_saveFilter.append(mimeType.filterString() + ";;");
_openFilter.append("*.");
_openFilter.append(mimeType.suffixes().join(" *."));
_openFilter.append(" ");
nameFilter.append(mimeType.suffixes());
}
auto supportedWrite = QImageWriter::supportedMimeTypes();
for(auto format : supportedWrite)
{
QMimeType mimeType = db.mimeTypeForName(format);
_saveFilter.append(mimeType.filterString() + ";;");
}
_openFilter.append("*.fit *.fits *.fts *.fz *.xisf *.cr2 *.cr3 *.nef *.dng)");
_openFilter.append(tr(";;All files (*)"));
nameFilter.append({"fit", "fits", "fts", "fz", "xisf", "cr2", "cr3", "nef", "dng"});
@@ -120,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();
@@ -146,6 +156,7 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
databaseViewDock->setWidget(m_databaseView);
databaseViewDock->setObjectName("databaseViewDock");
databaseViewDock->hide();
connect(databaseViewDock, &QDockWidget::visibilityChanged, m_databaseView, &DataBaseView::visible);
addDockWidget(Qt::BottomDockWidgetArea, databaseViewDock);
QDockWidget *filetreeDock = nullptr;
@@ -164,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);
@@ -189,7 +209,12 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
#endif
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 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->addAction(tr("Index directory"), this, static_cast<void (MainWindow::*)()>(&MainWindow::indexDir));
fileMenu->addAction(tr("Reindex files"), this, &MainWindow::reindex);
@@ -323,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
@@ -613,12 +639,16 @@ void MainWindow::reindex()
void MainWindow::saveAs()
{
QString selectedFilter;
ImagePtr ptr = m_ringList->currentImage();
if(!ptr)return;
QFileInfo srcFile(ptr->name());
QString file = QFileDialog::getSaveFileName(this,
tr("Save as"),
_lastDir,
_lastDir + "/" + srcFile.baseName(),
_saveFilter,
&selectedFilter);
auto filterToFormat = [](const QString &file, const QString &filter) -> const char*
auto filterToFormat = [](const QString &file, const QString &filter) -> const QString
{
QString suffix = QFileInfo(file).suffix();
if(!suffix.compare("jpg", Qt::CaseInsensitive) || !suffix.compare("jpeg", Qt::CaseInsensitive))return "jpeg";
@@ -628,30 +658,31 @@ void MainWindow::saveAs()
if(filter.contains("png"))return "png";
if(filter.contains("fits"))return "fits";
if(filter.contains("xisf"))return "xisf";
QRegularExpression suf("\\(\\*\\.([a-zA-Z]+).*\\)");
auto match = suf.match(filter);
if(match.hasMatch())
return match.captured(1);
return "jpeg";
};
if(!file.isEmpty())
{
auto button = QMessageBox::question(this, tr("Apply stretch?"), tr("Apply current stretch function to image?"));
QString format = filterToFormat(file, selectedFilter);
if(format == "fits" || format == "xisf")
{
convert(file, format);
}
else
{
QImage img = m_image->renderToImage();
if(!img.isNull())
img.save(file, filterToFormat(file, selectedFilter));
}
convert(file, format, button == QMessageBox::Yes);
}
}
void MainWindow::convert(const QString &outfile, const QString &format)
void MainWindow::convert(const QString &outfile, const QString &format, bool stretch)
{
QString file = m_ringList->currentImage()->name();
QThreadPool::globalInstance()->start(new ConvertRunable(file, outfile, format));
ConvertRunable::ConvertParams param;
param.stretch = stretch;
param.mtf = m_stretchPanel->params();
QThreadPool::globalInstance()->start(new ConvertRunable(file, outfile, format, param));
}
void MainWindow::markImage()
@@ -804,7 +835,7 @@ void MainWindow::checkNewVersion()
if(QMessageBox::question(this, tr("Update check"), tr("New version %1 is available. Do you want to download it now?").arg(tag)) == QMessageBox::Yes)
{
QUrl url(json.object().value("html_url").toString());
qDebug() << url;
qDebug() << "Opening url" << url;
if(url.host() == "gitea.nouspiro.space")
QDesktopServices::openUrl(url);
}
@@ -829,6 +860,20 @@ void MainWindow::openFileManager()
#endif
}
void MainWindow::runScript(const QString &script, const QString &outdir, const QStringList &paths, const QString &arg, bool exit)
{
BatchProcessing *batchProcessing = new BatchProcessing(m_database, this);
batchProcessing->setOutputDir(outdir);
batchProcessing->setPaths(paths);
if(exit)batchProcessing->hide();
QTimer::singleShot(500, [batchProcessing, script, exit, arg](){
batchProcessing->runScript(script, arg, exit);
batchProcessing->exec();
delete batchProcessing;
if(exit)QCoreApplication::exit();
});
}
void MainWindow::updateWindowTitle()
{
ImagePtr ptr = m_ringList->currentImage();
+4 -1
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;
@@ -52,7 +54,7 @@ public slots:
void indexDir(const QString &dir);
void reindex();
void saveAs();
void convert(const QString &outfile, const QString &format);
void convert(const QString &outfile, const QString &format, bool stretch);
void markImage();
void unmarkImage();
void markAndNext();
@@ -69,6 +71,7 @@ public slots:
void exportCSV();
void checkNewVersion();
void openFileManager();
void runScript(const QString &script, const QString &outdir, const QStringList &paths, const QString &arg, bool exit);
};
#endif // MAINWINDOW_H
+22 -6
View File
@@ -1195,12 +1195,28 @@ void RawImage::applySTF(const MTFParam &mtfParams)
for(size_t i = 0; i < len; i++)
{
float x;
if constexpr(std::numeric_limits<std::remove_reference_t<decltype(*src)>>::is_integer)x = src[i] * iscale;
else x = src[i] * unit.first + unit.second;
x = (x - mtfParams.blackPoint[0]) / (mtfParams.whitePoint[0] - mtfParams.blackPoint[0]);
x = std::clamp(x, 0.0f, 1.0f);
x = ((mtfParams.midPoint[0] - 1.0f) * x) / ((2.0f * mtfParams.midPoint[0] - 1.0f) * x - mtfParams.midPoint[0]);
src[i] = x * s;
if(m_ch == 4)
{
size_t c = i & 0x3;
if(c < 3)
{
if constexpr(std::numeric_limits<std::remove_reference_t<decltype(*src)>>::is_integer)x = src[i] * iscale;
else x = src[i] * unit.first + unit.second;
x = (x - mtfParams.blackPoint[c]) / (mtfParams.whitePoint[c] - mtfParams.blackPoint[c]);
x = std::clamp(x, 0.0f, 1.0f);
x = ((mtfParams.midPoint[c] - 1.0f) * x) / ((2.0f * mtfParams.midPoint[c] - 1.0f) * x - mtfParams.midPoint[c]);
src[i] = x * s;
}
}
else
{
if constexpr(std::numeric_limits<std::remove_reference_t<decltype(*src)>>::is_integer)x = src[i] * iscale;
else x = src[i] * unit.first + unit.second;
x = (x - mtfParams.blackPoint[0]) / (mtfParams.whitePoint[0] - mtfParams.blackPoint[0]);
x = std::clamp(x, 0.0f, 1.0f);
x = ((mtfParams.midPoint[0] - 1.0f) * x) / ((2.0f * mtfParams.midPoint[0] - 1.0f) * x - mtfParams.midPoint[0]);
src[i] = x * s;
}
}
};
+70 -8
View File
@@ -4,6 +4,7 @@
#include <QDebug>
#include <QInputDialog>
#include <QJsonValue>
#include <QJSValueIterator>
#include "loadrunable.h"
#include "rawimage.h"
#include "loadimage.h"
@@ -37,10 +38,12 @@ ScriptEngine::ScriptEngine(Database *database, BatchProcessing *parent)
#endif // PLATESOLVER
}
void ScriptEngine::setParams(const QString &scriptPath, const QList<QPair<QString, QString>> &paths, const QString &outputDir)
void ScriptEngine::setParams(const QString &scriptPath, const QList<QPair<QString, QString>> &paths, const QString &outputDir, const QString &arg)
{
_scriptPath = scriptPath;
_paths = paths;
if(!arg.isNull())
_jsEngine->globalObject().setProperty("scriptarg", arg);
setPaths(paths);
_outputDir = outputDir + "/";
}
@@ -417,14 +420,73 @@ QJSValue ScriptEngine::newArray(uint size)
return _jsEngine->newArray(size);
}
void ScriptEngine::run()
QJSValue ScriptEngine::eval(const QString &program)
{
QJSValue jsPaths = _jsEngine->newArray(_paths.size());
for(qsizetype i=0; i<_paths.size(); i++)
jsPaths.setProperty(i, _jsEngine->newQObject(new File(_paths[i].first, _paths[i].second, this)));
QStringList stackTrace;
QJSValue result = _jsEngine->evaluate(program, QString(), 1, &stackTrace);
if(result.isError())
{
QString error = result.property("name").toString() + " on line " + result.property("lineNumber").toString() + " : " + result.toString();
error += "\n" + result.property("stack").toString();
emit newMessage(error, true);
}
else if(!result.isUndefined())
{
emit newMessage(result.toString(), false);
}
return result;
}
QStringList ScriptEngine::complete(const QString &line)
{
QStringList complete;
QJSValue globObj = _jsEngine->globalObject();
QRegularExpression reg("[a-zA-Z_][a-zA-Z0-9_]*");
auto match = reg.match(line);
if(match.hasMatch())
{
QString var = match.captured();
if(globObj.hasProperty(var))
{
complete.clear();
QJSValueIterator it(globObj.property(var));
while(it.hasNext())
{
it.next();
if(it.name() != "constructor" && it.name() != "objectNameChanged")
complete.append(var + "." + it.name());
}
}
}
else
{
QJSValueIterator it(globObj);
while(it.hasNext())
{
it.next();
complete.append(it.name());
}
}
return complete;
}
void ScriptEngine::setPaths(const QList<QPair<QString, QString> > &paths)
{
_paths = paths;
QJSValue jsPaths = _jsEngine->newArray(paths.size());
for(qsizetype i=0; i<paths.size(); i++)
jsPaths.setProperty(i, _jsEngine->newQObject(new File(paths[i].first, paths[i].second, this)));
_jsEngine->globalObject().setProperty("files", jsPaths);
}
void ScriptEngine::run()
{
QFile scriptFile(_scriptPath);
if(!scriptFile.open(QIODevice::ReadOnly))
{
@@ -905,9 +967,9 @@ ScriptEngineThread::~ScriptEngineThread()
if(_engine)_engine->interrupt();
}
void ScriptEngineThread::setParams(const QString &scriptPath, const QList<QPair<QString, QString>> &paths, const QString &outputDir)
void ScriptEngineThread::setParams(const QString &scriptPath, const QList<QPair<QString, QString>> &paths, const QString &outputDir, const QString &arg)
{
_engine->setParams(scriptPath, paths, outputDir);
_engine->setParams(scriptPath, paths, outputDir, arg);
}
void ScriptEngineThread::start()
+5 -2
View File
@@ -33,7 +33,7 @@ class ScriptEngine : public QObject
Solver *_solver = nullptr;
public:
explicit ScriptEngine(Database *database, BatchProcessing *parent = nullptr);
void setParams(const QString &scriptPath, const QList<QPair<QString, QString>> &paths, const QString &outputDir);
void setParams(const QString &scriptPath, const QList<QPair<QString, QString>> &paths, const QString &outputDir, const QString &arg);
void reportError(const QString &message);
const QString& outputDir() const;
void interrupt();
@@ -64,6 +64,9 @@ public:
#endif // PLATESOLVER
QJSValue newObject();
QJSValue newArray(uint size);
QJSValue eval(const QString &program);
QStringList complete(const QString &line);
void setPaths(const QList<QPair<QString, QString>> &paths);
public slots:
void run();
signals:
@@ -79,7 +82,7 @@ class ScriptEngineThread : public QObject
public:
ScriptEngineThread(Database *database, BatchProcessing *parent = nullptr);
~ScriptEngineThread();
void setParams(const QString &scriptPath, const QList<QPair<QString, QString>> &paths, const QString &outputDir);
void setParams(const QString &scriptPath, const QList<QPair<QString, QString>> &paths, const QString &outputDir, const QString &arg);
void start();
void interrupt();
signals:
+29 -1
View File
@@ -129,11 +129,30 @@ SettingsDialog::SettingsDialog(QWidget *parent) : QDialog(parent)
delete item;
});
m_lang = new QComboBox(this);
m_lang->addItems({"English", "Français", "Slovenčina", "Português"});
QString lang;
switch(QLocale().language())
{
default:
case QLocale::English: lang = "en"; break;
case QLocale::French: lang = "fr"; break;
case QLocale::Slovak: lang = "sk"; break;
case QLocale::Portuguese: lang = "pt_BR"; break;
}
lang = settings.value("settings/lang", lang).toString();
if(lang == "en")m_lang->setCurrentIndex(0);
else if(lang == "fr")m_lang->setCurrentIndex(1);
else if(lang == "sk")m_lang->setCurrentIndex(2);
else if(lang == "pt_BR")m_lang->setCurrentIndex(3);
layout->addRow(tr("Image preload count"), m_preloadImages);
layout->addRow(tr("Thumbnails size"), m_thumSize);
layout->addRow(tr("Saturation"), m_saturation);
layout->addRow(tr("Slideshow interval"), m_slideShowTime);
layout->addRow(tr("Image interpolation"), m_filtering);
layout->addRow(tr("Language"), m_lang);
layout->addRow(m_qualityThumbnail);
layout->addRow(m_useNativeDialog);
layout->addRow(m_bestFit);
@@ -150,7 +169,6 @@ SettingsDialog::SettingsDialog(QWidget *parent) : QDialog(parent)
#endif
//layout->addRow(new QLabel(tr("Changes in settings will take effect after program restart.")));
QDialogButtonBox *buttonBox = new QDialogButtonBox(this);
buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
@@ -237,4 +255,14 @@ void SettingsDialog::saveSettings()
}
settings.setValue("settings/headerhighlightkeywords", headerHighlight.keys());
settings.setValue("settings/headerhighlightcolors", colors);
QString lang;
int langIdx = m_lang->currentIndex();
switch(langIdx)
{
case 0: lang = "en"; break;
case 1: lang = "fr"; break;
case 2: lang = "sk"; break;
case 3: lang = "pt_BR"; break;
}
settings.setValue("settings/lang", lang);
}
+1
View File
@@ -32,6 +32,7 @@ private:
QListWidget *m_headerHighlight;
QColor m_color = Qt::yellow;
QLineEdit *m_keyword;
QComboBox *m_lang;
};
#endif // SETTINGSDIALOG_H
+5
View File
@@ -104,6 +104,11 @@ StretchToolbar::~StretchToolbar()
settings.setValue("stretchtoolbar/autostretch", m_autoStretchOnLoad->isChecked());
}
const MTFParam &StretchToolbar::params() const
{
return m_mtfParam;
}
void StretchToolbar::stretchImage(Image *img)
{
if(img && img->rawImage())
+1
View File
@@ -22,6 +22,7 @@ class StretchToolbar : public QToolBar
public:
explicit StretchToolbar(QWidget *parent = nullptr);
~StretchToolbar();
const MTFParam& params() const;
public slots:
void stretchImage(Image *img);
void resetMTF();
Binary file not shown.
+503 -444
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.
+499 -444
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.
File diff suppressed because it is too large Load Diff
Binary file not shown.
+503 -444
View File
File diff suppressed because it is too large Load Diff