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:
+78
-127
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user