From dff125a451478ba91d72c260876ed47c6b5e6afa Mon Sep 17 00:00:00 2001 From: Lukas Spies Date: Sun, 1 Feb 2015 00:33:24 -0330 Subject: [PATCH] start of QML port - only real feature so far: thumbnails (almost complete) --- CMake/FindMagick.cmake | 45 ++ CMakeLists.txt | 84 ++++ build/.gitignore | 4 + cplusplus/handlefiles/loaddir.cpp | 219 ++++++++++ cplusplus/handlefiles/loaddir.h | 45 ++ cplusplus/imageprovider/imageproviderfull.cpp | 404 ++++++++++++++++++ cplusplus/imageprovider/imageproviderfull.h | 41 ++ .../imageprovider/imageproviderthumbnail.cpp | 215 ++++++++++ .../imageprovider/imageproviderthumbnail.h | 35 ++ cplusplus/main.cpp | 17 + cplusplus/mainwindow.cpp | 148 +++++++ cplusplus/mainwindow.h | 50 +++ cplusplus/settings/settings.cpp | 15 + cplusplus/settings/settings.h | 25 ++ cplusplus/variables.h | 20 + img.qrc | 6 + img/emptythumb.png | Bin 0 -> 2067 bytes img/nothumb.png | Bin 0 -> 584 bytes qml/Shortcuts.qml | 35 ++ qml/mainview/Background.qml | 46 ++ qml/mainview/Display.qml | 9 + qml/mainview/QuickInfo.qml | 143 +++++++ qml/mainwindow.qml | 88 ++++ qml/slidein/MetaData.qml | 9 + qml/slidein/ThumbnailBar.qml | 357 ++++++++++++++++ qrc.qrc | 11 + 26 files changed, 2071 insertions(+) create mode 100644 CMake/FindMagick.cmake create mode 100644 CMakeLists.txt create mode 100644 build/.gitignore create mode 100644 cplusplus/handlefiles/loaddir.cpp create mode 100644 cplusplus/handlefiles/loaddir.h create mode 100644 cplusplus/imageprovider/imageproviderfull.cpp create mode 100644 cplusplus/imageprovider/imageproviderfull.h create mode 100644 cplusplus/imageprovider/imageproviderthumbnail.cpp create mode 100644 cplusplus/imageprovider/imageproviderthumbnail.h create mode 100644 cplusplus/main.cpp create mode 100644 cplusplus/mainwindow.cpp create mode 100644 cplusplus/mainwindow.h create mode 100644 cplusplus/settings/settings.cpp create mode 100644 cplusplus/settings/settings.h create mode 100644 cplusplus/variables.h create mode 100644 img.qrc create mode 100644 img/emptythumb.png create mode 100644 img/nothumb.png create mode 100644 qml/Shortcuts.qml create mode 100644 qml/mainview/Background.qml create mode 100644 qml/mainview/Display.qml create mode 100644 qml/mainview/QuickInfo.qml create mode 100644 qml/mainwindow.qml create mode 100644 qml/slidein/MetaData.qml create mode 100644 qml/slidein/ThumbnailBar.qml create mode 100644 qrc.qrc diff --git a/CMake/FindMagick.cmake b/CMake/FindMagick.cmake new file mode 100644 index 000000000..1c1b5e65a --- /dev/null +++ b/CMake/FindMagick.cmake @@ -0,0 +1,45 @@ +#-*-cmake-*- +# +# Test for GraphicsMagick libraries, unlike CMake's FindGraphicsMagick.cmake which +# tests for GraphicsMagick's binary utilities +# +# Once loaded this will define +# MAGICK++_FOUND - system has GraphicsMagick +# MAGICK++_INCLUDE_DIR - include directory for GraphicsMagick +# MAGICK++_LIBRARY_DIR - library directory for GraphicsMagick +# MAGICK++_LIBRARIES - libraries you need to link to +# + +SET(MAGICK++_FOUND "NO" ) + +FIND_PATH(MAGICK++_INCLUDE_DIR Magick++.h + "$ENV{MAGICK_LOCATION}/Magick++" + "$ENV{MAGICK_LOCATION}/include/Magick++" + "$ENV{MAGICK_LOCATION}/include/" + "$ENV{MAGICK_LOCATION}" + /usr/include/Magick++ + /usr/include/GraphicsMagick + /usr/include/ + /opt/local/include/GraphicsMagick/Magick++ + /opt/local/include/GraphicsMagick +) + +FIND_LIBRARY(Magick++ GraphicsMagick++ PATHS + "$ENV{MAGICK_LOCATION}/.libs" + "$ENV{MAGICK_LOCATION}/lib" + /opt/local/lib + DOC "GraphicsMagick Magick++ library" +) + +SET(MAGICK++_LIBRARIES ${Magick++} ) + +IF(MAGICK++_INCLUDE_DIR) + IF(MAGICK++_LIBRARIES) + SET(MAGICK++_FOUND "YES") + MESSAGE(STATUS "GraphicsMagick found at: ${MAGICK++_INCLUDE_DIR}") + GET_FILENAME_COMPONENT(MAGICK++_LIBRARY_DIR ${Magick++} PATH) + ENDIF(MAGICK++_LIBRARIES) +ENDIF(MAGICK++_INCLUDE_DIR) + +##### + diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 000000000..85170dec6 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,84 @@ +################################################## +# CMakeLists for PhotoQt-QML: http://photoqt.org # +################################################## + +CMAKE_MINIMUM_REQUIRED(VERSION 2.8) +PROJECT(photoqt) + +#### SOURCES #### +SET(photoqt_SOURCES cplusplus/main.cpp cplusplus/mainwindow.cpp cplusplus/variables.h) +SET(photoqt_SOURCES ${photoqt_SOURCES} cplusplus/handlefiles/loaddir.cpp) +SET(photoqt_SOURCES ${photoqt_SOURCES} cplusplus/imageprovider/imageproviderfull.cpp cplusplus/imageprovider/imageproviderthumbnail.cpp) +SET(photoqt_SOURCES ${photoqt_SOURCES} cplusplus/settings/settings.cpp) +SET(photoqt_SOURCES ${photoqt_SOURCES} qml/mainwindow.qml qml/slidein/ThumbnailBar.qml qml/mainview/Background.qml qml/mainview/Display.qml qml/mainview/QuickInfo.qml qml/slidein/MetaData.qml qml/Shortcuts.qml) + +#### HEADER #### +SET(photoqt_HEADERS cplusplus/mainwindow.h cplusplus/variables.h) +SET(photoqt_HEADERS ${photoqt_HEADERS} cplusplus/handlefiles/loaddir.h) +SET(photoqt_HEADERS ${photoqt_HEADERS} cplusplus/imageprovider/imageproviderthumbnail.h cplusplus/imageprovider/imageproviderfull.h) +SET(photoqt_HEADERS ${photoqt_HEADERS} cplusplus/settings/settings.h) + + +############################################# +#### OPTIONS THAT CAN BE SET BY THE USER #### +############################################# + +OPTION(EXIV2 "Use exiv2 library" ON) +OPTION(GM "Use graphicsmagick library" ON) +OPTION(QTONLY "ONLY USE QT-ONLY FEATURES" OFF) + + +################################ +#### FIND REQUIRED PACKAGES #### +################################ + +FIND_PACKAGE(Qt5 COMPONENTS Quick Widgets Core Svg REQUIRED) +SET(CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/CMake" ${CMAKE_MODULE_PATH}) +FIND_PACKAGE(Magick) + +SET(CMAKE_BUILD_TYPE "Debug") + + +#################################### +#### TRANSLATIONS AND RESOURCES #### +#################################### + +# Set resources +SET(photoqt_RESOURCES qrc.qrc img.qrc) +QT5_ADD_RESOURCES(RESOURCES ${photoqt_RESOURCES}) + + +###################### +#### FINISHING UP #### +###################### + +SET(CMAKE_BUILD_TYPE Debug) + +# moc files +QT5_WRAP_CPP(photoqt_HEADERS_MOC ${photoqt_HEADERS}) + +# include, add defiunitions and include dirs +INCLUDE_DIRECTORIES(${Qt5Widgets_INCLUDES}) +ADD_DEFINITIONS(${Qt5Widgets_DEFINITIONS}) +INCLUDE_DIRECTORIES(${CMAKE_CURRENT_BINARY_DIR}) + +# And add the executeable +add_executable(photoqt ${photoqt_SOURCES} ${photoqt_HEADERS_MOC} ${RESOURCES}) +qt5_use_modules(photoqt Quick Widgets Sql Svg Core) + + +IF(GM) + IF(NOT ${MAGICK++_FOUND}) + MESSAGE(FATAL_ERROR "** Unable to locate GraphicsMagick... is it installed?") + ELSEIF(${MAGICK++_FOUND}) + INCLUDE_DIRECTORIES(${MAGICK++_INCLUDE_DIR}) + TARGET_LINK_LIBRARIES(photoqt "GraphicsMagick++") + ADD_DEFINITIONS(-DGM) + MESSAGE("** Graphicsmagick enabled") + ENDIF(NOT ${MAGICK++_FOUND}) +ELSEIF(NOT GM) + MESSAGE("** Graphicsmagick DISABLED") +ENDIF(GM) + + + diff --git a/build/.gitignore b/build/.gitignore new file mode 100644 index 000000000..5e7d2734c --- /dev/null +++ b/build/.gitignore @@ -0,0 +1,4 @@ +# Ignore everything in this directory +* +# Except this file +!.gitignore diff --git a/cplusplus/handlefiles/loaddir.cpp b/cplusplus/handlefiles/loaddir.cpp new file mode 100644 index 000000000..87481b18e --- /dev/null +++ b/cplusplus/handlefiles/loaddir.cpp @@ -0,0 +1,219 @@ +#include "loaddir.h" + +LoadDir::LoadDir() : QObject() { + settings = new QSettings(QDir::homePath() + "/.photoqt/settings",QSettings::IniFormat); +} + + +QFileInfoList LoadDir::loadDir(QByteArray filepath) { + + currentfile = filepath; + counttot = 0; + + QDir dir(QFileInfo(filepath).absolutePath()); + + // These are the images known by PhotoQt + QStringList flt = QStringList() << "*.jpg" << "*.png" << "*.jpeg"; + dir.setNameFilters(flt); + + + // Store a QFileInfoList and a QStringList with the filenames + allImgsInfo = dir.entryInfoList(QDir::Files,QDir::IgnoreCase); + + // When opening an unknown file (i.e., one that doesn't match any set format), then we need to manually add it to the list of loaded images + if(!allImgsInfo.contains(QFileInfo(currentfile))) allImgsInfo.append(QFileInfo(currentfile)); + + // Sort images... + bool asc = settings->value("Behaviour/SortImagesAscending").toBool(); + QString sortby = settings->value("Behaviour/SortImagesBy").toString(); + if(sortby == "name") { + qDebug() << "sortby: name"; + std::sort(allImgsInfo.begin(),allImgsInfo.end(),(asc ? sort_name : sort_name_desc)); + } + if(sortby == "naturalname") { + qDebug() << "sortby: natural name"; + std::sort(allImgsInfo.begin(),allImgsInfo.end(),(asc ? sort_naturalname : sort_naturalname_desc)); + } + if(sortby == "date") { + qDebug() << "sortby: date"; + std::sort(allImgsInfo.begin(),allImgsInfo.end(),(asc ? sort_date : sort_date_desc)); + } + if(sortby == "size") { + qDebug() << "sortby: size"; + std::sort(allImgsInfo.begin(),allImgsInfo.end(),(asc ? sort_size : sort_size_desc)); + } + + // Storing number of images + counttot = allImgsInfo.length(); + + return allImgsInfo; + +} + + +// FOR SORTING, WE HAVE ALL FUNCTIONS FOR DESCENDING AND ASCENDING CASE, AS THIS IS FASTER THAN REVERSING THE ORDER +// AFTERWARDS (PARTICULARLY FOR DIRECTORIES WITH A LARGE NUMBER OF FILES + +bool LoadDir::sort_name(const QFileInfo &s1fileinfo, const QFileInfo &s2fileinfo) { + return(s1fileinfo.fileName().compare(s2fileinfo.fileName(), Qt::CaseInsensitive) <= 0); +} +bool LoadDir::sort_name_desc(const QFileInfo &s1fileinfo, const QFileInfo &s2fileinfo) { + return(s2fileinfo.fileName().compare(s1fileinfo.fileName(), Qt::CaseInsensitive) <= 0); +} +bool LoadDir::sort_date(const QFileInfo &s1fileinfo, const QFileInfo &s2fileinfo) { + return(s1fileinfo.created().secsTo(s2fileinfo.created()) >= 0); +} +bool LoadDir::sort_date_desc(const QFileInfo &s1fileinfo, const QFileInfo &s2fileinfo) { + return(s1fileinfo.created().secsTo(s2fileinfo.created()) < 0); +} +bool LoadDir::sort_size(const QFileInfo &s1fileinfo, const QFileInfo &s2fileinfo) { + return(s1fileinfo.size() >= s2fileinfo.size()); +} +bool LoadDir::sort_size_desc(const QFileInfo &s1fileinfo, const QFileInfo &s2fileinfo) { + return(s1fileinfo.size() < s2fileinfo.size()); +} + +// Algorithm used for sorting a directory using natural sort +// Credits to: http://www.qtcentre.org/archive/index.php/t-21411.html +bool LoadDir::sort_naturalname(const QFileInfo& s1fileinfo,const QFileInfo& s2fileinfo) { + + const QString s1 = s1fileinfo.fileName(); + const QString s2 = s2fileinfo.fileName(); + + // ignore common prefix.. + int i = 0; + + while ((i < s1.length()) && (i < s2.length()) && (s1.at(i).toLower() == s2.at(i).toLower())) + ++i; + ++i; + + // something left to compare? + if ((i < s1.length()) && (i < s2.length())) { + + // get number prefix from position i - doesnt matter from which string + int k = i-1; + + //If not number return native comparator + if(!s1.at(k).isNumber() || !s2.at(k).isNumber()) { + + //Two next lines + //E.g. 1_... < 12_... + if(s1.at(k).isNumber()) + return false; + if(s2.at(k).isNumber()) + return true; + return QString::compare(s1, s2, Qt::CaseSensitive) < 0; + } + + QString n = ""; + k--; + + while ((k >= 0) && (s1.at(k).isNumber())) { + n = s1.at(k)+n; + --k; + } + + // get relevant/signficant number string for s1 + k = i-1; + QString n1 = ""; + while ((k < s1.length()) && (s1.at(k).isNumber())) { + n1 += s1.at(k); + ++k; + } + + // get relevant/signficant number string for s2 + //Decrease by + k = i-1; + QString n2 = ""; + while ((k < s2.length()) && (s2.at(k).isNumber())) { + n2 += s2.at(k); + ++k; + } + + // got two numbers to compare? + if (!n1.isEmpty() && !n2.isEmpty()) + return (n+n1).toInt() < (n+n2).toInt(); + else { + // not a number has to win over a number.. number could have ended earlier... same prefix.. + if (!n1.isEmpty()) + return false; + if (!n2.isEmpty()) + return true; + return s1.at(i) < s2.at(i); + } + } else { + // shortest string wins + return s1.length() < s2.length(); + } +} +bool LoadDir::sort_naturalname_desc(const QFileInfo& s1fileinfo,const QFileInfo& s2fileinfo) { + + const QString s2 = s1fileinfo.fileName(); + const QString s1 = s2fileinfo.fileName(); + + // ignore common prefix.. + int i = 0; + + while ((i < s1.length()) && (i < s2.length()) && (s1.at(i).toLower() == s2.at(i).toLower())) + ++i; + ++i; + + // something left to compare? + if ((i < s1.length()) && (i < s2.length())) { + + // get number prefix from position i - doesnt matter from which string + int k = i-1; + + //If not number return native comparator + if(!s1.at(k).isNumber() || !s2.at(k).isNumber()) { + + //Two next lines + //E.g. 1_... < 12_... + if(s1.at(k).isNumber()) + return false; + if(s2.at(k).isNumber()) + return true; + return QString::compare(s1, s2, Qt::CaseSensitive) < 0; + } + + QString n = ""; + k--; + + while ((k >= 0) && (s1.at(k).isNumber())) { + n = s1.at(k)+n; + --k; + } + + // get relevant/signficant number string for s1 + k = i-1; + QString n1 = ""; + while ((k < s1.length()) && (s1.at(k).isNumber())) { + n1 += s1.at(k); + ++k; + } + + // get relevant/signficant number string for s2 + //Decrease by + k = i-1; + QString n2 = ""; + while ((k < s2.length()) && (s2.at(k).isNumber())) { + n2 += s2.at(k); + ++k; + } + + // got two numbers to compare? + if (!n1.isEmpty() && !n2.isEmpty()) + return (n+n1).toInt() < (n+n2).toInt(); + else { + // not a number has to win over a number.. number could have ended earlier... same prefix.. + if (!n1.isEmpty()) + return false; + if (!n2.isEmpty()) + return true; + return s1.at(i) < s2.at(i); + } + } else { + // shortest string wins + return s1.length() < s2.length(); + } +} diff --git a/cplusplus/handlefiles/loaddir.h b/cplusplus/handlefiles/loaddir.h new file mode 100644 index 000000000..7aa54d922 --- /dev/null +++ b/cplusplus/handlefiles/loaddir.h @@ -0,0 +1,45 @@ +#ifndef LOADDIR_H +#define LOADDIR_H + +#include +#include +#include +#include +#include +#include +#include + +class MyCppModel; + +class LoadDir : public QObject { + + Q_OBJECT + +public: + explicit LoadDir(); + + QFileInfoList loadDir(QByteArray filepath); + +private: + + QSettings *settings; + + QString currentfile; + QStringList imageFilter; + + int counttot; + int countpos; + QFileInfoList allImgsInfo; + + static bool sort_name(const QFileInfo &s1fileinfo, const QFileInfo &s2fileinfo); + static bool sort_name_desc(const QFileInfo &s1fileinfo, const QFileInfo &s2fileinfo); + static bool sort_naturalname(const QFileInfo &s1fileinfo, const QFileInfo &s2fileinfo); + static bool sort_naturalname_desc(const QFileInfo &s1fileinfo, const QFileInfo &s2fileinfo); + static bool sort_date(const QFileInfo &s1fileinfo, const QFileInfo &s2fileinfo); + static bool sort_date_desc(const QFileInfo &s1fileinfo, const QFileInfo &s2fileinfo); + static bool sort_size(const QFileInfo &s1fileinfo, const QFileInfo &s2fileinfo); + static bool sort_size_desc(const QFileInfo &s1fileinfo, const QFileInfo &s2fileinfo); + +}; + +#endif // LOADDIR_H diff --git a/cplusplus/imageprovider/imageproviderfull.cpp b/cplusplus/imageprovider/imageproviderfull.cpp new file mode 100644 index 000000000..46cf91cb5 --- /dev/null +++ b/cplusplus/imageprovider/imageproviderfull.cpp @@ -0,0 +1,404 @@ +#include "imageproviderfull.h" + +ImageProviderFull::ImageProviderFull() : QQuickImageProvider(QQuickImageProvider::Image) { + + verbose = false; + + settingsPerSession = new QSettings("photoqt_session"); + + gmfiles = "*.avs,*.x,*.cals,*.cal,*.dcl,*.ras,*.cin,*.cut,*.acr,*.dcm,*.dicom,*.dic,*.dcx,*.dib,*.dpx,*.epdf,*.fax,*.fits,*.fts,*.fit,*.fpx,*.jng,*.mat,*.miff,*.mono,*.mtv,*.otb,*.p7,*.palm,*.pam,*.pcd,*.pcds,*.pcx,*.pdb,*.pict,*.pct,*.pic,*.pix,*.pal,*.pnm,*.psd,*.ptif,*.ptiff,*.sfw,*.sgi,*.sun,*.tga,*.txt,*.vicar,*.viff,*.wpg,*.xwd"; + qtfiles = "*.bmp,*.bitmap,*.dds,*.gif,*.tif,*.tiff,*.jpeg2000,*.jp2,*.jpc,*.j2k,*.jpf,*.jpx,*.jpm,*.mj2,*.mng,*.ico,*.icns,*.jpeg,*.jpg,*.png,*.pbm,*.pgm,*.ppm,*.svg,*.svgz,*.wbmp,*.webp,*.xbm,*.xpm,*.fil"; + extrasfiles = ""; + +} + +QImage ImageProviderFull::requestImage(const QString &filename_encoded, QSize *size, const QSize &requestedSize) { + + QString filename = QByteArray::fromPercentEncoding(filename_encoded.toUtf8()); + + if(requestedSize.width() > 20 || requestedSize.height() > 20) + maxSize = requestedSize; + else + maxSize = settingsPerSession->value("curSize").toSize(); + +// if(verbose) std::clog << "[reader] zoomed: " << zoomed << std::endl; + + // Which GraphicsEngine should we use? + QString whatToUse = whatDoIUse(filename); + + if(verbose) + std::clog << "Using Graphicsengine: " + << (whatToUse=="gm" ? "GraphicsMagick" : (whatToUse=="qt" ? "ImageReader" : "External Tool")) + << " [" << whatToUse.toStdString() << "]" << std::endl; + + QImage ret; + + // Try to use XCFtools for XCF (if enabled) + if(QFileInfo(filename).suffix().toLower() == "xcf" && whatToUse == "extra") + ret = readImage_XCF(filename); + + // Try to use GraphicsMagick (if available) + else if(whatToUse == "gm") + ret = readImage_GM(filename); + + // Try to use Qt + else + ret = readImage_QT(filename); + + int w = ret.width(); + int h = ret.height(); + if(w < maxSize.width() && h < maxSize.height()) { + QPixmap retS(w,maxSize.height()); + retS.fill(Qt::transparent); + int x = 0; + int y = 0; + if(h < maxSize.height()) + y = (maxSize.height()-h)/2; + QPainter retP(&retS); + retP.drawImage(x,y,ret); + retP.end(); + return retS.toImage(); + } + + return ret; + +} + +QImage ImageProviderFull::readImage_QT(QString filename) { + + // For reading SVG files + QSvgRenderer svg; + QPixmap svg_pixmap; + + // For all other supported file types + QImageReader reader; + + // Return image + QImage img; + + // Suffix, for easier access later-on + QString suffix = QFileInfo(filename).suffix().toLower(); + + if(suffix == "svg") { + + // Loading SVG file + svg.load(filename); + + // Invalid vector graphic + if(!svg.isValid()) { + std::cerr << "[reader svg] Error: invalid svg file" << std::endl; + QPixmap pix(":/img/plainerrorimg.png"); + QPainter paint(&pix); + QTextDocument txt; + txt.setHtml("
ERROR LOADING IMAGE


The file doesn't contain valid a vector graphic
"); + paint.translate(100,150); + txt.setTextWidth(440); + txt.drawContents(&paint); + paint.end(); + origSize = pix.size(); + return pix.toImage(); + } + + // Render SVG into pixmap + svg_pixmap = QPixmap(svg.defaultSize()); + svg_pixmap.fill(Qt::transparent); + QPainter painter(&svg_pixmap); + svg.render(&painter); + + // Store the width/height for later use + origSize = svg.defaultSize(); + + } else { + + // Setting QImageReader + reader.setFileName(filename); + + // Store the width/height for later use + origSize = reader.size(); + + // Sometimes the size returned by reader.size() is <= 0 (observed for, e.g., .jp2 files) + // -> then we need to load the actual image to get dimensions + if(origSize.width() <= 0 || origSize.height() <= 0) { + qDebug() << "[imagereader qt] failed to read origsize"; + QImageReader r; + r.setFileName(filename); + origSize = r.read().size(); + } + + } + + int dispWidth = origSize.width(); + int dispHeight = origSize.height(); + + double q; + + if(dispWidth > maxSize.width()) { + q = maxSize.width()/(dispWidth*1.0); + dispWidth *= q; + dispHeight *= q; + } + + // If thumbnails are kept visible, then we need to subtract their height from the absolute height otherwise they overlap with the main image + if(dispHeight > maxSize.height()) { + q = maxSize.height()/(dispHeight*1.0); + dispWidth *= q; + dispHeight *= q; + } + + // Finalise SVG files + if(suffix == "svg") { + + // Convert pixmap to image + img = svg_pixmap.toImage(); + + } else { + + // Scale imagereader + reader.setScaledSize(QSize(dispWidth,dispHeight)); + + // Eventually load the image + img = reader.read(); + + // If an error occured + if(img.isNull()) { + QString err = reader.errorString(); + std::cerr << "[reader qt] Error: file failed to load: " << err.toStdString() << std::endl; + std::cerr << "Filename: " << filename.toStdString() << std::endl; + QPixmap pix(":/img/plainerrorimg.png"); + QPainter paint(&pix); + QTextDocument txt; + txt.setHtml(QString("
ERROR LOADING IMAGE


%1
").arg(err)); + paint.translate(100,150); + txt.setTextWidth(440); + txt.drawContents(&paint); + paint.end(); + return pix.toImage(); + } + + } + + return img; + +} + +QImage ImageProviderFull::readImage_GM(QString filename) { + +#ifdef GM + + QFile file(filename); + file.open(QIODevice::ReadOnly); + char *data = new char[file.size()]; + qint64 s = file.read(data, file.size()); + if (s < file.size()) { + delete[] data; + if(verbose) std::cerr << "[reader gm] ERROR reading image file data" << std::endl; + return QImage(); + } + + Magick::Blob blob(data, file.size()); + try { + Magick::Image image; + + QString suf = QFileInfo(filename).suffix().toLower(); + + if(suf == "x" || suf == "avs") + + image.magick("AVS"); + + else if(suf == "cals" || suf == "cal" || suf == "dcl" || suf == "ras") + + image.magick("CALS"); + + else if(suf == "cgm") + + image.magick("CGM"); + + else if(suf == "cut") + + image.magick("CUT"); + + else if(suf == "cur") + + image.magick("CUR"); + + else if(suf == "acr" || suf == "dcm" || suf == "dicom" || suf == "dic") + + image.magick("DCM"); + + else if(suf == "fax") + + image.magick("FAX"); + + else if(suf == "ico") + + image.magick("ICO"); + + else if(suf == "mono") { + + image.magick("MONO"); + image.size(Magick::Geometry(4000,3000)); + + } else if(suf == "mtv") + + image.magick("MTV"); + + else if(suf == "otb") + + image.magick("OTB"); + + else if(suf == "palm") + + image.magick("PALM"); + + else if(suf == "pfb") + + image.magick("PFB"); + + else if(suf == "pict" || suf == "pct" || suf == "pic") + + image.magick("PICT"); + + else if(suf == "pix" + || suf == "pal") + + image.magick("PIX"); + + else if(suf == "tga") + + image.magick("TGA"); + + else if(suf == "ttf") + + image.magick("TTF"); + + else if(suf == "txt") + + image.magick("TXT"); + + else if(suf == "wbm" + || suf == "wbmp") + + image.magick("WBMP"); + + + image.read(blob); + Magick::Blob ob; + image.type(Magick::TrueColorMatteType); + image.magick("PNG"); + image.write(&ob); + const QByteArray imgData((char*)(ob.data()),ob.length()); + QImage img(maxSize, QImage::Format_ARGB32); + img.loadFromData(imgData); + return img; + + } catch(Magick::Exception &error_) { + delete[] data; + std::cerr << "[reader gm] Error: " << error_.what() << std::endl; + QPixmap pix(":/img/plainerrorimg.png"); + QPainter paint(&pix); + QTextDocument txt; + txt.setHtml("
ERROR LOADING IMAGE


" + QString(error_.what()) + "
"); + paint.translate(100,150); + txt.setTextWidth(440); + txt.drawContents(&paint); + paint.end(); + pix.save(QDir::tempPath() + "/photoqt_tmp.png"); +// fileformat = ""; + origSize = pix.size(); +// scaleImg1 = -1; +// scaleImg2 = -1; +// animatedImg = false; + return pix.toImage(); + } + +#endif + + return QImage(); + +} + +QImage ImageProviderFull::readImage_XCF(QString filename) { + + // We first check if xcftools is actually installed + QProcess which; +#if QT_VERSION >= 0x050200 + which.setStandardOutputFile(QProcess::nullDevice()); +#endif + which.start("which xcf2png"); + which.waitForFinished(); + // If it isn't -> display error + if(which.exitCode()) { + std::cerr << "[reader xcf] Error: xcftools not found" << std::endl; + QPixmap pix(":/img/plainerrorimg.png"); + QPainter paint(&pix); + QTextDocument txt; + txt.setHtml("
ERROR LOADING IMAGE


PhotoQt relies on 'xcftools'' to display XCF images, but it wasn't found!
"); + paint.translate(100,150); + txt.setTextWidth(440); + txt.drawContents(&paint); + paint.end(); +// fileformat = ""; + origSize = pix.size(); +// scaleImg1 = -1; +// scaleImg2 = -1; +// animatedImg = false; + return pix.toImage(); + } + + // Convert xcf to png using xcf2png (part of xcftools) + QProcess p; + p.execute(QString("xcf2png \"%1\" -o %2").arg(filename).arg(QDir::tempPath() + "/photoqt_tmp.png")); + + // And load it + return readImage_QT(QDir::tempPath() + "/photoqt_tmp.png"); + +} + +QString ImageProviderFull::whatDoIUse(QString filename) { + + QString use = "qt"; + + if(extrasfiles.trimmed() != "") { + + // We need this list for GM and EXTRA below + QStringList extrasFiles = extrasfiles.split(","); + + // Check for extra + for(int i = 0; i < extrasFiles.length(); ++i) { + // We need to remove the first character of qtfiles.at(i), since that is a "*" + if(filename.toLower().endsWith(QString(extrasFiles.at(i)).remove(0,2))) { + use = "extra"; + break; + } + } + + } + +#ifdef GM + + // Check for GM (i.e., check for not qt and not extra) + bool usegm = true; + QStringList qtFiles = qtfiles.split(","); + + for(int i = 0; i < qtFiles.length(); ++i) { + // We need to remove the first character of qtfiles.at(i), since that is a "*" + if(filename.toLower().endsWith(QString(qtFiles.at(i)).remove(0,1))) + usegm = false; + } + if(extrasfiles.trimmed() != "") { + + // We need this list for GM and EXTRA below + QStringList extrasFiles = extrasfiles.split(","); + for(int i = 0; i < extrasFiles.length(); ++i) { + // We need to remove the first character of qtfiles.at(i), since that is a "*" + if(filename.toLower().endsWith(QString(extrasFiles.at(i)).remove(0,2))) + usegm = false; + } + } + + if(usegm) use = "gm"; +#endif + + return use; + +} diff --git a/cplusplus/imageprovider/imageproviderfull.h b/cplusplus/imageprovider/imageproviderfull.h new file mode 100644 index 000000000..d1ca33e64 --- /dev/null +++ b/cplusplus/imageprovider/imageproviderfull.h @@ -0,0 +1,41 @@ +#ifndef IMAGEPROVIDERFULL_H +#define IMAGEPROVIDERFULL_H + +#include +#include +#include +#include + +#ifdef GM +#include +#endif + +class ImageProviderFull : public QQuickImageProvider { + +public: + explicit ImageProviderFull(); + + QImage requestImage(const QString &filename_encoded, QSize *size, const QSize &requestedSize); + + QSize origSize; + +private: + bool verbose; + + QSize maxSize; + QSettings *settingsPerSession; + + QString qtfiles; + QString gmfiles; + QString extrasfiles; + + QImage readImage_QT(QString filename); + QImage readImage_GM(QString filename); + QImage readImage_XCF(QString filename); + + QString whatDoIUse(QString filename); + +}; + + +#endif // IMAGEPROVIDERFULL_H diff --git a/cplusplus/imageprovider/imageproviderthumbnail.cpp b/cplusplus/imageprovider/imageproviderthumbnail.cpp new file mode 100644 index 000000000..bac35c2e7 --- /dev/null +++ b/cplusplus/imageprovider/imageproviderthumbnail.cpp @@ -0,0 +1,215 @@ +#include "imageproviderthumbnail.h" + +ImageProviderThumbnail::ImageProviderThumbnail() : QQuickImageProvider(QQuickImageProvider::Pixmap) { + + // Setup database + db = QSqlDatabase::addDatabase("QSQLITE","thumbDB" + QString::number(rand())); + db.setDatabaseName(QDir::homePath() + "/.photoqt/thumbnails"); + db.open(); + + // Get permanent and temporary settings + settings = new Settings; + + // No transaction has been started yet + dbTransactionStarted = false; + +} + +QPixmap ImageProviderThumbnail::requestPixmap(const QString &filename_encoded, QSize *size, const QSize &requestedSize) { + + QByteArray filename = QByteArray::fromPercentEncoding(filename_encoded.toUtf8()); + + // Commit database and exit + if(filename.startsWith("__**__")) { + if(db.isOpen()) db.commit(); + db.close(); + dbTransactionStarted = false; + return QPixmap(1,1); + } + + // Some general settings that are needed multiple times later-on + int width = requestedSize.width(); + if(width == -1) width = settings->value("Thumbnail/ThumbnailSize").toInt(); + int thbsize = settings->value("Thumbnail/ThumbnailSize").toInt(); + + // Get full thumbnail + QImage thumbnail = getThumbnailImage(filename, width); + + + // Scaling it here as opposed to simple passing it on to QML and letting it handle the scaling there + // yields much better quality (no matter if 'smooth' or 'minimap' property is set in QML) + + // Get right image dimensions + int w = thumbnail.width(); + int h = thumbnail.height(); + if(w > thbsize) { + double q = (double)thbsize/(double)w; + w *= q; + h *= q; + } + if(h > thbsize) { + double q = (double)thbsize/(double)h; + w *= q; + h *= q; + } + + // Scale image + thumbnail = thumbnail.scaled(w,h,Qt::IgnoreAspectRatio,Qt::SmoothTransformation); + + return QPixmap::fromImage(thumbnail); + +} + +QImage ImageProviderThumbnail::getThumbnailImage(QByteArray filename, int thbsize) { + + QString typeCache = (settings->value("Thumbnail/ThbCacheFile").toBool() ? "files" : "db"); + bool cacheEnabled = settings->value("Thumbnail/ThumbnailCache").toBool(); + + if(!db.isOpen()) db.open(); + + // Create the md5 hash for the thumbnail file + QByteArray path = "file://" + filename; + QByteArray md5 = QCryptographicHash::hash(path,QCryptographicHash::Md5).toHex(); + + // Prepare the return QImage + QImage p; + + // We always opt for the 256px resolution for the thumbnails, + // as then we don't have to re-create thumbnails depending on change in settings + int ts = 256; + + int origwidth = -1; + int origheight = -1; + + bool wasoncecreated = false; + + // If files in ~/.thumbnails/ shall be used, then do use them + if(typeCache == "files" && cacheEnabled) { + + // If there exists a thumbnail of the current file already + if(QFile(QDir::homePath() + "/.thumbnails/large/" + md5 + ".png").exists() && cacheEnabled) { + +// if(verbose) std::clog << "thread: Loading existing thumb from file: " << createThisOne << std::endl; + + p.load(QDir::homePath() + "/.thumbnails/large/" + md5 + ".png"); + uint mtime = p.text("Thumb").remove("MTime:").trimmed().toInt(); + + // Use image if it's up-to-date + if(QFileInfo(filename).lastModified().toTime_t() == mtime) { + QSize dim = allSizes.value(filename); + origwidth = dim.width(); + origheight = dim.height(); + wasoncecreated = true; + } + + } + + // otherwise use the database (default) + } else if(cacheEnabled) { + + QSqlQuery query(db); + query.exec(QString("SELECT thumbnail,filelastmod,origwidth,origheight FROM Thumbnails WHERE filepath='%1'").arg(QString::fromUtf8(filename))); + if(query.next()) { + + if(query.value(query.record().indexOf("filelastmod")).toUInt() == QFileInfo(filename).lastModified().toTime_t()) { +// if(verbose) std::clog << "thread: Loading existing thumb from db: " << createThisOne << std::endl; + QByteArray b; + b = query.value(query.record().indexOf("thumbnail")).toByteArray(); + p.loadFromData(b); + origwidth = query.value(query.record().indexOf("origwidth")).toInt(); + origheight = query.value(query.record().indexOf("origheight")).toInt(); + wasoncecreated = true; + } + + + } + + query.clear(); + + } + + // If file wasn't loaded from file or database, then it doesn't exist yet (or isn't up-to-date anymore) and we have to create it + + if(!wasoncecreated) { + + ImageProviderFull image; + p = image.requestImage(filename.toPercentEncoding(),new QSize(ts,ts),QSize(ts,ts)); + + origwidth = image.origSize.width(); + origheight = image.origSize.height(); + + if(typeCache == "files" && cacheEnabled) { + + // If the file itself wasn't read from the thumbnails folder, is not a temporary file, and if the original file isn't at thumbnail size itself + if(filename.startsWith((QDir::homePath() + "/.thumbnails/").toLatin1()) + && !filename.startsWith(QDir::tempPath().toLatin1()) + && (p.height() > ts || p.width() > ts)) { + + // We use a QImageWriter (faster, metainfo support) - the path is a temporary path (for reason, see below) + QImageWriter writer(QDir::tempPath() + "/" + md5 + "__photo.png","png"); + + // The following meta information is required by the freedesktop standard + writer.setText("Thumb::MTime",QString("%1").arg(QFileInfo(filename).lastModified().toTime_t())); + + // We write the temporary file + writer.write(p); + + // If the file still doesn't exist, copy it to the right location (>> protection from concurrency) + if(QFile(QDir::homePath() + "/.thumbnails/large/" + md5 + ".png").exists()) + QFile(QDir::homePath() + "/.thumbnails/large/" + md5 + ".png").remove(); + + if(!QFile(QDir::tempPath() + "/" + md5 + "__photo.png").copy(QDir::homePath() + "/.thumbnails/large/" + md5 + ".png")) + std::cerr << "ERROR creating new thumbnail file!" << std::endl; + // Delete temporary file + QFile(QDir::tempPath() + "/" + md5 + "__photo.png").remove(); + + } + + } else if(cacheEnabled) { + + if(!dbTransactionStarted) { + db.transaction(); + dbTransactionStarted = true; + } + + QSqlQuery query2(db); + + QByteArray b; + QBuffer buf(&b); + buf.open(QIODevice::WriteOnly); + + // If file has transparent areas, we save it as png to preserver transparency. Otherwise we choose jpg (smaller) + if(p.hasAlphaChannel()) + p.save(&buf,"PNG"); + else + p.save(&buf,"JPG"); + + // If it was once created, i.e. if the file changed (i.e. if last mod date changed), then we have to update it + if(wasoncecreated) + query2.prepare("UPDATE Thumbnails SET filepath=:path,thumbnail=:thb,filelastmod=:mod,thumbcreated=:crt,origwidth=:origw,origheight=:origh WHERE filepath=:path"); + else + query2.prepare("INSERT INTO Thumbnails(filepath,thumbnail,filelastmod,thumbcreated,origwidth,origheight) VALUES(:path,:thb,:mod,:crt,:origw,:origh)"); + + query2.bindValue(":path",filename); + query2.bindValue(":thb",b); + query2.bindValue(":mod",QFileInfo(filename).lastModified().toTime_t()); + query2.bindValue(":crt",QDateTime::currentMSecsSinceEpoch()); + query2.bindValue(":origw",origwidth); + query2.bindValue(":origh",origheight); + query2.exec(); + if(query2.lastError().text().trimmed().length()) + std::cerr << "ERROR [" << filename.toStdString() << "]: " << query2.lastError().text().trimmed().toStdString() << std::endl; + query2.clear(); + + } + + } + + return p; + +} + +ImageProviderThumbnail::~ImageProviderThumbnail() { + db.commit(); + db.close(); +} diff --git a/cplusplus/imageprovider/imageproviderthumbnail.h b/cplusplus/imageprovider/imageproviderthumbnail.h new file mode 100644 index 000000000..88f7319eb --- /dev/null +++ b/cplusplus/imageprovider/imageproviderthumbnail.h @@ -0,0 +1,35 @@ +#ifndef IMAGEPROVIDERTHUMBS_H +#define IMAGEPROVIDERTHUMBS_H + +#include +#include +#include +#include +#include +#include +#include +#include "../settings/settings.h" + +#include "imageproviderfull.h" + +class ImageProviderThumbnail : public QQuickImageProvider { + +public: + explicit ImageProviderThumbnail(); + ~ImageProviderThumbnail(); + + QPixmap requestPixmap(const QString &filename_encoded, QSize *size, const QSize &requestedSize); + +private: + QSqlDatabase db; + Settings *settings; + + QImage getThumbnailImage(QByteArray filename, int thbsize); + + bool dbTransactionStarted; + + QHash allSizes; + +}; + +#endif // IMAGEPROVIDERTHUMBS_H diff --git a/cplusplus/main.cpp b/cplusplus/main.cpp new file mode 100644 index 000000000..66232f6ea --- /dev/null +++ b/cplusplus/main.cpp @@ -0,0 +1,17 @@ +#include +#include "mainwindow.h" + +int main(int argc, char *argv[]) { + + QApplication app(argc, argv); + +#ifdef GM + Magick::InitializeMagick(*argv); +#endif + + MainWindow w; + w.showFullScreen(); + + return app.exec(); + +} diff --git a/cplusplus/mainwindow.cpp b/cplusplus/mainwindow.cpp new file mode 100644 index 000000000..7d9f95322 --- /dev/null +++ b/cplusplus/mainwindow.cpp @@ -0,0 +1,148 @@ +#include "mainwindow.h" + +MainWindow::MainWindow(QWindow *parent) : QQuickView(parent) { + + // Settings and variables + settingsPerSession = new QSettings("photoqt_session"); + settingsPermanent = new Settings; + variables = new Variables; + + // Add image providers + this->engine()->addImageProvider("thumb",new ImageProviderThumbnail); + this->engine()->addImageProvider("full",new ImageProviderFull); + + // Add settings access to QML + qmlRegisterType("my.settings", 1, 0, "Settings"); + + // Load QML + this->setSource(QUrl("qrc:/qml/mainwindow.qml")); + this->setColor(QColor(Qt::transparent)); + this->setFlags(Qt::FramelessWindowHint | Qt::Window); + + // Get object (for signals and stuff) + object = this->rootObject(); + + // Class to load a new directory + loadDir = new LoadDir; + + // Window resized + connect(this, SIGNAL(widthChanged(int)), this, SLOT(resized())); + connect(this, SIGNAL(heightChanged(int)), this, SLOT(resized())); + + // Scrolled view + connect(object, SIGNAL(thumbScrolled(QVariant)), this, SLOT(handleThumbnails(QVariant))); + + // Open file + connect(object, SIGNAL(openFile()), this, SLOT(openNewFile())); + + // Quit PhotoQt + connect(this->engine(), SIGNAL(quit()), qApp, SLOT(quit())); + + // We have to call it with a timer to ensure the window is actually visible first + QTimer::singleShot(100, this, SLOT(openNewFile())); + +} + +// Window has been resized +void MainWindow::resized() { + + settingsPerSession->setValue("curSize",QSize(this->width(),this->height())); + + QMetaObject::invokeMethod(object, "resizeElements", + Q_ARG(QVariant, this->width()), + Q_ARG(QVariant, this->height())); + +} + +// Open a new file +void MainWindow::openNewFile() { + + // Get new filename + QByteArray file = QFileDialog::getOpenFileName(0,tr("Open image file"),QDir::homePath(),tr("All Files") + " (*)").toUtf8(); + + if(file.trimmed() == "") return; + + // Clear loaded thumbnails + variables->loadedThumbnails.clear(); + + // Load direcgtory + QFileInfoList l = loadDir->loadDir(file); + + // Get and store length + int l_length = l.length(); + settingsPerSession->setValue("countTot",l_length); + + // Convert QFileInfoList into QStringList and store it + QStringList ll; + for(int i = 0; i < l_length; ++i) + ll.append(l.at(i).absoluteFilePath().toUtf8().toPercentEncoding("/ ")); + settingsPerSession->setValue("allFileList",ll); + + // Setiup thumbnail model + QMetaObject::invokeMethod(object, "setupModel", + Q_ARG(QVariant, ll)); + + // Get and store current position + int curPos = l.indexOf(QFileInfo(file)); + settingsPerSession->setValue("curPos",curPos); + + // Display current postiion in main image view + QMetaObject::invokeMethod(object, "displayImage", + Q_ARG(QVariant, curPos)); + + // And handle the thumbnails + handleThumbnails(curPos); + +} + +// Thumbnail handling (centerPos is image currently displayed in the visible center of thumbnail bar) +void MainWindow::handleThumbnails(QVariant centerPos) { + + // Get some settings for later use + int thumbSize = settingsPermanent->value("Thumbnail/ThumbnailSize").toInt(); + int thumbSpacing = settingsPermanent->value("Thumbnail/ThumbnailSpacingBetween").toInt(); + int dynamicSmartNormal = settingsPermanent->value("Thumbnail/ThumbnailDynamic").toInt(); + + // Get total and center pos + int countTot = settingsPerSession->value("countTot").toInt(); + int center = centerPos.toInt(); + + // Generate how many to each side + int numberToOneSide = (this->width()/(thumbSize+thumbSpacing))/2; + + // Load full directory + if(dynamicSmartNormal == 0) numberToOneSide = qMax(center,countTot-center); + + // Load thumbnails (we start at the screen edge working towards the center, cause QML starts with the latest one changed) + for(int i = numberToOneSide+3; i >= 1; --i) { + if(center-i >= 0 && !variables->loadedThumbnails.contains(center-i)) { + QMetaObject::invokeMethod(object, "reloadImage", + Q_ARG(QVariant, center-i)); + variables->loadedThumbnails.append(center-i); + } + if(center+i < countTot && !variables->loadedThumbnails.contains(center+i)) { + QMetaObject::invokeMethod(object, "reloadImage", + Q_ARG(QVariant, center+i)); + variables->loadedThumbnails.append(center+i); + } + } + // The first image to be loaded should be the central image + if(!variables->loadedThumbnails.contains(center)) { + QMetaObject::invokeMethod(object, "reloadImage", + Q_ARG(QVariant, center)); + variables->loadedThumbnails.append(center); + } + + // In 'smart thumbnails' mode we load all other visible thumbnails after the currently visible ones are loaded + if(dynamicSmartNormal == 2) + smartLoadDirectory(); + +} + +void MainWindow::smartLoadDirectory() { + + // TO-DO + +} + +MainWindow::~MainWindow() { } diff --git a/cplusplus/mainwindow.h b/cplusplus/mainwindow.h new file mode 100644 index 000000000..3cfd0677e --- /dev/null +++ b/cplusplus/mainwindow.h @@ -0,0 +1,50 @@ +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "handlefiles/loaddir.h" +#include "imageprovider/imageproviderthumbnail.h" +#include "imageprovider/imageproviderfull.h" +#include "settings/settings.h" +#include "variables.h" + +class MainWindow : public QQuickView { + + Q_OBJECT + +public: + explicit MainWindow(QWindow *parent = 0); + ~MainWindow(); + +private: + QQuickItem *item; + QObject *object; + LoadDir *loadDir; + + QSettings *settingsPerSession; + Settings *settingsPermanent; + Variables *variables; + +private slots: + void openNewFile(); + + void resized(); + + void handleThumbnails(QVariant centerPos); + void smartLoadDirectory(); + +signals: + void doSetupModel(); + + +}; + +#endif // MAINWINDOW_H diff --git a/cplusplus/settings/settings.cpp b/cplusplus/settings/settings.cpp new file mode 100644 index 000000000..18ba60360 --- /dev/null +++ b/cplusplus/settings/settings.cpp @@ -0,0 +1,15 @@ +#include "settings.h" + +// Convenience class to access and change permanent settings + +Settings::Settings(QObject *parent) : QObject(parent) { + settings_ = new QSettings(QDir::homePath() + "/.photoqt/settings",QSettings::IniFormat); +} + +void Settings::setValue(const QString &key, const QVariant &value) { + settings_->setValue(key, value); +} + +QVariant Settings::value(const QString &key, const QVariant &defaultValue) const { + return settings_->value(key, defaultValue); +} diff --git a/cplusplus/settings/settings.h b/cplusplus/settings/settings.h new file mode 100644 index 000000000..12849ac1f --- /dev/null +++ b/cplusplus/settings/settings.h @@ -0,0 +1,25 @@ +#ifndef SETTINGS_H +#define SETTINGS_H + +#include +#include +#include + +// Convenience class to access and change permanent settings + +class Settings : public QObject { + + Q_OBJECT + +public: + explicit Settings(QObject *parent = 0); + Q_INVOKABLE void setValue(const QString & key, const QVariant & value); + Q_INVOKABLE QVariant value(const QString &key, const QVariant &defaultValue = QVariant()) const; + + +private: + QSettings *settings_; + +}; + +#endif // SETTINGS_H diff --git a/cplusplus/variables.h b/cplusplus/variables.h new file mode 100644 index 000000000..eb1c86116 --- /dev/null +++ b/cplusplus/variables.h @@ -0,0 +1,20 @@ +#ifndef VARIABLES_H +#define VARIABLES_H + +#include + +class Variables { + +public: + Variables() { + loadedThumbnails.clear(); + } + +public: + QList loadedThumbnails; + +}; + + + +#endif // VARIABLES_H diff --git a/img.qrc b/img.qrc new file mode 100644 index 000000000..ac6e97dde --- /dev/null +++ b/img.qrc @@ -0,0 +1,6 @@ + + + img/emptythumb.png + img/nothumb.png + + diff --git a/img/emptythumb.png b/img/emptythumb.png new file mode 100644 index 0000000000000000000000000000000000000000..1a051e7f144e80fb69e9275d956cd2a07a63cb44 GIT binary patch literal 2067 zcmbW2iBnVe7RPTu7J*bPh%2Ia14a~z5U526CNOYWBS9*%*eVHGSbQ;M$54>?92MmT z6w|O-C7>b*$|fNQh>h}^Silqrq8JEIK`0;uiMcO2{RjHyH|KZG?=#Pu7q;QB5$&^TSC1RWvPX*ds>z+A(tS8_mSUhig2 z$u(s)Xk>Taj&}f#Rh4_dv1ItHVL}G{Yv&z6mj5Xw5vD*6P(3KQesEpYGcEWIR$Pru zfN@acpX*6I3t(*sD*!%gB<=}nLPPBbb`kofZHm)XcFR4l>NK?T!;J@>j?k_?IvMOg z_d6)2r~QE^KXj9@g#6j5zyMYde9SQ)oLf!`DxEhyH}U`W*6YDD7j&4-fXpcw73GsZ8W@aod;(!z0Ent zBg>S5T|XaU`NP)!T27M@-0Qk$qd^~tAPM*_-P}%9dErl|;4P*L^QrLRT;deEIz~BI z5}O1Qq0+LqYFLxX%RpRK2hh9o;mveRyjzve#*G_7|CQRB0Xr7h~humy=a&e6? zUAZY;B>*!R{sKE*=64c0c_F&&mskoc?CX8gl^^)uk?I$3LDW)Lydwri* z3?kPv?NrdYxxO%7dO!U~9ZKPvWjXDYSTp@#yimbo+i(8XDz&JBXdQKkoYN+nO}?`y z3P{AUCv#`yh1*Y=COt`g`qE0xj553NCOC*yUb$+&=;!H{d$Wf+47j9EDq+70T|Hwn z@`qugDnHE8p{TxpZ79$>HGtF3yxa0;si>N|gndjrXp*frYvvHbiqRilxSf+FQhZV6 z=~VZ^<_)hTAA8guXk7`ghXi~CIgF0)%g@OwVHiKBoFyYgbA2XBeVW3Drn6^uD2>aX zePucMM$tLEaO6=tbtA#V>uQnTl?Kx!vSep|H21QejB?3x@s}II0R;Dc)XUOD zqr6Zw;ef%%)aF^&qBh)8@s1k+d(fl$E>%uB0gKnatPC(+{7L+UPJk(LHNuHX&7cbT ziW`ME?%L=k@PlIKXc2=t3f~PFY(mv+_I6Z+6Iq$MdKYf;>l(u5kjHfuj#S&_E(_Zy z(^z~&&qEXBBWIwlA|$x%`Y~Bj<rr8hDu=1AE)vV7gi#Eo*zZ?eEZ7u|r3OSpjQ9GR;-%7{oes3sWx*#ErlB9gHOZ z=MqB89i8Dp?cvQ*BREP>PrihDqW0!TSf9KmBR2SA?6>%9XIA4~;=*BTmqLv@ImlJd z<%byf0UCo;gaz>R(MJStMR~uXA26r&Xg!ay$Yp8wqR&Hs|1SvkK-(g5W8p3?W9y+s zT%r0buTAQZO%?DMyYJb;oD_IG8Rc>!{FZQ<@25T1&A`j5YF!p?b4up3XfNz}Td~Dz z-%t_M>@``DCf-x14iTo`Nx65K=)T9HP@Qr!y=xtryLmKV?PT3E zI9{rx>JJH&7YW8A3(w8O+g`l}XEyjxUb#eJ$o19PmkDld*a0}2907cViLkjZOO!(O zxDR*qo3UL%PKNzcmHiJ*W_{0W9HLX<5!{CC1gO#!cVAkBvqQu~`zqdhI_rIq9?tui zcAjJp+?V>~eT>-MZvsp^i%IzTGp#FaX8riHq14qg`qhbUW5(H*gOOF%=6Sn;`?7q( zr+9OQLH6QcXjNGap$3q7E?K-)#Z+(?TeefSt!gDzRb=(R@m2emtuMIy$&SkVqky}X z$7!{6NE=3;tNrk9&Gb^eK)tHmZfKiSxnYxmNyd_eEm8npHN~=@Wsp4{xCPG51hDc* z1h;7ZTU3PvM0%RXjY`}~mGjcE){cBo{-hT#nqa)AS?1W2_o5qjNpduC^tu}81!ft7 l2mb0@^AEnH|696s6p!M@`|XjBxGFvufqa6!?-G8<`5#MOsBr)Q literal 0 HcmV?d00001 diff --git a/img/nothumb.png b/img/nothumb.png new file mode 100644 index 0000000000000000000000000000000000000000..02af944b2baf4277abd653b4606b406202fca823 GIT binary patch literal 584 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST_$yu_bxCyDx`7I;J! zGca%qgD@k*tT_@uLG}_)Usv{f+@cIJS|_cqf{c;P42dXl&d<$F%_{+N85o?4QWHy3 zQxwWGOEMJPJ$(aG^itUw7#P=hx;TbZ+DawBOg|BTK)v S(J}*2gnGLAxvX#3i literal 0 HcmV?d00001 diff --git a/qml/Shortcuts.qml b/qml/Shortcuts.qml new file mode 100644 index 000000000..7128576d9 --- /dev/null +++ b/qml/Shortcuts.qml @@ -0,0 +1,35 @@ +import QtQuick 2.3 +import QtQuick.Controls 1.2 + +Item { + Action { + shortcut: "Right" + onTriggered: + thumbnailBar.nextImage() + } + Action { + shortcut: "Space" + onTriggered: + thumbnailBar.nextImage() + } + Action { + shortcut: "Left" + onTriggered: + thumbnailBar.previousImage() + } + Action { + shortcut: "Backspace" + onTriggered: + thumbnailBar.previousImage() + } + Action { + shortcut: "Escape" + onTriggered: + Qt.quit() + } + Action { + shortcut: "O" + onTriggered: + openFile() + } +} diff --git a/qml/mainview/Background.qml b/qml/mainview/Background.qml new file mode 100644 index 000000000..30fb960e1 --- /dev/null +++ b/qml/mainview/Background.qml @@ -0,0 +1,46 @@ +import QtQuick 2.3 + +Rectangle { + + id: background + color: "#AA000000" + + // Show thumbnail bar + MouseArea { + + x: 0 + y: background.height-50 + + width: background.width + height: 50 + + hoverEnabled: true + + onEntered: + PropertyAnimation { + target: thumbnailBar + property: (settings.value("Thumbnail/ThumbnailKeepVisible")*1 == 0 ? "y" : ""); + to: background.height-settings.value("Thumbnail/ThumbnailSize")*1-thumbnailbarheight_addon + } + } + + // Hide thumbnail bar + MouseArea { + + x: 0 + y: 0 + + width: background.width + height: background.height-settings.value("Thumbnail/ThumbnailSize")*1-thumbnailbarheight_addon-50 + + hoverEnabled: true + + onEntered: + PropertyAnimation { + target: thumbnailBar + property: "y" + to: background.height-(settings.value("Thumbnail/ThumbnailKeepVisible")*1 ? settings.value("Thumbnail/ThumbnailSize")*1+thumbnailbarheight_addon : 0) + } + } + +} diff --git a/qml/mainview/Display.qml b/qml/mainview/Display.qml new file mode 100644 index 000000000..83cf1022f --- /dev/null +++ b/qml/mainview/Display.qml @@ -0,0 +1,9 @@ +import QtQuick 2.3 + +Image { + + fillMode: Image.PreserveAspectFit + asynchronous: false + clip: true + +} diff --git a/qml/mainview/QuickInfo.qml b/qml/mainview/QuickInfo.qml new file mode 100644 index 000000000..49b2a7d92 --- /dev/null +++ b/qml/mainview/QuickInfo.qml @@ -0,0 +1,143 @@ +import QtQuick.Controls 1.2 +import QtQuick.Controls.Styles 1.2 +import QtQuick 2.3 + +Item { + + // Set data + function updateQuickInfo(pos, totalNumberImages, filepath) { + counter.text = (pos+1).toString() + "/" + totalNumberImages.toString() + filename.text = filepath + } + + // Rectangle holding all the items + Rectangle { + + id: counterRect + + x: 0 + y: 0 + + // it is always as big as the item it contains + width: childrenRect.width+6 + height: childrenRect.height+6 + + // Some styling + color: "#55000000" + radius: 5 + + // COUNTER + Text { + + id: counter + + x:3 + y:3 + + text: " Open a file to begin..." + + color: "white" + font.bold: true + + // Show context menu on right click + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton + onClicked: { + if (mouse.button == Qt.RightButton) { + contextmenuCounter.popup() + } + } + } + + // The context menu + Menu { + id: contextmenuCounter + style: MenuStyle { + frame: menuFrame + itemDelegate.background: menuHighlight + } + + MenuItem { + text: "Hide Counter" + // onTriggered: ... + } + + } + + } + + // SPACING - it does nothing but seperate counter from filename + Text { + id: spacing + + y: 3 + width: 10 + anchors.left: counter.right + + text: "" + + } + + // FILENAME + Text { + + id: filename + + y: 3 + anchors.left: spacing.right + + text: "" + color: "white" + font.bold: true + + // Show context menu + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton | Qt.RightButton + onClicked: { + if (mouse.button == Qt.RightButton) { + contextmenuFilename.popup() + } + } + } + + // The actual context menu + Menu { + id: contextmenuFilename + style: MenuStyle { + frame: menuFrame + itemDelegate.background: menuHighlight + } + + MenuItem { + text: "Hide Filepath, leave Filename" + // onTriggered: ... + } + + MenuItem { + text: "Hide everything" + // onTriggered: ... + } + + } + + } + + } + + // Some menu styling + Component { + id: menuFrame + Rectangle { + color: "#0F0F0F" + } + } + Component { + id: menuHighlight + Rectangle { + color: (styleData.selected ? "#4f4f4f" :"#0F0F0F") + } + } + +} diff --git a/qml/mainwindow.qml b/qml/mainwindow.qml new file mode 100644 index 000000000..d73523a89 --- /dev/null +++ b/qml/mainwindow.qml @@ -0,0 +1,88 @@ +import QtQuick 2.3 +import my.settings 1.0 + +import "mainview/" +import "slidein/" + +Item { + + id: toplevel + + // This is how much bigger than the thumbnails the thumbnail bar is (this is the space to the top) + property int thumbnailbarheight_addon: 50 + + // This signal is picked up by the mainwindow.cpp file + signal thumbScrolled(var filenameAtCenter) + signal openFile() + + // Access to the permanent settings file (~/.photoqt/settings) + Settings { id: settings } + + Shortcuts { id: sh } + + // Application background + Background { + id: background + objectName: "background" + } + + // The main displayed image + Display { + id: image + objectName: "image" + } + + // The thumbnail bar at the bottom + ThumbnailBar { + id: thumbnailBar + objectName: "thumbnailbar" + height: settings.value("Thumbnail/ThumbnailSize")*1+thumbnailbarheight_addon + } + + // The quickinfo (position in folder, filename) + QuickInfo { + id: quickInfo + x:5 + y:5 + } + + // MetaData of the image (using the C++ Exiv2 library) + MetaData { + id: metaData + width: 300 + height: 550 + radius: 10 + objectName: "metaData" + } + + + // Adjust size of all the elements + function resizeElements(w,h) { + + var thumbKeepVisible = settings.value("Thumbnail/ThumbnailKeepVisible")*1 + + background.width = w + background.height = h + + thumbnailBar.width = w + thumbnailBar.y = h-(thumbKeepVisible ? settings.value("Thumbnail/ThumbnailSize")*1+thumbnailbarheight_addon : 0) + + image.width = w + image.height = (thumbKeepVisible ? h-thumbnailBar.height : h) + + metaData.x = -10 + metaData.y = (h-metaData.height)/3 + + image.sourceSize.width = w + image.sourceSize.height = h + + } + + // Slots accessable by mainwindow.cpp, passed on to thumbnailbar + function reloadImage(pos) { thumbnailBar.reloadImage(pos) } + function setupModel(stringlist) { thumbnailBar.setupModel(stringlist) } + function displayImage(pos) { thumbnailBar.displayImage(pos) } + function nextImage() { thumbnailBar.nextImage(); } + function previousImage() { thumbnailBar.previousImage(); } + +} diff --git a/qml/slidein/MetaData.qml b/qml/slidein/MetaData.qml new file mode 100644 index 000000000..5f6b045f7 --- /dev/null +++ b/qml/slidein/MetaData.qml @@ -0,0 +1,9 @@ +import QtQuick 2.3 +import QtQuick.Controls 1.2 + +Rectangle { + + color: "#AA000000" + visible: false + +} diff --git a/qml/slidein/ThumbnailBar.qml b/qml/slidein/ThumbnailBar.qml new file mode 100644 index 000000000..61f588542 --- /dev/null +++ b/qml/slidein/ThumbnailBar.qml @@ -0,0 +1,357 @@ +import QtQuick 2.3 +import QtQuick.Controls 1.2 + + +Rectangle { + + id: thumbnailBar + + // Transparent background + color: "#00000000" + + // Store the total number of images (for loop, and for quickinfo) + property int totalNumberImages: 0 + + // Update quickinfo (position in dir, filename) + signal updateQuickInfo() + + // We need those two to manage the current item as we use custom styling for hovered/selected item (previous¤t) + property var previousItem: Item + property int previousIndex: -1 + property int hoveredIndex: -1 + + + // Setup a new model + function setupModel(stringlist) { + + // remove previous index + previousIndex = -1 + + // Clear model of all thumbnails + imageModel.clear() + + // Store total number of images + totalNumberImages = stringlist.length + + // Add elements to model + for (var i = 0; i < totalNumberImages; ++i) + imageModel.append({"imageUrl" : stringlist[i], "counter" : i, "pre" : true}) + + // (Re-)set model + gridView.model = imageModel + + // Adjust gridView width + gridView.width = stringlist.length*gridView.cellWidth + + } + + // Display an image in large main view + function displayImage(pos) { + + // Store some values + var imageUrl = imageModel.get(pos).imageUrl; + var thumbnailliftup = settings.value("Thumbnail/ThumbnailLiftUp")*1 + var thumbnailsize = settings.value("Thumbnail/ThumbnailSize")*1 + var thumbnailspacing = settings.value("Thumbnail/ThumbnailSpacingBetween")*1 + + // Load image + image.source = "image://full/" + imageUrl + + // Store new position + gridView.currentIndex = pos + + // Ensure selected item is centered/visible + if(gridView.width > thumbnailBar.width) { + + // content x and width of flick + var visible_x = flick.visibleArea.xPosition*gridView.width + var visible_width = flick.visibleArea.widthRatio*gridView.width + + // Newly loaded dir => center item + if(previousIndex == -1) + flick.contentX = (pos+0.5)*(thumbnailsize+thumbnailspacing)-0.5*thumbnailBar.width + // Ensure visible to the right + else if((pos+1)*(thumbnailsize+thumbnailspacing) > visible_x+visible_width) + flick.contentX = (pos+1.5)*(thumbnailsize+thumbnailspacing)-thumbnailBar.width + // Ensure visible to the left + else + if((pos-1)*(thumbnailsize+thumbnailspacing) < visible_x) + flick.contentX = (pos-0.5)*(thumbnailsize+thumbnailspacing) + } + + // Ensure loaded item is lifted up + if(hoveredIndex != pos) gridView.currentItem.y = gridView.currentItem.y-thumbnailliftup + if(previousIndex != -1 && hoveredIndex != previousIndex) previousItem.y = previousItem.y+thumbnailliftup + + // Store selected item + previousItem = gridView.currentItem + previousIndex = gridView.currentIndex + + // Update quickinfo (position, filename) + quickInfo.updateQuickInfo(pos, totalNumberImages, imageUrl); + + } + + // Load next image + function nextImage() { + displayImage(previousIndex+1); + scrollTimer.restart() + } + + // Load previous image + function previousImage() { + displayImage(previousIndex-1) + scrollTimer.restart() + } + + // Load proper thumbnail at position 'pos' + function reloadImage(pos) { + var imageUrl = imageModel.get(pos).imageUrl; + imageModel.set(pos,{"imageUrl" : imageUrl, "counter" : pos, "pre" : false}) + } + + // This image (and timer below) takes care of 'commit'ing the thumbnail database images + Image { + id: hiddenImageCommitDatabase + visible: false + source: "" + cache: false + } + Timer { + id: timerhiddenImageCommitDatabase + interval: 1000 + running: false + repeat: false + onTriggered: { + hiddenImageCommitDatabase.source = "image://thumb/__**__" + Math.random() + } + } + + // If the view was scrolled/moved, this timer is set off + Timer { + id: scrollTimer + interval: 500 + running: false + repeat: false + onTriggered: { + // Item in the center of the screen + var centerpos = (flick.contentX+flick.width/2)/(settings.value("Thumbnail/ThumbnailSize")*1+settings.value("Thumbnail/ThumbnailSpacingBetween")*1) + // Emit 'scrolled' signal + toplevel.thumbScrolled(centerpos) + } + } + + // Enable moving of flick with mouse wheel + MouseArea { + anchors.fill: parent + hoverEnabled: true + onWheel: { + if(wheel.angleDelta.y >= 0 && flick.contentX-50 >= 0) + flick.contentX = flick.contentX-50 + else if(wheel.angleDelta.y < 0 && flick.contentWidth >= (flick.contentX+flick.width+50)) + flick.contentX = flick.contentX+50 + scrollTimer.restart() + + } + } + + // Model of thumbnail bar + ListModel { + id: imageModel + objectName: "model" + } + + // Individual Element of model + Component { + id: imageDelegate + Row { + + // Store some values + property int thumbnailsize: settings.value("Thumbnail/ThumbnailSize")*1 + property int thumbnailbarsize: thumbnailsize+thumbnailbarheight_addon + property int thumbnailliftup: settings.value("Thumbnail/ThumbnailLiftUp")*1 + property int thumbnailspacing: settings.value("Thumbnail/ThumbnailSpacingBetween")*1 + + Rectangle { + + id: imgrect + + // half-transparent background + color: "#88000000" + + // Set a border + border.color: "#AA000000" + border.width: 1 + + // Adjust position + x: 0 + y: thumbnailbarheight_addon-5 + + // Set width/height + width: gridView.cellWidth + height: gridView.cellHeight + + // The actual thumbnail image + Image { + + id: img + + // Store some values + property var count: counter + property var path: imageUrl + + // Set position + y: 0 + x: thumbnailspacing/2 + + // Adjust size + width: thumbnailsize + height: thumbnailsize + + // Set image source (preload or normal) and displayed source dimension + source: (pre ? "qrc:/img/emptythumb.png" : "image://thumb/" + imageUrl) + sourceSize.width: thumbnailsize + sourceSize.height: thumbnailsize + + // Adjust different values + fillMode: Image.PreserveAspectFit + clip: true + cache: true + smooth: true + asynchronous: true + + MouseArea { + + cursorShape: Qt.PointingHandCursor + anchors.fill: parent + hoverEnabled: true + + // Lift item up on hover + onEntered: { + hoveredIndex = img.count + if(img.count != gridView.currentIndex) + imgrect.y = imgrect.y-thumbnailliftup; + } + // Remove item lift when leaving it + onExited: { + hoveredIndex = -1 + if(gridView.currentIndex != img.count) + imgrect.y = imgrect.y+thumbnailliftup; + } + // Load thumbnail as main image + onClicked: { + if(previousIndex != index) { + displayImage(index) + } + } + } + // Catch 'loading completed' of thumbnail + onStatusChanged: { + // If image is ready and it's not a preload image + if(img.status == Image.Ready && pre == false) + // Start timer to commit thumbnail database + timerhiddenImageCommitDatabase.restart() + } + } + + // Rectangle+Text to display filename + Rectangle { + + id: filenamerect + + x: 3 + y: parent.height*0.67 + + // spacing to the left&right: 3 pixel + width: parent.width-6 + height: parent.height-y + + // Some styling + color: "#88000000" + visible: settings.value("Thumbnail/ThumbnailWriteFilename")*1 + radius: 3 + + // The actual filename text + Text { + + id: filename + + // Set filename (we need to filter it out of the path) + property var l: imageUrl.split("/") + text: l[l.length-1] + + // Same size as parent of course + anchors.fill: parent + + // text is white, bold, and a certain size + color: "white" + font.bold: true + font.pointSize: settings.value("Thumbnail/ThumbnailFontSize")*1 + + // Some text settings + maximumLineCount: 2 + wrapMode: Text.WrapAnywhere + elide: Text.ElideRight + + // Center text in middle of item + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + + } + } + } + } + } // END Component - individual model element + + // Scroll area containing the thumbnail view + Flickable { + + id: flick + + // Maximum size + anchors.fill: parent + + // Adjust size + contentHeight: gridView.height + contentWidth: (parent.width < gridView.width ? gridView.width : parent.width) + + // Behaviour + clip: true + flickableDirection: Flickable.HorizontalFlick + boundsBehavior: Flickable.StopAtBounds +// maximumFlickVelocity: 0 + + GridView { + + id: gridView + + property int thumbnailsize: settings.value("Thumbnail/ThumbnailSize")*1 + property int thumbnailspacing: settings.value("Thumbnail/ThumbnailSpacingBetween")*1 + + // Center the view (if smaller than the Flickable) + anchors.horizontalCenter: parent.horizontalCenter + + // Set model and delegate + model: imageModel + delegate: imageDelegate + + // Set fixed cell size + cellWidth: thumbnailsize+thumbnailspacing + cellHeight: thumbnailsize + + // Set flow + flow: GridView.TopToBottom + + } + + // When flicking finished + onMovementEnded: { + // Item in center of flickable + var centerpos = (flick.contentX+flick.width/2)/(settings.value("Thumbnail/ThumbnailSize")*1+settings.value("Thumbnail/ThumbnailSpacingBetween")*1) + // Emit 'scrolled' signal + toplevel.thumbScrolled(centerpos) + } + + } + +} diff --git a/qrc.qrc b/qrc.qrc new file mode 100644 index 000000000..b204ad9e0 --- /dev/null +++ b/qrc.qrc @@ -0,0 +1,11 @@ + + + qml/mainwindow.qml + qml/mainview/Background.qml + qml/mainview/Display.qml + qml/mainview/QuickInfo.qml + qml/slidein/ThumbnailBar.qml + qml/slidein/MetaData.qml + qml/Shortcuts.qml + +