rebrand to orc-gallery and optimise search/thumbnail responsiveness

Rebrand:
- Rename project, binary, desktop file, and icons from screenshot-gallery/orcs-gallery to orc-gallery
- Update display name to "ORC Gallery" throughout (window title, .desktop, README, plan.md)
- Config dir changed from ScreenshotOCRGallery to OrcGallery

Performance:
- cancelSearch() is now non-blocking; new searches start immediately instead of waiting for the previous thread to drain
- Remove double-emit from QFutureWatcher::finished handler; results emitted once directly from background thread
- SQLite WAL mode + NORMAL sync + 20 MB page cache + memory temp store + 256 MB mmap applied on init and per-thread connections
- Typing inactivity timer reduced from 500 ms to 150 ms
- COUNT(*) OVER() window function folds total-count into the main SELECT, eliminating a separate COUNT round-trip per search
- ImageThumbnail::paintEvent draws the filename overlay; removes 3 child QObjects (QFrame + QLabel + QHBoxLayout) per thumbnail
- loadThumbnailAsync posts QImage load+scale to QThreadPool; QPixmap::fromImage called on main thread via Qt::QueuedConnection invokeMethod; QPointer guards against use-after-free on gallery clear

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
yzinchuk
2026-07-03 03:53:07 -04:00
parent 3fb8d7e507
commit 55454d7ef6
23 changed files with 231 additions and 315 deletions
+78 -127
View File
@@ -20,28 +20,8 @@ DatabaseManager::DatabaseManager(QObject *parent)
m_allImagesCache.clear();
m_lastCacheUpdate = QDateTime::currentDateTime();
// Connect future watcher to handle search results
connect(&m_searchWatcher, &QFutureWatcher<void>::finished,
this, [this]() {
if (!m_searchCancelled) {
// Emit signal with the results only if not cancelled
QMutexLocker locker(&m_searchMutex);
QString searchText = m_currentSearchText;
int offset = m_currentOffset;
int limit = m_currentLimit;
if (!searchText.isEmpty()) {
QMutexLocker cacheLocker(&m_cacheMutex);
if (m_searchCache.contains(searchText) &&
m_searchCache[searchText].contains(qMakePair(offset, limit))) {
SearchCacheItem cacheItem = m_searchCache[searchText][qMakePair(offset, limit)];
emit searchResultsReady(cacheItem.results, searchText,
offset, limit, cacheItem.totalCount);
}
}
}
});
// Watcher is used only for lifecycle tracking; results are emitted directly
// from performSearchInBackground to avoid double-emit.
// Clean cache periodically
QTimer *cleanupTimer = new QTimer(this);
@@ -51,9 +31,14 @@ DatabaseManager::DatabaseManager(QObject *parent)
DatabaseManager::~DatabaseManager()
{
// Cancel any ongoing search and wait for it to finish
cancelSearch();
// Signal cancellation and wait for the background thread to drain cleanly.
{
QMutexLocker locker(&m_searchMutex);
m_searchCancelled = true;
}
if (m_searchFuture.isRunning())
m_searchFuture.waitForFinished();
if (m_db.isOpen()) {
m_db.close();
}
@@ -182,6 +167,17 @@ bool DatabaseManager::initialize(const QString &dbPath)
m_ftsEnabled = false;
}
// Performance pragmas applied once on the main connection.
// WAL allows concurrent reads alongside the OCR writer.
// cache_size=-20000 sets a 20 MB page cache.
// mmap_size enables memory-mapped I/O for faster sequential reads.
QSqlQuery pragmaQuery;
pragmaQuery.exec("PRAGMA journal_mode=WAL");
pragmaQuery.exec("PRAGMA synchronous=NORMAL");
pragmaQuery.exec("PRAGMA cache_size=-20000");
pragmaQuery.exec("PRAGMA temp_store=MEMORY");
pragmaQuery.exec("PRAGMA mmap_size=268435456");
m_initialized = true;
qDebug() << "Database initialized successfully.";
return true;
@@ -318,9 +314,12 @@ QSqlDatabase DatabaseManager::getDatabaseConnection()
if (!threadDb.open()) {
qDebug() << "Failed to open database in thread:" << threadDb.lastError().text();
} else {
// Enable foreign keys in this connection
QSqlQuery query(threadDb);
query.exec("PRAGMA foreign_keys = ON");
query.exec("PRAGMA journal_mode=WAL");
query.exec("PRAGMA synchronous=NORMAL");
query.exec("PRAGMA cache_size=-20000");
query.exec("PRAGMA temp_store=MEMORY");
query.exec("PRAGMA mmap_size=268435456");
}
return threadDb;
@@ -397,16 +396,11 @@ void DatabaseManager::searchImages(const QString &searchText, int offset, int li
void DatabaseManager::cancelSearch()
{
// Set cancelled flag
// Non-blocking: just set the flag. The background thread checks it at
// multiple points and exits early. The new search starts immediately.
QMutexLocker locker(&m_searchMutex);
m_searchCancelled = true;
m_currentSearchText.clear();
locker.unlock();
// Wait for any running search to complete
if (m_searchFuture.isRunning()) {
m_searchFuture.waitForFinished();
}
}
void DatabaseManager::performSearchInBackground(const QString &searchText, int offset, int limit)
@@ -428,34 +422,8 @@ void DatabaseManager::performSearchInBackground(const QString &searchText, int o
return;
}
}
// First, get total count for pagination info
QSqlQuery countQuery(threadDb);
int totalCount = 0;
if (m_ftsEnabled) {
QString ftsQuery = prepareFTSQuery(searchText);
countQuery.prepare("SELECT COUNT(*) FROM ocr_results r "
"JOIN ocr_fts f ON r.id = f.rowid "
"WHERE ocr_fts MATCH :query");
countQuery.bindValue(":query", ftsQuery);
} else {
if (searchText.length() <= 3) {
countQuery.prepare("SELECT COUNT(*) FROM ocr_results WHERE ocr_text LIKE :search");
countQuery.bindValue(":search", "%" + searchText + "%");
} else {
countQuery.prepare("SELECT COUNT(*) FROM ocr_results WHERE ocr_text LIKE :search OR ocr_text LIKE :wordstart");
countQuery.bindValue(":search", "%" + searchText + "%");
countQuery.bindValue(":wordstart", "% " + searchText + "%");
}
}
if (countQuery.exec() && countQuery.next()) {
totalCount = countQuery.value(0).toInt();
} else {
qDebug() << "Failed to get count:" << countQuery.lastError().text();
totalCount = 0;
}
// Check if search was cancelled before main query
{
QMutexLocker locker(&m_searchMutex);
@@ -463,24 +431,24 @@ void DatabaseManager::performSearchInBackground(const QString &searchText, int o
return;
}
}
// Start transaction to speed up query
threadDb.transaction();
// Single query: COUNT(*) OVER() gives the total match count without a
// separate round-trip. SQLite evaluates window functions before LIMIT, so
// the count reflects the full result set even when we page with LIMIT/OFFSET.
threadDb.transaction();
QSqlQuery query(threadDb);
if (m_ftsEnabled) {
// Use FTS5 virtual table for much faster text search
QString ftsQuery = prepareFTSQuery(searchText);
QString queryStr = "SELECT r.id, r.full_path, r.ocr_text FROM ocr_results r "
"JOIN ocr_fts f ON r.id = f.rowid "
"WHERE ocr_fts MATCH :query "
"ORDER BY rank";
if (limit > 0) {
QString queryStr =
"SELECT r.id, r.full_path, r.ocr_text, COUNT(*) OVER() AS total_count "
"FROM ocr_results r "
"JOIN ocr_fts f ON r.id = f.rowid "
"WHERE ocr_fts MATCH :query "
"ORDER BY rank";
if (limit > 0)
queryStr += " LIMIT :limit OFFSET :offset";
}
query.prepare(queryStr);
query.bindValue(":query", ftsQuery);
if (limit > 0) {
@@ -488,57 +456,40 @@ void DatabaseManager::performSearchInBackground(const QString &searchText, int o
query.bindValue(":offset", offset);
}
} else {
// Fallback to LIKE queries if FTS is not available
// Optimize the query based on length of search text
if (searchText.length() <= 3) {
// For short search terms, use a more targeted approach
QString queryStr = "SELECT id, full_path, ocr_text FROM ocr_results WHERE ocr_text LIKE :search "
"ORDER BY id";
if (limit > 0) {
queryStr += " LIMIT :limit OFFSET :offset";
}
query.prepare(queryStr);
query.bindValue(":search", "%" + searchText + "%");
if (limit > 0) {
query.bindValue(":limit", limit);
query.bindValue(":offset", offset);
}
} else {
// For longer search terms, use LIKE with a more specific pattern at start
QString queryStr = "SELECT id, full_path, ocr_text FROM ocr_results WHERE ocr_text LIKE :search OR ocr_text LIKE :wordstart "
"ORDER BY id";
if (limit > 0) {
queryStr += " LIMIT :limit OFFSET :offset";
}
query.prepare(queryStr);
query.bindValue(":search", "%" + searchText + "%");
QString whereClause = (searchText.length() <= 3)
? "ocr_text LIKE :search"
: "ocr_text LIKE :search OR ocr_text LIKE :wordstart";
QString queryStr =
"SELECT id, full_path, ocr_text, COUNT(*) OVER() AS total_count "
"FROM ocr_results WHERE " + whereClause + " ORDER BY id";
if (limit > 0)
queryStr += " LIMIT :limit OFFSET :offset";
query.prepare(queryStr);
query.bindValue(":search", "%" + searchText + "%");
if (searchText.length() > 3)
query.bindValue(":wordstart", "% " + searchText + "%");
if (limit > 0) {
query.bindValue(":limit", limit);
query.bindValue(":offset", offset);
}
if (limit > 0) {
query.bindValue(":limit", limit);
query.bindValue(":offset", offset);
}
}
if (!query.exec()) {
qDebug() << "Failed to search images:" << query.lastError().text();
qDebug() << "Error details:" << query.lastError().databaseText();
qDebug() << "Search query failed:" << query.lastError().text();
threadDb.rollback();
// If FTS query failed, try fallback to LIKE
// FTS failure: fall back to LIKE for this request
if (m_ftsEnabled) {
qDebug() << "Trying fallback to LIKE query...";
qDebug() << "Falling back to LIKE query";
threadDb.transaction();
query.prepare("SELECT full_path, ocr_text FROM ocr_results WHERE ocr_text LIKE :search");
query.prepare(
"SELECT id, full_path, ocr_text, COUNT(*) OVER() AS total_count "
"FROM ocr_results WHERE ocr_text LIKE :search ORDER BY id");
query.bindValue(":search", "%" + searchText + "%");
if (!query.exec()) {
qDebug() << "Fallback query also failed:" << query.lastError().text();
qDebug() << "Fallback query failed:" << query.lastError().text();
threadDb.rollback();
return;
}
@@ -547,7 +498,7 @@ void DatabaseManager::performSearchInBackground(const QString &searchText, int o
}
}
// Check if search was cancelled
// Check cancellation again after query execution
{
QMutexLocker locker(&m_searchMutex);
if (m_searchCancelled || m_currentSearchText != searchText) {
@@ -556,11 +507,9 @@ void DatabaseManager::performSearchInBackground(const QString &searchText, int o
}
}
// Reserve space for results to avoid reallocations
images.reserve(query.size() > 0 ? query.size() : 100);
images.reserve(100);
while (query.next()) {
// Periodically check if search was cancelled
if (query.at() % 20 == 0) {
QMutexLocker locker(&m_searchMutex);
if (m_searchCancelled || m_currentSearchText != searchText) {
@@ -568,16 +517,18 @@ void DatabaseManager::performSearchInBackground(const QString &searchText, int o
return;
}
}
// Read total count from the window function column (same on every row)
if (totalCount == 0)
totalCount = query.value(3).toInt();
ImageItem item;
item.id = query.value(0).toInt();
item.id = query.value(0).toInt();
item.filePath = query.value(1).toString();
item.ocrText = query.value(2).toString();
// Only add images that have a non-empty path
if (!item.filePath.isEmpty()) {
item.ocrText = query.value(2).toString();
if (!item.filePath.isEmpty())
images.append(item);
}
}
threadDb.commit();
+56 -104
View File
@@ -24,14 +24,33 @@ ImageThumbnail::ImageThumbnail(const QString &filePath, QWidget *parent)
setLineWidth(2);
setScaledContents(false);
setCursor(Qt::PointingHandCursor);
// We're removing tooltips as requested
// Enable text wrapping and text interaction
setWordWrap(true);
setTextInteractionFlags(Qt::TextSelectableByMouse);
// Set a minimum size policy
setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum);
if (!filePath.isEmpty()) {
QString name = QFileInfo(filePath).fileName();
if (name.length() > 30)
name = name.left(13) + "..." + name.right(13);
m_displayName = name;
}
}
void ImageThumbnail::paintEvent(QPaintEvent *event)
{
QLabel::paintEvent(event);
if (m_displayName.isEmpty())
return;
QPainter painter(this);
const int overlayH = 22;
QRect overlayRect(0, height() - overlayH, width(), overlayH);
painter.fillRect(overlayRect, QColor(0, 0, 0, 178));
painter.setPen(Qt::white);
QFont f = painter.font();
f.setPointSize(9);
painter.setFont(f);
painter.drawText(overlayRect, Qt::AlignCenter, m_displayName);
}
void ImageThumbnail::mousePressEvent(QMouseEvent *event)
@@ -207,40 +226,10 @@ void ImageGallery::displayImages(const QList<DatabaseManager::ImageItem> &images
continue; // Skip this image
}
// Create and add thumbnail
QPixmap thumbnail = createThumbnail(item.filePath, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT);
ImageThumbnail *thumbnailLabel = new ImageThumbnail(item.filePath, this);
thumbnailLabel->setPixmap(thumbnail);
// Add filename overlay at the bottom of the thumbnail
QFileInfo fileNameInfo(item.filePath);
QString fileName = fileNameInfo.fileName();
// Create overlay container with dark background - store it as a property of the thumbnail
QFrame* overlay = new QFrame(thumbnailLabel);
overlay->setObjectName("filenameOverlay"); // Set object name for finding it later
overlay->setStyleSheet("background-color: rgba(0, 0, 0, 0.7);");
overlay->setFixedHeight(20);
// Create label for the filename
QLabel* fileNameLabel = new QLabel(fileName, overlay);
fileNameLabel->setStyleSheet("color: white; background: transparent;");
fileNameLabel->setAlignment(Qt::AlignCenter);
// Truncate filename if too long (more than 30 chars)
if (fileName.length() > 30) {
fileNameLabel->setText(fileName.left(13) + "..." + fileName.right(13));
}
// Layout for the overlay
QHBoxLayout* overlayLayout = new QHBoxLayout(overlay);
overlayLayout->setContentsMargins(5, 0, 5, 0);
overlayLayout->addWidget(fileNameLabel);
// Position the overlay at the bottom of the thumbnail
// Width and position will be adjusted in updateGridLayout()
overlay->setFixedWidth(thumbnailLabel->width());
overlay->move(0, THUMBNAIL_HEIGHT - overlay->height());
thumbnailLabel->setPixmap(createPlaceholderThumbnail(THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT, ""));
loadThumbnailAsync(thumbnailLabel, item.filePath);
// Connect the thumbnail click signal
connect(thumbnailLabel, &ImageThumbnail::thumbnailClicked,
this, &ImageGallery::handleThumbnailClicked);
@@ -546,26 +535,32 @@ void ImageGallery::handleThumbnailClicked(const QString &filePath)
QApplication::restoreOverrideCursor();
}
QPixmap ImageGallery::createThumbnail(const QString &filePath, int width, int height)
void ImageGallery::loadThumbnailAsync(ImageThumbnail *label, const QString &filePath)
{
// Verify file exists before attempting to load
QFileInfo fileInfo(filePath);
if (!fileInfo.exists() || !fileInfo.isReadable()) {
qDebug() << "Image file does not exist or is not readable:" << filePath;
return createPlaceholderThumbnail(width, height, "File not found");
}
// Try to load the image
QPixmap pixmap(filePath);
// If loading failed, create a placeholder
if (pixmap.isNull()) {
qDebug() << "Failed to load image:" << filePath;
return createPlaceholderThumbnail(width, height, "Failed to load");
}
// Scale pixmap while maintaining aspect ratio
return pixmap.scaled(width, height, Qt::KeepAspectRatio, Qt::SmoothTransformation);
// QImage loading and scaling are thread-safe; QPixmap creation must happen
// on the main thread. We do the heavy work in a background thread and marshal
// the result back via invokeMethod so the main thread never stalls on disk I/O.
QPointer<ImageThumbnail> ptr(label);
QThreadPool::globalInstance()->start([this, ptr, filePath]() {
QImage image;
QFileInfo fi(filePath);
if (fi.exists() && fi.isReadable()) {
image = QImage(filePath);
if (!image.isNull())
image = image.scaled(THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT,
Qt::KeepAspectRatio, Qt::SmoothTransformation);
}
QMetaObject::invokeMethod(this, [ptr, image]() {
if (!ptr) return;
if (image.isNull()) {
// File missing or unreadable — placeholder already shown, nothing to do
return;
}
ptr->setPixmap(QPixmap::fromImage(image));
}, Qt::QueuedConnection);
});
}
void ImageGallery::resizeEvent(QResizeEvent *event)
@@ -687,62 +682,19 @@ void ImageGallery::updateGridLayout()
m_containerWidget->adjustSize();
m_containerWidget->updateGeometry();
// Handle single column mode specially
if (m_columnsCount == 1) {
// Center the thumbnails in the viewport
int centeringMargin = (viewportWidth - THUMBNAIL_WIDTH) / 2;
centeringMargin = std::max(THUMBNAIL_SPACING, centeringMargin);
int centeringMargin = std::max(THUMBNAIL_SPACING, (viewportWidth - THUMBNAIL_WIDTH) / 2);
m_gridLayout->setContentsMargins(centeringMargin, THUMBNAIL_SPACING, centeringMargin, THUMBNAIL_SPACING);
// Let all thumbnails expand to fill available width in single column mode
for (auto thumbnail : m_thumbnails) {
thumbnail->setMaximumWidth(THUMBNAIL_WIDTH);
thumbnail->setAlignment(Qt::AlignCenter);
// Update the overlay to match thumbnail width
QFrame* overlay = thumbnail->findChild<QFrame*>("filenameOverlay");
if (overlay) {
overlay->setFixedWidth(THUMBNAIL_WIDTH);
overlay->move(0, THUMBNAIL_HEIGHT - overlay->height());
}
}
} else {
// For multi-column, use minimal margins
m_gridLayout->setContentsMargins(THUMBNAIL_SPACING, THUMBNAIL_SPACING, THUMBNAIL_SPACING, THUMBNAIL_SPACING);
// Reset thumbnail constraints
for (auto thumbnail : m_thumbnails) {
for (auto thumbnail : m_thumbnails)
thumbnail->setMaximumWidth(QWIDGETSIZE_MAX);
// Update the overlay to match thumbnail width in multi-column mode
QFrame* overlay = thumbnail->findChild<QFrame*>("filenameOverlay");
if (overlay) {
overlay->setFixedWidth(thumbnail->width());
overlay->move(0, THUMBNAIL_HEIGHT - overlay->height());
}
}
// For multi-column, let the container fill the viewport
m_containerWidget->setMinimumWidth(viewportWidth);
}
// Force update of overlay position and size after a short delay
// This ensures the overlays are sized correctly after all layout changes
QTimer::singleShot(50, this, [this]() {
for (auto thumbnail : m_thumbnails) {
QFrame* overlay = thumbnail->findChild<QFrame*>("filenameOverlay");
if (overlay) {
overlay->setFixedWidth(thumbnail->width());
overlay->move(0, THUMBNAIL_HEIGHT - overlay->height());
}
}
});
// Update the layout again after a short delay to handle edge cases
QTimer::singleShot(10, this, [this](){
m_gridLayout->update();
m_containerWidget->update();
});
}
}
}
+9 -5
View File
@@ -12,6 +12,8 @@
#include <QPushButton>
#include <QResizeEvent>
#include <QPushButton>
#include <QPointer>
#include <QThreadPool>
#include "databasemanager.h"
// Global constants
@@ -22,20 +24,22 @@ static const int THUMBNAIL_SPACING = 2; // Reduced spacing between thumbnails
class ImageThumbnail : public QLabel
{
Q_OBJECT
public:
explicit ImageThumbnail(const QString &filePath, QWidget *parent = nullptr);
QString getFilePath() const { return m_filePath; }
protected:
void mousePressEvent(QMouseEvent *event) override;
void paintEvent(QPaintEvent *event) override;
signals:
void thumbnailClicked(const QString &filePath);
private:
QString m_filePath;
QString m_displayName; // pre-truncated filename for painting
};
class ImageGallery : public QWidget
@@ -88,7 +92,7 @@ private:
QLabel *m_loadingIndicator;
QPushButton *m_loadMoreButton;
QPixmap createThumbnail(const QString &filePath, int width, int height);
void loadThumbnailAsync(ImageThumbnail *label, const QString &filePath);
QPixmap createPlaceholderThumbnail(int width, int height, const QString &message);
QString m_lastSearchQuery; // Track the last search text
+5 -5
View File
@@ -7,15 +7,15 @@ int main(int argc, char *argv[])
QApplication app(argc, argv);
// Set application properties
app.setApplicationName("screenshot-gallery");
app.setApplicationName("orc-gallery");
app.setApplicationVersion("1.0.0");
// Set application icon
QIcon appIcon;
appIcon.addFile(":/icons/orcs-gallery-64.png", QSize(64, 64));
appIcon.addFile(":/icons/orcs-gallery-128.png", QSize(128, 128));
appIcon.addFile(":/icons/orcs-gallery-256.png", QSize(256, 256));
appIcon.addFile(":/icons/orcs-gallery-512.png", QSize(512, 512));
appIcon.addFile(":/icons/orc-gallery-64.png", QSize(64, 64));
appIcon.addFile(":/icons/orc-gallery-128.png", QSize(128, 128));
appIcon.addFile(":/icons/orc-gallery-256.png", QSize(256, 256));
appIcon.addFile(":/icons/orc-gallery-512.png", QSize(512, 512));
app.setWindowIcon(appIcon);
// Create and show main window
+7 -7
View File
@@ -17,7 +17,7 @@
#include <stdexcept>
// Define settings file path
const QString MainWindow::CONFIG_FILE_PATH = QDir::homePath() + "/.config/ScreenshotOCRGallery/settings.ini";
const QString MainWindow::CONFIG_FILE_PATH = QDir::homePath() + "/.config/OrcGallery/settings.ini";
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
@@ -32,15 +32,15 @@ MainWindow::MainWindow(QWidget *parent)
, m_imagePreloadCount(DEFAULT_PRELOAD_COUNT)
{
// Set window title and size
setWindowTitle(tr("Screenshot OCR Gallery"));
setWindowTitle(tr("ORC Gallery"));
resize(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT);
// Set window icon
QIcon appIcon;
appIcon.addFile(":/icons/orcs-gallery-64.png", QSize(64, 64));
appIcon.addFile(":/icons/orcs-gallery-128.png", QSize(128, 128));
appIcon.addFile(":/icons/orcs-gallery-256.png", QSize(256, 256));
appIcon.addFile(":/icons/orcs-gallery-512.png", QSize(512, 512));
appIcon.addFile(":/icons/orc-gallery-64.png", QSize(64, 64));
appIcon.addFile(":/icons/orc-gallery-128.png", QSize(128, 128));
appIcon.addFile(":/icons/orc-gallery-256.png", QSize(256, 256));
appIcon.addFile(":/icons/orc-gallery-512.png", QSize(512, 512));
setWindowIcon(appIcon);
// Remove fixed minimum size to allow for single column layout at any width
@@ -66,7 +66,7 @@ MainWindow::MainWindow(QWidget *parent)
// Initialize typing inactivity timer
m_typingTimer->setSingleShot(true);
m_typingTimer->setInterval(500); // 500ms of no typing before searching
m_typingTimer->setInterval(150);
connect(m_typingTimer, &QTimer::timeout, this, &MainWindow::handleTypingInactivityTimeout);
// Only create animation timer for searching visual feedback
+1 -1
View File
@@ -8,7 +8,7 @@
// Define static constants
const QString SettingsDialog::DEFAULT_SCREENSHOTS_DIR = QDir::homePath() + "/Screenshots";
const QString SettingsDialog::DEFAULT_DATABASE_FILENAME = "screenshot_ocr.db";
const QString SettingsDialog::CONFIG_FILE_PATH = QDir::homePath() + "/.config/ScreenshotOCRGallery/settings.ini";
const QString SettingsDialog::CONFIG_FILE_PATH = QDir::homePath() + "/.config/OrcGallery/settings.ini";
SettingsDialog::SettingsDialog(QWidget *parent)
: QDialog(parent)