diff --git a/NEWS b/NEWS index 9196083df6..82089714d2 100644 --- a/NEWS +++ b/NEWS @@ -1,67 +1,69 @@ digiKam 6.2.0 - Release date: 2019-06-?? ***************************************************************************************************** NEW FEATURES: IconView : HiDPI support for 4K screens. ***************************************************************************************************** BUGFIXES: 001 ==> 279216 - "Resize image" feature is missing some useful options [patch]. 002 ==> 406581 - "No such album" when dragging duplicated Tag to merge them. 003 ==> 406585 - Feature request: clean or hide empty Tags. 004 ==> 389657 - QFile::rename: Empty or null file name. 005 ==> 406610 - Hit enter when renaming, sometime don't rename correctly. 006 ==> 406637 - Duplicates "Search in" Doesn't Show Collection Name Upon First Selection. 007 ==> 406226 - Adding tags to a selection from a large list of no-tags images takes a very long time. 008 ==> 374302 - MYSQL : all Images with Geoinfo seem to be at (0°, 0°). 009 ==> 406815 - Can not setup Thunderbid in Send by mail. 010 ==> 406855 - Feature request: date from filename. 011 ==> 406938 - Face region is not saved after resizing. 012 ==> 406942 - Thumbnails does not get saved for videofiles over 2^64b, mysql. 013 ==> 406981 - GoPro video thumbnails are just noise. 014 ==> 407043 - Image rename to folder fails on Windows. 015 ==> 407157 - Lens Auto-Correction for fixed-lens cameras 016 ==> 407197 - Target album does not exist. 017 ==> 371005 - Zoom function doesn't work properly on HiDPI Display 018 ==> 407233 - Pictures distribution on Light Table. 019 ==> 403197 - Thumbnails in icon and preview views are not HiDPI (macOS). 020 ==> 370705 - Current Album is deselected when clearing search bar. 021 ==> 407232 - Swith from list to display with enter. 022 ==> 407262 - Face tagging doesn't work with rotation. 023 ==> 407345 - Verbose debug messages. 024 ==> 407334 - Accented characters not taken into account google photos. 025 ==> 406594 - Interface becomes irresponsive for a while after editing metadata in search panel. 026 ==> 407357 - Print Creator Gimp 2.10. not recgonized. 027 ==> 397865 - Current GIMP version cannot be selected. 028 ==> 407440 - After export to google photo, metadata are written to sidecar files (and perhaps to picture file too). 029 ==> 401100 - Release plan text not readable. 030 ==> 360475 - Open in file manager for individual images. 031 ==> 407565 - Entering multi tags to the same photo failed. 032 ==> 378112 - No progress bar while maint initializes. 033 ==> 407699 - Text format for "add tag" and "remove tag" is inconsistent. 034 ==> 407736 - Status Bar Not Visible in Full Screen. 035 ==> 407806 - Applied reverse geocoding are not saved. 036 ==> 407946 - Duplicates search should perform the search automatically. 037 ==> 407947 - Crash while updating fingerprints. 038 ==> 407993 - Feature request: feedback when deleting unassigned tags in Tag Manager. 039 ==> 407958 - In Thumbnail View Creation date is not correct. 040 ==> 407948 - Option to configure temp directory. 041 ==> 387770 - Plugin Instructions Unreadable in Some Themes. 042 ==> 397599 - Usability deteriorated. 043 ==> 388359 - Icons on the toolbar are not visible when background is dark. 044 ==> 408147 - From LightTable, "Open" picture does not open the right one. 045 ==> 408157 - "Tone Color Picker" cursor is not correctly positionned vs Mouse. 046 ==> 408095 - Sort items by manual. 047 ==> 378364 - Sequence number options need date-aware modifier. 048 ==> 408213 - '+' symbol is a legitimate win10 file character but unable to enter it into digiKam via Rename. 049 ==> 408261 - In Metadata editor, "Ok" and "Apply" buttons should save changes for all tabs, not just the active one. 050 ==> 408265 - The stacked images tool appears to be non-functional. 051 ==> 369657 - XMP subject matter code can be added twice. 052 ==> 408311 - Opening and closing downloading windows. 053 ==> 408334 - Windows elements design changed? 054 ==> 408332 - Disabled Export plugins visible in rightside toolbar. 055 ==> 342379 - Allow to use decimals values with custom size settings. 056 ==> 408450 - digikam git r43105 cannot build without marble. -057 ==> +057 ==> 408420 - Can't add images to Lightable. +058 ==> 408466 - When exporting >1 picture to Google Photos, all pictures have title & caption of the last one. +059 ==> diff --git a/README.DEVEL b/README.DEVEL index 834ede4815..146985e417 100644 --- a/README.DEVEL +++ b/README.DEVEL @@ -1,11 +1,11 @@ See The API documentation to generate with Doxygen. You will find all information about the dependencies, -the configuration options, the rules to compile and install, and all pointers to contribute to the project as developer. +the configuration options, the rules to compile and install, and all pointers to contribute to the project as a developer. Install Doxygen and Graphviz, and then run make doc from build directory. You can also run doxygen command line tool directly from this folder or consult the Mainpage.dox file. Online version: https://www.digikam.org/api/index.html For the todo list, see bugzilla for details which is the complete story of the project. https://bugs.kde.org/component-report.cgi?product=digikam diff --git a/core/dplugins/generic/webservices/google/gphoto/gptalker.cpp b/core/dplugins/generic/webservices/google/gphoto/gptalker.cpp index bb7bd5a985..01dfc56e74 100644 --- a/core/dplugins/generic/webservices/google/gphoto/gptalker.cpp +++ b/core/dplugins/generic/webservices/google/gphoto/gptalker.cpp @@ -1,948 +1,961 @@ /* ============================================================ * * This file is a part of digiKam project * https://www.digikam.org * * Date : 2007-16-07 * Description : a tool to export items to Google web services * * Copyright (C) 2007-2008 by Vardhman Jain * Copyright (C) 2008-2019 by Gilles Caulier * Copyright (C) 2009 by Luka Renko * Copyright (C) 2018 by Thanh Trung Dinh * * This program is free software; you can redistribute it * and/or modify it under the terms of the GNU General * Public License as published by the Free Software Foundation; * either version 2, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * ============================================================ */ #include "gptalker.h" // Qt includes #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // KDE includes #include // Local includes #include "wstoolutils.h" #include "digikam_version.h" #include "gswindow.h" #include "gpmpform.h" #include "digikam_debug.h" #include "previewloadthread.h" #include "dmetadata.h" using namespace Digikam; #define NB_MAX_ITEM_UPLOAD 50 namespace DigikamGenericGoogleServicesPlugin { static bool gphotoLessThan(const GSFolder& p1, const GSFolder& p2) { return (p1.title.toLower() < p2.title.toLower()); } class Q_DECL_HIDDEN GPTalker::Private { public: enum State { GP_LOGOUT = -1, GP_LISTALBUMS = 0, GP_GETUSER, GP_LISTPHOTOS, GP_ADDPHOTO, GP_UPDATEPHOTO, GP_UPLOADPHOTO, GP_GETPHOTO, GP_CREATEALBUM }; public: explicit Private() { state = GP_LOGOUT; netMngr = nullptr; userInfoUrl = QLatin1String("https://www.googleapis.com/plus/v1/people/me"); apiVersion = QLatin1String("v1"); apiUrl = QString::fromLatin1("https://photoslibrary.googleapis.com/%1/%2").arg(apiVersion); albumIdToUpload = QLatin1String("-1"); previousImageId = QLatin1String("-1"); } public: QString userInfoUrl; QString apiUrl; QString apiVersion; State state; - QString descriptionToUpload; QString albumIdToUpload; QString previousImageId; + + QStringList descriptionList; QStringList uploadTokenList; QList albumList; QNetworkAccessManager* netMngr; }; GPTalker::GPTalker(QWidget* const parent) : GSTalkerBase(parent, QStringList() // to get user login (temporary until gphoto supports it officially) << QLatin1String("https://www.googleapis.com/auth/plus.login") // to add and download photo in the library << QLatin1String("https://www.googleapis.com/auth/photoslibrary") // to download photo created by digiKam on GPhoto << QLatin1String("https://www.googleapis.com/auth/photoslibrary.readonly.appcreateddata") // for shared albums << QLatin1String("https://www.googleapis.com/auth/photoslibrary.sharing"), QLatin1String("GooglePhotos")), d(new Private) { m_reply = nullptr; d->netMngr = new QNetworkAccessManager(this); connect(d->netMngr, SIGNAL(finished(QNetworkReply*)), this, SLOT(slotFinished(QNetworkReply*))); connect(this, SIGNAL(signalError(QString)), this, SLOT(slotError(QString))); connect(this, SIGNAL(signalReadyToUpload()), this, SLOT(slotUploadPhoto())); } GPTalker::~GPTalker() { if (m_reply) { m_reply->abort(); } WSToolUtils::removeTemporaryDir("google"); delete d; } QStringList GPTalker::getUploadTokenList() { return d->uploadTokenList; } /** * (Trung): Comments below are not valid anymore with google photos api * Google Photo's Album listing request/response * First a request is sent to the url below and then we might(?) get a redirect URL * We then need to send the GET request to the Redirect url. * This uses the authenticated album list fetching to get all the albums included the unlisted-albums * which is not returned for an unauthorised request as done without the Authorization header. */ void GPTalker::listAlbums(const QString& nextPageToken) { if (m_reply) { m_reply->abort(); m_reply = nullptr; } qCDebug(DIGIKAM_WEBSERVICES_LOG) << "list albums"; QUrl url(d->apiUrl.arg(QLatin1String("albums"))); if (nextPageToken.isEmpty()) { d->albumList.clear(); } else { QUrlQuery query(url); query.addQueryItem(QLatin1String("pageToken"), nextPageToken); url.setQuery(query); } qCDebug(DIGIKAM_WEBSERVICES_LOG) << "url for list albums " << url; QNetworkRequest netRequest(url); netRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String("application/json")); netRequest.setRawHeader("Authorization", m_bearerAccessToken.toLatin1()); m_reply = d->netMngr->get(netRequest); d->state = Private::GP_LISTALBUMS; emit signalBusy(true); } /** * We get user profile from Google Plus API * This is a temporary solution until Google Photo support API for user profile */ void GPTalker::getLoggedInUser() { qCDebug(DIGIKAM_WEBSERVICES_LOG) << "getLoggedInUser"; if (m_reply) { m_reply->abort(); m_reply = nullptr; } QUrl url(d->userInfoUrl); qCDebug(DIGIKAM_WEBSERVICES_LOG) << "url for list albums " << url; qCDebug(DIGIKAM_WEBSERVICES_LOG) << "m_accessToken " << m_accessToken; QNetworkRequest netRequest(url); netRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String("application/json")); netRequest.setRawHeader("Authorization", m_bearerAccessToken.toLatin1()); m_reply = d->netMngr->get(netRequest); d->state = Private::GP_GETUSER; emit signalBusy(true); } void GPTalker::listPhotos(const QString& albumId, const QString& /*imgmax*/) { if (m_reply) { m_reply->abort(); m_reply = nullptr; } QUrl url(d->apiUrl.arg(QLatin1String("mediaItems:search"))); QNetworkRequest netRequest(url); netRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String("application/json")); netRequest.setRawHeader("Authorization", m_bearerAccessToken.toUtf8()); QByteArray data; data += "{\"pageSize\": \"100\","; data += "\"albumId\":\""; data += albumId.toUtf8(); data += "\"}"; qCDebug(DIGIKAM_WEBSERVICES_LOG) << "data to list photos : " << data; m_reply = d->netMngr->post(netRequest, data); d->state = Private::GP_LISTPHOTOS; emit signalBusy(true); } void GPTalker::createAlbum(const GSFolder& album) { if (m_reply) { m_reply->abort(); m_reply = nullptr; } // Create body in json QByteArray data; data += "{\"album\":"; data += "{\"title\":\""; data += album.title.toUtf8(); data += "\"}}"; qCDebug(DIGIKAM_WEBSERVICES_LOG) << data; QUrl url(d->apiUrl.arg(QLatin1String("albums"))); QNetworkRequest netRequest(url); netRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String("application/json")); netRequest.setRawHeader("Authorization", m_bearerAccessToken.toLatin1()); m_reply = d->netMngr->post(netRequest, data); d->state = Private::GP_CREATEALBUM; emit signalBusy(true); } /** * First a request is sent to the url below and then we will get an upload token * Upload token then will be sent with url in GPTlaker::uploadPhoto to create real photos on user account */ bool GPTalker::addPhoto(const QString& photoPath, GSPhoto& info, const QString& albumId, bool rescale, int maxDim, int imageQuality) { if (m_reply) { m_reply->abort(); m_reply = nullptr; } QUrl url(d->apiUrl.arg(QLatin1String("uploads"))); // Save album ID and description to upload - d->descriptionToUpload = info.description; - d->albumIdToUpload = albumId; + d->descriptionList << info.description; + d->albumIdToUpload = albumId; QString path(photoPath); QMimeDatabase mimeDB; if (mimeDB.mimeTypeForFile(photoPath).name().startsWith(QLatin1String("image/"))) { QImage image = PreviewLoadThread::loadHighQualitySynchronously(photoPath).copyQImage(); if (image.isNull()) { image.load(photoPath); } if (image.isNull()) { return false; } path = WSToolUtils::makeTemporaryDir("google").filePath(QFileInfo(photoPath) .baseName().trimmed() + QLatin1String(".jpg")); if (rescale && (image.width() > maxDim || image.height() > maxDim)) { image = image.scaled(maxDim, maxDim, Qt::KeepAspectRatio, Qt::SmoothTransformation); } image.save(path, "JPEG", imageQuality); DMetadata meta; if (meta.load(photoPath)) { meta.setItemDimensions(image.size()); meta.setItemOrientation(MetaEngine::ORIENTATION_NORMAL); meta.setMetadataWritingMode((int)DMetadata::WRITE_TO_FILE_ONLY); meta.save(path, true); } } // Create the body for temporary upload QFile imageFile(path); if (!imageFile.open(QIODevice::ReadOnly)) { return false; } QByteArray data = imageFile.readAll(); imageFile.close(); QString imageName = QUrl::fromLocalFile(path).fileName(); QNetworkRequest netRequest(url); netRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String("application/octet-stream")); netRequest.setRawHeader("Authorization", m_bearerAccessToken.toLatin1()); netRequest.setRawHeader("X-Goog-Upload-File-Name", imageName.toUtf8()); netRequest.setRawHeader("X-Goog-Upload-Protocol", "raw"); qCDebug(DIGIKAM_WEBSERVICES_LOG) << imageName; m_reply = d->netMngr->post(netRequest, data); d->state = Private::GP_ADDPHOTO; emit signalBusy(true); return true; } bool GPTalker::updatePhoto(const QString& photoPath, GSPhoto& info, /*const QString& albumId,*/ bool rescale, int maxDim, int imageQuality) { if (m_reply) { m_reply->abort(); m_reply = nullptr; } emit signalBusy(true); GPMPForm form; QString path = photoPath; QMimeDatabase mimeDB; if (mimeDB.mimeTypeForFile(path).name().startsWith(QLatin1String("image/"))) { QImage image = PreviewLoadThread::loadHighQualitySynchronously(photoPath).copyQImage(); if (image.isNull()) { image.load(photoPath); } if (image.isNull()) { emit signalBusy(false); return false; } path = WSToolUtils::makeTemporaryDir("google").filePath(QFileInfo(photoPath) .baseName().trimmed() + QLatin1String(".jpg")); if (rescale && (image.width() > maxDim || image.height() > maxDim)) { image = image.scaled(maxDim,maxDim, Qt::KeepAspectRatio,Qt::SmoothTransformation); } image.save(path, "JPEG", imageQuality); DMetadata meta; if (meta.load(photoPath)) { meta.setItemDimensions(image.size()); meta.setItemOrientation(MetaEngine::ORIENTATION_NORMAL); meta.setMetadataWritingMode((int)DMetadata::WRITE_TO_FILE_ONLY); meta.save(path, true); } } //Create the Body in atom-xml QDomDocument docMeta; QDomProcessingInstruction instr = docMeta.createProcessingInstruction( QLatin1String("xml"), QLatin1String("version='1.0' encoding='UTF-8'")); docMeta.appendChild(instr); QDomElement entryElem = docMeta.createElement(QLatin1String("entry")); docMeta.appendChild(entryElem); entryElem.setAttribute( QLatin1String("xmlns"), QLatin1String("http://www.w3.org/2005/Atom")); QDomElement titleElem = docMeta.createElement(QLatin1String("title")); entryElem.appendChild(titleElem); QDomText titleText = docMeta.createTextNode(QFileInfo(path).fileName()); titleElem.appendChild(titleText); QDomElement summaryElem = docMeta.createElement(QLatin1String("summary")); entryElem.appendChild(summaryElem); QDomText summaryText = docMeta.createTextNode(info.description); summaryElem.appendChild(summaryText); QDomElement categoryElem = docMeta.createElement(QLatin1String("category")); entryElem.appendChild(categoryElem); categoryElem.setAttribute( QLatin1String("scheme"), QLatin1String("http://schemas.google.com/g/2005#kind")); categoryElem.setAttribute( QLatin1String("term"), QLatin1String("http://schemas.google.com/photos/2007#photo")); QDomElement mediaGroupElem = docMeta.createElementNS( QLatin1String("http://search.yahoo.com/mrss/"), QLatin1String("media:group")); entryElem.appendChild(mediaGroupElem); QDomElement mediaKeywordsElem = docMeta.createElementNS( QLatin1String("http://search.yahoo.com/mrss/"), QLatin1String("media:keywords")); mediaGroupElem.appendChild(mediaKeywordsElem); QDomText mediaKeywordsText = docMeta.createTextNode(info.tags.join(QLatin1Char(','))); mediaKeywordsElem.appendChild(mediaKeywordsText); if (!info.gpsLat.isEmpty() && !info.gpsLon.isEmpty()) { QDomElement whereElem = docMeta.createElementNS( QLatin1String("http://www.georss.org/georss"), QLatin1String("georss:where")); entryElem.appendChild(whereElem); QDomElement pointElem = docMeta.createElementNS( QLatin1String("http://www.opengis.net/gml"), QLatin1String("gml:Point")); whereElem.appendChild(pointElem); QDomElement gpsElem = docMeta.createElementNS( QLatin1String("http://www.opengis.net/gml"), QLatin1String("gml:pos")); pointElem.appendChild(gpsElem); QDomText gpsVal = docMeta.createTextNode(info.gpsLat + QLatin1Char(' ') + info.gpsLon); gpsElem.appendChild(gpsVal); } form.addPair(QLatin1String("descr"), docMeta.toString(), QLatin1String("application/atom+xml")); if (!form.addFile(QLatin1String("photo"), path)) { emit signalBusy(false); return false; } form.finish(); QNetworkRequest netRequest(info.editUrl); netRequest.setHeader(QNetworkRequest::ContentTypeHeader, form.contentType()); netRequest.setRawHeader("Authorization", m_bearerAccessToken.toLatin1() + "\nIf-Match: *"); m_reply = d->netMngr->put(netRequest, form.formData()); d->state = Private::GP_UPDATEPHOTO; return true; } void GPTalker::getPhoto(const QString& imgPath) { if (m_reply) { m_reply->abort(); m_reply = nullptr; } emit signalBusy(true); QUrl url(imgPath); qCDebug(DIGIKAM_WEBSERVICES_LOG) << "link to get photo " << url.url(); m_reply = d->netMngr->get(QNetworkRequest(url)); d->state = Private::GP_GETPHOTO; } void GPTalker::cancel() { if (m_reply) { m_reply->abort(); m_reply = nullptr; } + d->descriptionList.clear(); + d->uploadTokenList.clear(); + emit signalBusy(false); } void GPTalker::slotError(const QString & error) { QString transError; int errorNo = 0; if (!error.isEmpty()) errorNo = error.toInt(); switch (errorNo) { case 2: transError=i18n("No photo specified"); break; case 3: transError=i18n("General upload failure"); break; case 4: transError=i18n("File-size was zero"); break; case 5: transError=i18n("File-type was not recognized"); break; case 6: transError=i18n("User exceeded upload limit"); break; case 96: transError=i18n("Invalid signature"); break; case 97: transError=i18n("Missing signature"); break; case 98: transError=i18n("Login failed / Invalid auth token"); break; case 100: transError=i18n("Invalid API Key"); break; case 105: transError=i18n("Service currently unavailable"); break; case 108: transError=i18n("Invalid Frob"); break; case 111: transError=i18n("Format \"xxx\" not found"); break; case 112: transError=i18n("Method \"xxx\" not found"); break; case 114: transError=i18n("Invalid SOAP envelope"); break; case 115: transError=i18n("Invalid XML-RPC Method Call"); break; case 116: transError=i18n("The POST method is now required for all setters."); break; default: transError=i18n("Unknown error"); }; QMessageBox::critical(QApplication::activeWindow(), i18nc("@title:window", "Error"), i18n("Error occurred: %1\nUnable to proceed further.",transError + error)); } void GPTalker::slotFinished(QNetworkReply* reply) { emit signalBusy(false); if (reply != m_reply) { return; } m_reply = nullptr; qCDebug(DIGIKAM_WEBSERVICES_LOG) << "reply error : " << reply->error() << " - " << reply->errorString(); if (reply->error() != QNetworkReply::NoError) { if (d->state == Private::GP_ADDPHOTO) { emit signalAddPhotoDone(reply->error(), reply->errorString()); } else { QMessageBox::critical(QApplication::activeWindow(), i18n("Error"), reply->errorString()); } reply->deleteLater(); return; } QByteArray buffer = reply->readAll(); switch (d->state) { case (Private::GP_LOGOUT): break; case (Private::GP_GETUSER): parseResponseGetLoggedInUser(buffer); break; case (Private::GP_CREATEALBUM): parseResponseCreateAlbum(buffer); break; case (Private::GP_LISTALBUMS): parseResponseListAlbums(buffer); break; case (Private::GP_LISTPHOTOS): parseResponseListPhotos(buffer); break; case (Private::GP_ADDPHOTO): parseResponseAddPhoto(buffer); break; case (Private::GP_UPDATEPHOTO): emit signalAddPhotoDone(1, QString()); break; case (Private::GP_UPLOADPHOTO): parseResponseUploadPhoto(buffer); break; case (Private::GP_GETPHOTO): // qCDebug(DIGIKAM_WEBSERVICES_LOG) << buffer; // all we get is data of the image emit signalGetPhotoDone(1, QString(), buffer); break; } reply->deleteLater(); } void GPTalker::slotUploadPhoto() { /* Keep track of number of items will be uploaded, because * Google Photo API upload maximum NB_MAX_ITEM_UPLOAD items in at a time */ int nbItemsUpload = 0; if (m_reply) { m_reply->abort(); m_reply = nullptr; } QUrl url(d->apiUrl.arg(QLatin1String("mediaItems:batchCreate"))); QByteArray data; data += '{'; if (d->albumIdToUpload != QLatin1String("-1")) { data += "\"albumId\": \""; data += d->albumIdToUpload.toLatin1(); data += "\","; } data += "\"newMediaItems\": ["; if (d->uploadTokenList.isEmpty()) { qCDebug(DIGIKAM_WEBSERVICES_LOG) << "token list is empty"; } while (!d->uploadTokenList.isEmpty() && nbItemsUpload < NB_MAX_ITEM_UPLOAD) { const QString& uploadToken = d->uploadTokenList.takeFirst(); data += "{\"description\": \""; - data += d->descriptionToUpload.toUtf8(); + + if (d->descriptionList.isEmpty()) + { + qCDebug(DIGIKAM_WEBSERVICES_LOG) << "description list is empty"; + } + else + { + data += d->descriptionList.takeFirst().toUtf8(); + } + data += "\","; data += "\"simpleMediaItem\": {"; data += "\"uploadToken\": \""; data += uploadToken.toUtf8(); data += "\"}}"; if (d->uploadTokenList.length() > 0) { data += ','; } nbItemsUpload ++; } if (d->previousImageId == QLatin1String("-1")) { data += ']'; } else { data += "],\"albumPosition\": {"; data += "\"position\": \"AFTER_MEDIA_ITEM\","; data += "\"relativeMediaItemId\": \""; data += d->previousImageId.toLatin1(); data += "\"}\r\n"; } data += "}\r\n"; qCDebug(DIGIKAM_WEBSERVICES_LOG) << data; QNetworkRequest netRequest(url); netRequest.setHeader(QNetworkRequest::ContentTypeHeader, QLatin1String("application/json")); netRequest.setRawHeader("Authorization", m_bearerAccessToken.toLatin1()); m_reply = d->netMngr->post(netRequest, data); d->state = Private::GP_UPLOADPHOTO; emit signalBusy(true); } void GPTalker::parseResponseListAlbums(const QByteArray& data) { qCDebug(DIGIKAM_WEBSERVICES_LOG) << "parseResponseListAlbums"; QJsonParseError err; QJsonDocument doc = QJsonDocument::fromJson(data, &err); if (err.error != QJsonParseError::NoError) { emit signalBusy(false); emit signalListAlbumsDone(0, QString::fromLatin1("Code: %1 - %2").arg(err.error) .arg(err.errorString()), QList()); return; } QJsonObject jsonObject = doc.object(); QJsonArray jsonArray = jsonObject[QLatin1String("albums")].toArray(); qCDebug(DIGIKAM_WEBSERVICES_LOG) << "json array " << doc; /** * Google-photos allows user to post photos on their main page (not in any albums) * so this folder is created for that purpose */ if (d->albumList.isEmpty()) { GSFolder mainPage; d->albumList.append(mainPage); } foreach (const QJsonValue& value, jsonArray) { GSFolder album; QJsonObject obj = value.toObject(); album.id = obj[QLatin1String("id")].toString(); album.title = obj[QLatin1String("title")].toString(); album.url = obj[QLatin1String("productUrl")].toString(); album.isWriteable = obj[QLatin1String("isWriteable")].toBool(); d->albumList.append(album); } QString nextPageToken = jsonObject[QLatin1String("nextPageToken")].toString(); if (!nextPageToken.isEmpty()) { listAlbums(nextPageToken); return; } std::sort(d->albumList.begin(), d->albumList.end(), gphotoLessThan); emit signalListAlbumsDone(1, QLatin1String(""), d->albumList); } void GPTalker::parseResponseListPhotos(const QByteArray& data) { qCDebug(DIGIKAM_WEBSERVICES_LOG) << "parseResponseListPhotos"; QJsonParseError err; QJsonDocument doc = QJsonDocument::fromJson(data, &err); if (err.error != QJsonParseError::NoError) { emit signalBusy(false); emit signalListPhotosDone(0, i18n("Failed to fetch photo-set list"), QList()); qCDebug(DIGIKAM_WEBSERVICES_LOG) << "error code: " << err.error << ", msg: " << err.errorString(); return; } QJsonObject jsonObject = doc.object(); QJsonArray jsonArray = jsonObject[QLatin1String("mediaItems")].toArray(); QList photoList; foreach (const QJsonValue& value, jsonArray) { QJsonObject obj = value.toObject(); GSPhoto photo; photo.baseUrl = obj[QLatin1String("baseUrl")].toString(); photo.description = obj[QLatin1String("description")].toString(); photo.id = obj[QLatin1String("id")].toString(); photo.mimeType = obj[QLatin1String("mimeType")].toString(); photo.location = obj[QLatin1String("Location")].toString(); // Not yet available in v1 but will be in the future QJsonObject metadata = obj[QLatin1String("mediaMetadata")].toObject(); photo.creationTime = metadata[QLatin1String("creationTime")].toString(); photo.width = metadata[QLatin1String("width")].toString(); photo.height = metadata[QLatin1String("height")].toString(); photo.originalURL = QUrl(photo.baseUrl + QLatin1String("=d")); qCDebug(DIGIKAM_WEBSERVICES_LOG) << photo.originalURL.url(); photoList.append(photo); } emit signalListPhotosDone(1, QLatin1String(""), photoList); } void GPTalker::parseResponseCreateAlbum(const QByteArray& data) { qCDebug(DIGIKAM_WEBSERVICES_LOG) << "parseResponseCreateAlbums"; QJsonParseError err; QJsonDocument doc = QJsonDocument::fromJson(data, &err); if (err.error != QJsonParseError::NoError) { emit signalBusy(false); emit signalCreateAlbumDone(0, QString::fromLatin1("Code: %1 - %2").arg(err.error) .arg(err.errorString()), QString()); return; } QJsonObject jsonObject = doc.object(); QString albumId = jsonObject[QLatin1String("id")].toString(); qCDebug(DIGIKAM_WEBSERVICES_LOG) << "album Id " << doc; emit signalCreateAlbumDone(1, QLatin1String(""), albumId); } void GPTalker::parseResponseAddPhoto(const QByteArray& data) { qCDebug(DIGIKAM_WEBSERVICES_LOG) << "parseResponseAddPhoto"; qCDebug(DIGIKAM_WEBSERVICES_LOG) << "response " << data; d->uploadTokenList << QString::fromUtf8(data); emit signalAddPhotoDone(1, QString()); } void GPTalker::parseResponseGetLoggedInUser(const QByteArray& data) { qCDebug(DIGIKAM_WEBSERVICES_LOG) << "parseResponseGetLoggedInUser"; QJsonParseError err; QJsonDocument doc = QJsonDocument::fromJson(data, &err); if (err.error != QJsonParseError::NoError) { emit signalBusy(false); return; } QJsonObject jsonObject = doc.object(); QString userName = jsonObject[QLatin1String("displayName")].toString(); emit signalSetUserName(userName); listAlbums(); } //TODO: Parse and return photoID void GPTalker::parseResponseUploadPhoto(const QByteArray& data) { qCDebug(DIGIKAM_WEBSERVICES_LOG) << "parseResponseUploadPhoto"; QJsonParseError err; QJsonDocument doc = QJsonDocument::fromJson(data, &err); qCDebug(DIGIKAM_WEBSERVICES_LOG) << "doc " << doc; if (err.error != QJsonParseError::NoError) { emit signalBusy(false); emit signalUploadPhotoDone(0, err.errorString(), QStringList()); return; } QJsonObject jsonObject = doc.object(); QJsonArray jsonArray = jsonObject[QLatin1String("newMediaItemResults")].toArray(); QStringList listPhotoId; foreach (const QJsonValue& value, jsonArray) { QJsonObject obj = value.toObject(); QJsonObject mediaItem = obj[QLatin1String("mediaItem")].toObject(); listPhotoId << mediaItem[QLatin1String("id")].toString(); } d->previousImageId = listPhotoId.last(); qCDebug(DIGIKAM_WEBSERVICES_LOG) << "list photo Id " << listPhotoId.join(QLatin1String(", ")); emit signalBusy(false); emit signalUploadPhotoDone(1, QString(), listPhotoId); } } // namespace DigikamGenericGoogleServicesPlugin diff --git a/core/dplugins/generic/webservices/google/gswindow.cpp b/core/dplugins/generic/webservices/google/gswindow.cpp index e42b01052c..431451b1e4 100644 --- a/core/dplugins/generic/webservices/google/gswindow.cpp +++ b/core/dplugins/generic/webservices/google/gswindow.cpp @@ -1,1265 +1,1265 @@ /* ============================================================ * * This file is a part of digiKam project * https://www.digikam.org * * Date : 2013-11-18 * Description : a tool to export items to Google web services * * Copyright (C) 2013 by Pankaj Kumar * Copyright (C) 2015 by Shourya Singh Gupta * Copyright (C) 2013-2018 by Caulier Gilles * * This program is free software; you can redistribute it * and/or modify it under the terms of the GNU General * Public License as published by the Free Software Foundation; * either version 2, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * ============================================================ */ #include "gswindow.h" // Qt includes #include #include #include #include #include #include #include #include #include #include #include #include #include // KDE includes #include #include #include // Local includes #include "wstoolutils.h" #include "ditemslist.h" #include "digikam_version.h" #include "dprogresswdg.h" #include "gdtalker.h" #include "gsitem.h" #include "gsnewalbumdlg.h" #include "gswidget.h" #include "gptalker.h" #include "gsreplacedlg.h" #include "digikam_debug.h" namespace DigikamGenericGoogleServicesPlugin { class Q_DECL_HIDDEN GSWindow::Private { public: explicit Private() { widget = nullptr; albumDlg = nullptr; gphotoAlbumDlg = nullptr; talker = nullptr; gphotoTalker = nullptr; iface = nullptr; imagesCount = 0; imagesTotal = 0; renamingOpt = 0; service = GoogleService::GPhotoImport; } unsigned int imagesCount; unsigned int imagesTotal; int renamingOpt; QString serviceName; QString toolName; GoogleService service; QString tmp; GSWidget* widget; GSNewAlbumDlg* albumDlg; GSNewAlbumDlg* gphotoAlbumDlg; GDTalker* talker; GPTalker* gphotoTalker; QString currentAlbumId; QList< QPair > transferQueue; QList< QPair > uploadQueue; DInfoInterface* iface; DMetadata meta; }; GSWindow::GSWindow(DInfoInterface* const iface, QWidget* const /*parent*/, const QString& serviceName) : WSToolDialog(nullptr, QString::fromLatin1("%1Export Dialog").arg(serviceName)), d(new Private) { d->iface = iface; d->serviceName = serviceName; if (QString::compare(d->serviceName, QLatin1String("googledriveexport"), Qt::CaseInsensitive) == 0) { d->service = GoogleService::GDrive; d->toolName = QLatin1String("Google Drive"); } else if (QString::compare(d->serviceName, QLatin1String("googlephotoexport"), Qt::CaseInsensitive) == 0) { d->service = GoogleService::GPhotoExport; d->toolName = QLatin1String("Google Photos"); } else { d->service = GoogleService::GPhotoImport; d->toolName = QLatin1String("Google Photos"); } d->tmp = WSToolUtils::makeTemporaryDir("google").absolutePath() + QLatin1Char('/');; d->widget = new GSWidget(this, d->iface, d->service, d->toolName); setMainWidget(d->widget); setModal(false); switch (d->service) { case GoogleService::GDrive: setWindowTitle(i18n("Export to Google Drive")); startButton()->setText(i18n("Start Upload")); startButton()->setToolTip(i18n("Start upload to Google Drive")); d->widget->setMinimumSize(700,500); d->albumDlg = new GSNewAlbumDlg(this, d->serviceName, d->toolName); d->talker = new GDTalker(this); connect(d->talker,SIGNAL(signalBusy(bool)), this,SLOT(slotBusy(bool))); connect(d->talker,SIGNAL(signalAccessTokenObtained()), this,SLOT(slotAccessTokenObtained())); connect(d->talker, SIGNAL(signalAuthenticationRefused()), this,SLOT(slotAuthenticationRefused())); connect(d->talker,SIGNAL(signalSetUserName(QString)), this,SLOT(slotSetUserName(QString))); connect(d->talker,SIGNAL(signalListAlbumsDone(int,QString,QList)), this,SLOT(slotListAlbumsDone(int,QString,QList))); connect(d->talker,SIGNAL(signalCreateFolderDone(int,QString)), this,SLOT(slotCreateFolderDone(int,QString))); connect(d->talker,SIGNAL(signalAddPhotoDone(int,QString)), this,SLOT(slotAddPhotoDone(int,QString))); connect(d->talker, SIGNAL(signalUploadPhotoDone(int,QString,QStringList)), this, SLOT(slotUploadPhotoDone(int,QString,QStringList))); readSettings(); buttonStateChange(false); d->talker->doOAuth(); break; case GoogleService::GPhotoImport: case GoogleService::GPhotoExport: if (d->service == GoogleService::GPhotoExport) { setWindowTitle(i18n("Export to Google Photos Service")); startButton()->setText(i18n("Start Upload")); startButton()->setToolTip(i18n("Start upload to Google Photos Service")); d->widget->setMinimumSize(700, 500); } else { setWindowTitle(i18n("Import from Google Photos Service")); startButton()->setText(i18n("Start Download")); startButton()->setToolTip(i18n("Start download from Google Photos service")); d->widget->setMinimumSize(300, 400); } d->gphotoAlbumDlg = new GSNewAlbumDlg(this, d->serviceName, d->toolName); d->gphotoTalker = new GPTalker(this); connect(d->gphotoTalker, SIGNAL(signalBusy(bool)), this, SLOT(slotBusy(bool))); connect(d->gphotoTalker,SIGNAL(signalSetUserName(QString)), this,SLOT(slotSetUserName(QString))); connect(d->gphotoTalker, SIGNAL(signalAccessTokenObtained()), this, SLOT(slotAccessTokenObtained())); connect(d->gphotoTalker, SIGNAL(signalAuthenticationRefused()), this,SLOT(slotAuthenticationRefused())); connect(d->gphotoTalker, SIGNAL(signalListAlbumsDone(int,QString,QList)), this, SLOT(slotListAlbumsDone(int,QString,QList))); connect(d->gphotoTalker, SIGNAL(signalCreateAlbumDone(int,QString,QString)), this, SLOT(slotCreateFolderDone(int,QString,QString))); connect(d->gphotoTalker, SIGNAL(signalAddPhotoDone(int,QString)), this, SLOT(slotAddPhotoDone(int,QString))); connect(d->gphotoTalker, SIGNAL(signalUploadPhotoDone(int,QString,QStringList)), this, SLOT(slotUploadPhotoDone(int,QString,QStringList))); connect(d->gphotoTalker, SIGNAL(signalGetPhotoDone(int,QString,QByteArray)), this, SLOT(slotGetPhotoDone(int,QString,QByteArray))); readSettings(); buttonStateChange(false); d->gphotoTalker->doOAuth(); break; } connect(d->widget->imagesList(), SIGNAL(signalImageListChanged()), this, SLOT(slotImageListChanged())); connect(d->widget->getChangeUserBtn(), SIGNAL(clicked()), this, SLOT(slotUserChangeRequest())); connect(d->widget->getNewAlbmBtn(), SIGNAL(clicked()), this,SLOT(slotNewAlbumRequest())); connect(d->widget->getReloadBtn(), SIGNAL(clicked()), this, SLOT(slotReloadAlbumsRequest())); connect(startButton(), SIGNAL(clicked()), this, SLOT(slotStartTransfer())); connect(this, SIGNAL(finished(int)), this, SLOT(slotFinished())); } GSWindow::~GSWindow() { delete d->widget; delete d->albumDlg; delete d->gphotoAlbumDlg; delete d->talker; delete d->gphotoTalker; delete d; } void GSWindow::reactivate() { d->widget->imagesList()->loadImagesFromCurrentSelection(); d->widget->progressBar()->hide(); show(); } void GSWindow::readSettings() { KConfig config; KConfigGroup grp; switch (d->service) { case GoogleService::GDrive: grp = config.group("Google Drive Settings"); break; default: grp = config.group("Google Photo Settings"); break; } d->currentAlbumId = grp.readEntry("Current Album", QString()); if (grp.readEntry("Resize", false)) { d->widget->getResizeCheckBox()->setChecked(true); d->widget->getDimensionSpB()->setEnabled(true); } else { d->widget->getResizeCheckBox()->setChecked(false); d->widget->getDimensionSpB()->setEnabled(false); } d->widget->getPhotoIdCheckBox()->setChecked(grp.readEntry("Write PhotoID", true)); d->widget->getDimensionSpB()->setValue(grp.readEntry("Maximum Width", 1600)); d->widget->getImgQualitySpB()->setValue(grp.readEntry("Image Quality", 90)); if (d->service == GoogleService::GPhotoExport && d->widget->m_tagsBGrp) { d->widget->m_tagsBGrp->button(grp.readEntry("Tag Paths", 0))->setChecked(true); } KConfigGroup dialogGroup = config.group(QString::fromLatin1("%1Export Dialog").arg(d->serviceName)); winId(); KWindowConfig::restoreWindowSize(windowHandle(), dialogGroup); resize(windowHandle()->size()); } void GSWindow::writeSettings() { KConfig config; KConfigGroup grp; switch (d->service) { case GoogleService::GDrive: grp = config.group("Google Drive Settings"); break; default: grp = config.group("Google Photo Settings"); break; } grp.writeEntry("Current Album", d->currentAlbumId); grp.writeEntry("Resize", d->widget->getResizeCheckBox()->isChecked()); grp.writeEntry("Write PhotoID", d->widget->getPhotoIdCheckBox()->isChecked()); grp.writeEntry("Maximum Width", d->widget->getDimensionSpB()->value()); grp.writeEntry("Image Quality", d->widget->getImgQualitySpB()->value()); if (d->service == GoogleService::GPhotoExport && d->widget->m_tagsBGrp) { grp.writeEntry("Tag Paths", d->widget->m_tagsBGrp->checkedId()); } KConfigGroup dialogGroup = config.group(QString::fromLatin1("%1Export Dialog").arg(d->serviceName)); KWindowConfig::saveWindowSize(windowHandle(), dialogGroup); config.sync(); } void GSWindow::slotSetUserName(const QString& msg) { d->widget->updateLabels(msg); } void GSWindow::slotListPhotosDoneForDownload(int errCode, const QString& errMsg, const QList & photosList) { disconnect(d->gphotoTalker, SIGNAL(signalListPhotosDone(int,QString,QList)), this, SLOT(slotListPhotosDoneForDownload(int,QString,QList))); if (errCode == 0) { QMessageBox::critical(this, i18nc("@title:window", "Error"), i18n("Google Photos call failed: %1\n", errMsg)); return; } typedef QPair Pair; d->transferQueue.clear(); QList::const_iterator itPWP; for (itPWP = photosList.begin() ; itPWP != photosList.end() ; ++itPWP) { d->transferQueue.append(Pair((*itPWP).originalURL, (*itPWP))); } if (d->transferQueue.isEmpty()) return; d->currentAlbumId = d->widget->getAlbumsCoB()->itemData(d->widget->getAlbumsCoB()->currentIndex()).toString(); d->imagesTotal = d->transferQueue.count(); d->imagesCount = 0; d->widget->progressBar()->setFormat(i18n("%v / %m")); d->widget->progressBar()->show(); d->renamingOpt = 0; // start download with first photo in queue downloadNextPhoto(); } void GSWindow::slotListAlbumsDone(int code, const QString& errMsg, const QList & list) { switch (d->service) { case GoogleService::GDrive: if (code == 0) { QMessageBox::critical(this, i18nc("@title:window", "Error"), i18n("Google Drive call failed: %1\n", errMsg)); return; } d->widget->getAlbumsCoB()->clear(); for (int i = 0 ; i < list.size() ; ++i) { d->widget->getAlbumsCoB()->addItem(QIcon::fromTheme(QLatin1String("system-users")), list.value(i).title, list.value(i).id); if (d->currentAlbumId == list.value(i).id) { d->widget->getAlbumsCoB()->setCurrentIndex(i); } } buttonStateChange(true); d->talker->getUserName(); break; default: if (code == 0) { QMessageBox::critical(this, i18nc("@title:window", "Error"), i18n("Google Photos call failed: %1\n", errMsg)); return; } d->widget->getAlbumsCoB()->clear(); for (int i = 0 ; i < list.size() ; ++i) { QString albumIcon; if (list.at(i).isWriteable) { albumIcon = QLatin1String("folder"); } else { albumIcon = QLatin1String("folder-locked"); } d->widget->getAlbumsCoB()->addItem(QIcon::fromTheme(albumIcon), list.at(i).title, list.at(i).id); if (d->currentAlbumId == list.at(i).id) d->widget->getAlbumsCoB()->setCurrentIndex(i); buttonStateChange(true); } break; } } void GSWindow::slotBusy(bool val) { if (val) { setCursor(Qt::WaitCursor); d->widget->getChangeUserBtn()->setEnabled(false); buttonStateChange(false); } else { setCursor(Qt::ArrowCursor); d->widget->getChangeUserBtn()->setEnabled(true); buttonStateChange(true); } } void GSWindow::slotStartTransfer() { d->widget->imagesList()->clearProcessedStatus(); switch (d->service) { case GoogleService::GDrive: case GoogleService::GPhotoExport: if (d->widget->imagesList()->imageUrls().isEmpty()) { QMessageBox::critical(this, i18nc("@title:window", "Error"), i18n("No image selected. Please select which images should be uploaded.")); return; } break; case GoogleService::GPhotoImport: break; } switch (d->service) { case GoogleService::GDrive: if (!(d->talker->authenticated())) { QPointer warn = new QMessageBox(QMessageBox::Warning, i18n("Warning"), i18n("Authentication failed. Click \"Continue\" to authenticate."), QMessageBox::Yes | QMessageBox::No); (warn->button(QMessageBox::Yes))->setText(i18n("Continue")); (warn->button(QMessageBox::No))->setText(i18n("Cancel")); if (warn->exec() == QMessageBox::Yes) { d->talker->doOAuth(); delete warn; return; } else { delete warn; return; } } break; default: if (!(d->gphotoTalker->authenticated())) { QPointer warn = new QMessageBox(QMessageBox::Warning, i18n("Warning"), i18n("Authentication failed. Click \"Continue\" to authenticate."), QMessageBox::Yes | QMessageBox::No); (warn->button(QMessageBox::Yes))->setText(i18n("Continue")); (warn->button(QMessageBox::No))->setText(i18n("Cancel")); if (warn->exec() == QMessageBox::Yes) { d->gphotoTalker->doOAuth(); delete warn; return; } else { delete warn; return; } } if (d->service == GoogleService::GPhotoImport) { qCDebug(DIGIKAM_WEBSERVICES_LOG) << "Google Photo Transfer invoked"; // list photos of the album, then start download connect(d->gphotoTalker, SIGNAL(signalListPhotosDone(int,QString,QList)), this, SLOT(slotListPhotosDoneForDownload(int,QString,QList))); d->gphotoTalker->listPhotos( d->widget->getAlbumsCoB()->itemData(d->widget->getAlbumsCoB()->currentIndex()).toString(), d->widget->getDimensionCoB()->itemData(d->widget->getDimensionCoB()->currentIndex()).toString()); return; } } typedef QPair Pair; for (int i = 0 ; i < (d->widget->imagesList()->imageUrls().size()) ; ++i) { DItemInfo info(d->iface->itemInfo(d->widget->imagesList()->imageUrls().value(i))); GSPhoto temp; qCDebug(DIGIKAM_WEBSERVICES_LOG) << "in start transfer info " <service) { case GoogleService::GDrive: temp.title = info.title(); temp.description = info.comment().section(QLatin1String("\n"), 0, 0); break; default: temp.title = info.name(); // Google Photo doesn't support image titles. Include it in descriptions if needed. QStringList descriptions = QStringList() << info.title() << info.comment(); descriptions.removeAll(QLatin1String("")); temp.description = descriptions.join(QLatin1String("\n\n")); break; } temp.gpsLat.setNum(info.latitude()); temp.gpsLon.setNum(info.longitude()); temp.tags = info.tagsPath(); - d->transferQueue.append(Pair(d->widget->imagesList()->imageUrls().value(i),temp)); + d->transferQueue.append(Pair(d->widget->imagesList()->imageUrls().value(i), temp)); } d->currentAlbumId = d->widget->getAlbumsCoB()->itemData(d->widget->getAlbumsCoB()->currentIndex()).toString(); d->imagesTotal = d->transferQueue.count(); d->imagesCount = 0; d->widget->progressBar()->setFormat(i18n("%v / %m")); d->widget->progressBar()->setMaximum(d->imagesTotal); d->widget->progressBar()->setValue(0); d->widget->progressBar()->show(); switch (d->service) { case GoogleService::GDrive: d->widget->progressBar()->progressScheduled(i18n("Google Drive export"), true, true); d->widget->progressBar()->progressThumbnailChanged( QIcon::fromTheme(QLatin1String("dk-googledrive")).pixmap(22, 22)); break; default: d->widget->progressBar()->progressScheduled(i18n("Google Photo export"), true, true); d->widget->progressBar()->progressThumbnailChanged( QIcon::fromTheme((QLatin1String("dk-googlephoto"))).pixmap(22, 22)); break; } uploadNextPhoto(); } void GSWindow::uploadNextPhoto() { qCDebug(DIGIKAM_WEBSERVICES_LOG) << "in upload nextphoto " << d->transferQueue.count(); if (d->transferQueue.isEmpty()) { //d->widget->progressBar()->hide(); d->widget->progressBar()->progressCompleted(); /** * Now all raw photos have been added, * for GPhoto: prepare to upload on user account * for GDrive: get listPhotoId to write metadata and finish upload */ if (d->service == GoogleService::GPhotoExport) { emit d->gphotoTalker->signalReadyToUpload(); } else { emit d->talker->signalReadyToUpload(); } return; } typedef QPair Pair; Pair pathComments = d->transferQueue.first(); GSPhoto info = pathComments.second; bool res = true; d->widget->imagesList()->processing(pathComments.first); switch (d->service) { case GoogleService::GDrive: { res = d->talker->addPhoto(pathComments.first.toLocalFile(), info, d->currentAlbumId, d->widget->getResizeCheckBox()->isChecked(), d->widget->getDimensionSpB()->value(), d->widget->getImgQualitySpB()->value()); break; } case GoogleService::GPhotoExport: { bool bCancel = false; bool bAdd = true; if (!info.id.isEmpty() && !info.editUrl.isEmpty()) { switch (d->renamingOpt) { case PWR_ADD_ALL: bAdd = true; break; case PWR_REPLACE_ALL: bAdd = false; break; default: { QPointer dlg = new ReplaceDialog(this, QLatin1String(""), d->iface, pathComments.first, info.thumbURL); dlg->exec(); switch (dlg->getResult()) { case PWR_ADD_ALL: d->renamingOpt = PWR_ADD_ALL; break; case PWR_ADD: bAdd = true; break; case PWR_REPLACE_ALL: d->renamingOpt = PWR_REPLACE_ALL; break; case PWR_REPLACE: bAdd = false; break; case PWR_CANCEL: default: bCancel = true; break; } delete dlg; break; } } } // adjust tags according to radio button clicked if (d->widget->m_tagsBGrp) { switch (d->widget->m_tagsBGrp->checkedId()) { case GPTagLeaf: { QStringList newTags; QStringList::const_iterator itT; for (itT = info.tags.constBegin() ; itT != info.tags.constEnd() ; ++itT) { QString strTmp = *itT; int idx = strTmp.lastIndexOf(QLatin1Char('/')); if (idx > 0) { strTmp.remove(0, idx + 1); } newTags.append(strTmp); } info.tags = newTags; break; } case GPTagSplit: { QSet newTagsSet; QStringList::const_iterator itT; for (itT = info.tags.constBegin() ; itT != info.tags.constEnd() ; ++itT) { QStringList strListTmp = itT->split(QLatin1Char('/')); QStringList::const_iterator itT2; for (itT2 = strListTmp.constBegin() ; itT2 != strListTmp.constEnd() ; ++itT2) { if (!newTagsSet.contains(*itT2)) { newTagsSet.insert(*itT2); } } } info.tags.clear(); QSet::const_iterator itT3; for (itT3 = newTagsSet.begin() ; itT3 != newTagsSet.end() ; ++itT3) { info.tags.append(*itT3); } break; } case GPTagCombined: default: break; } } if (bCancel) { slotTransferCancel(); res = true; } else { if (bAdd) { res = d->gphotoTalker->addPhoto(pathComments.first.toLocalFile(), info, d->currentAlbumId, d->widget->getResizeCheckBox()->isChecked(), d->widget->getDimensionSpB()->value(), d->widget->getImgQualitySpB()->value()); } else { res = d->gphotoTalker->updatePhoto(pathComments.first.toLocalFile(), info, d->widget->getResizeCheckBox()->isChecked(), d->widget->getDimensionSpB()->value(), d->widget->getImgQualitySpB()->value()); } } break; } case GoogleService::GPhotoImport: break; } if (!res) { slotAddPhotoDone(0, QLatin1String("")); return; } } void GSWindow::downloadNextPhoto() { if (d->transferQueue.isEmpty()) { d->widget->progressBar()->hide(); d->widget->progressBar()->progressCompleted(); return; } d->widget->progressBar()->setMaximum(d->imagesTotal); d->widget->progressBar()->setValue(d->imagesCount); QString imgPath = d->transferQueue.first().first.url(); d->gphotoTalker->getPhoto(imgPath); } void GSWindow::slotGetPhotoDone(int errCode, const QString& errMsg, const QByteArray& photoData) { GSPhoto item = d->transferQueue.first().second; /** * (Trung) * Google Photo API now does not support title for image, so we use creation time for image name instead */ QString itemName(item.title); QString suffix(item.mimeType.section(QLatin1Char('/'), -1)); if (item.title.isEmpty()) { itemName = QString::fromLatin1("image-%1").arg(item.creationTime); // Replace colon for Windows file systems itemName.replace(QLatin1Char(':'), QLatin1Char('-')); } QUrl tmpUrl = QUrl::fromLocalFile(QString(d->tmp + itemName + QLatin1Char('.') + suffix)); if (errCode == 1) { QString errText; QFile imgFile(tmpUrl.toLocalFile()); if (!imgFile.open(QIODevice::WriteOnly)) { errText = imgFile.errorString(); qCDebug(DIGIKAM_WEBSERVICES_LOG) << "error write"; } else if (imgFile.write(photoData) != photoData.size()) { errText = imgFile.errorString(); } else { imgFile.close(); } if (errText.isEmpty()) { if (d->meta.load(tmpUrl.toLocalFile())) { if (d->meta.supportXmp() && d->meta.canWriteXmp(tmpUrl.toLocalFile())) { d->meta.setXmpTagString("Xmp.digiKam.picasawebGPhotoId", item.id); d->meta.setXmpKeywords(item.tags); } if (!item.gpsLat.isEmpty() && !item.gpsLon.isEmpty()) { d->meta.setGPSInfo(0.0, item.gpsLat.toDouble(), item.gpsLon.toDouble()); } d->meta.setMetadataWritingMode((int)DMetadata::WRITE_TO_FILE_ONLY); d->meta.save(tmpUrl.toLocalFile()); } d->transferQueue.removeFirst(); d->imagesCount++; } else { QPointer warn = new QMessageBox(QMessageBox::Warning, i18n("Warning"), i18n("Failed to save photo: %1\n" "Do you want to continue?", errText), QMessageBox::Yes | QMessageBox::No); (warn->button(QMessageBox::Yes))->setText(i18n("Continue")); (warn->button(QMessageBox::No))->setText(i18n("Cancel")); if (warn->exec() != QMessageBox::Yes) { slotTransferCancel(); delete warn; return; } delete warn; } } else { QPointer warn = new QMessageBox(QMessageBox::Warning, i18n("Warning"), i18n("Failed to download photo: %1\n" "Do you want to continue?", errMsg), QMessageBox::Yes | QMessageBox::No); (warn->button(QMessageBox::Yes))->setText(i18n("Continue")); (warn->button(QMessageBox::No))->setText(i18n("Cancel")); if (warn->exec() != QMessageBox::Yes) { slotTransferCancel(); delete warn; return; } delete warn; } QUrl newUrl = QUrl::fromLocalFile(QString::fromLatin1("%1/%2").arg(d->widget->getDestinationPath()) .arg(tmpUrl.fileName())); qCDebug(DIGIKAM_WEBSERVICES_LOG) << "location " << newUrl.url(); QFileInfo targetInfo(newUrl.toLocalFile()); if (targetInfo.exists()) { int i = 0; bool fileFound = false; do { QFileInfo newTargetInfo(newUrl.toLocalFile()); if (!newTargetInfo.exists()) { fileFound = false; } else { newUrl = newUrl.adjusted(QUrl::RemoveFilename); newUrl.setPath(newUrl.path() + targetInfo.completeBaseName() + QString::fromUtf8("_%1.").arg(++i) + targetInfo.completeSuffix()); fileFound = true; } } while (fileFound); } if (!QFile::rename(tmpUrl.toLocalFile(), newUrl.toLocalFile())) { QMessageBox::critical(this, i18nc("@title:window", "Error"), i18n("Failed to save image to %1", newUrl.toLocalFile())); } downloadNextPhoto(); } void GSWindow::slotAddPhotoDone(int err, const QString& msg) { if (err == 0) { d->widget->imagesList()->processed(d->transferQueue.first().first,false); QPointer warn = new QMessageBox(QMessageBox::Warning, i18n("Warning"), i18n("Failed to upload photo to %1.\n%2\n" "Do you want to continue?", d->toolName, msg), QMessageBox::Yes | QMessageBox::No); (warn->button(QMessageBox::Yes))->setText(i18n("Continue")); (warn->button(QMessageBox::No))->setText(i18n("Cancel")); if (warn->exec() != QMessageBox::Yes) { d->transferQueue.clear(); d->widget->progressBar()->hide(); } else { d->transferQueue.removeFirst(); d->imagesTotal--; d->widget->progressBar()->setMaximum(d->imagesTotal); d->widget->progressBar()->setValue(d->imagesCount); uploadNextPhoto(); } delete warn; } else { /** * (Trung) Take first item out of transferQueue and append to uploadQueue, * in order to use it again to write id in slotUploadPhotoDone */ QPair item = d->transferQueue.first(); d->uploadQueue.append(item); // Remove photo uploaded from the transfer queue d->transferQueue.removeFirst(); d->imagesCount++; qCDebug(DIGIKAM_WEBSERVICES_LOG) << "In slotAddPhotoSucceeded" << d->imagesCount; d->widget->progressBar()->setMaximum(d->imagesTotal); d->widget->progressBar()->setValue(d->imagesCount); uploadNextPhoto(); } } void GSWindow::slotUploadPhotoDone(int err, const QString& msg, const QStringList& listPhotoId) { if (err == 0) { QPointer warn = new QMessageBox(QMessageBox::Warning, i18n("Warning"), i18n("Failed to finish uploading photo to %1.\n%2\n" "No image uploaded to your account.", d->toolName, msg), QMessageBox::Yes); (warn->button(QMessageBox::Yes))->setText(i18n("OK")); d->uploadQueue.clear(); d->widget->progressBar()->hide(); delete warn; } else { foreach (const QString& photoId, listPhotoId) { // Remove image from upload list and from UI QPair item = d->uploadQueue.takeFirst(); d->widget->imagesList()->removeItemByUrl(item.first); QUrl fileUrl = item.first; qCDebug(DIGIKAM_WEBSERVICES_LOG) << "photoID: " << photoId; if (d->widget->getPhotoIdCheckBox()->isChecked() && d->meta.supportXmp() && d->meta.canWriteXmp(fileUrl.toLocalFile()) && d->meta.load(fileUrl.toLocalFile()) && !photoId.isEmpty()) { d->meta.setXmpTagString("Xmp.digiKam.picasawebGPhotoId", photoId); d->meta.save(fileUrl.toLocalFile()); } } if (!d->widget->imagesList()->imageUrls().isEmpty()) { qCDebug(DIGIKAM_WEBSERVICES_LOG) << "continue to upload"; emit d->gphotoTalker->signalReadyToUpload(); } } } void GSWindow::slotImageListChanged() { startButton()->setEnabled(!(d->widget->imagesList()->imageUrls().isEmpty())); } void GSWindow::slotNewAlbumRequest() { switch (d->service) { case GoogleService::GDrive: if (d->albumDlg->exec() == QDialog::Accepted) { GSFolder newFolder; d->albumDlg->getAlbumProperties(newFolder); d->currentAlbumId = d->widget->getAlbumsCoB()->itemData(d->widget->getAlbumsCoB()->currentIndex()).toString(); d->talker->createFolder(newFolder.title, d->currentAlbumId); } break; default: if (d->gphotoAlbumDlg->exec() == QDialog::Accepted) { GSFolder newFolder; d->gphotoAlbumDlg->getAlbumProperties(newFolder); d->gphotoTalker->createAlbum(newFolder); } break; } } void GSWindow::slotReloadAlbumsRequest() { switch (d->service) { case GoogleService::GDrive: d->talker->listFolders(); break; case GoogleService::GPhotoImport: case GoogleService::GPhotoExport: d->gphotoTalker->listAlbums(); break; } } void GSWindow::slotAccessTokenObtained() { switch (d->service) { case GoogleService::GDrive: d->talker->listFolders(); break; case GoogleService::GPhotoImport: case GoogleService::GPhotoExport: d->gphotoTalker->getLoggedInUser(); break; } } void GSWindow::slotAuthenticationRefused() { // QMessageBox::critical(this, i18nc("@title:window", "Error"), // i18n("An authentication error occurred: account failed to link")); // Clear list albums d->widget->getAlbumsCoB()->clear(); // Clear user name d->widget->updateLabels(QString()); return; } void GSWindow::slotCreateFolderDone(int code, const QString& msg, const QString& albumId) { switch (d->service) { case GoogleService::GDrive: if (code == 0) QMessageBox::critical(this, i18nc("@title:window", "Error"), i18n("Google Drive call failed:\n%1", msg)); else { d->currentAlbumId = albumId; d->talker->listFolders(); } break; case GoogleService::GPhotoImport: case GoogleService::GPhotoExport: if (code == 0) QMessageBox::critical(this, i18nc("@title:window", "Error"), i18n("Google Photos call failed:\n%1", msg)); else { d->currentAlbumId = albumId; d->gphotoTalker->listAlbums(); } break; } } void GSWindow::slotTransferCancel() { d->transferQueue.clear(); d->widget->progressBar()->hide(); switch (d->service) { case GoogleService::GDrive: d->talker->cancel(); break; case GoogleService::GPhotoImport: case GoogleService::GPhotoExport: d->gphotoTalker->cancel(); break; } } void GSWindow::slotUserChangeRequest() { QPointer warn = new QMessageBox(QMessageBox::Warning, i18n("Warning"), i18n("You will be logged out of your account, " "click \"Continue\" to authenticate for another account"), QMessageBox::Yes | QMessageBox::No); (warn->button(QMessageBox::Yes))->setText(i18n("Continue")); (warn->button(QMessageBox::No))->setText(i18n("Cancel")); if (warn->exec() == QMessageBox::Yes) { /** * We do not force user to logout from their account * We simply unlink user account and direct use to login page to login new account * (In the future, we may not unlink() user, but let them change account and * choose which one they want to use) * After unlink(), waiting actively until O2 completely unlink() account, before doOAuth() again */ switch (d->service) { case GoogleService::GDrive: d->talker->unlink(); while(d->talker->authenticated()); d->talker->doOAuth(); break; case GoogleService::GPhotoImport: case GoogleService::GPhotoExport: d->gphotoTalker->unlink(); while(d->gphotoTalker->authenticated()); d->gphotoTalker->doOAuth(); break; } } delete warn; } void GSWindow::buttonStateChange(bool state) { d->widget->getNewAlbmBtn()->setEnabled(state); d->widget->getReloadBtn()->setEnabled(state); startButton()->setEnabled(state); } void GSWindow::slotFinished() { writeSettings(); d->widget->imagesList()->listView()->clear(); } void GSWindow::closeEvent(QCloseEvent* e) { if (!e) { return; } slotFinished(); e->accept(); } } // namespace DigikamGenericGoogleServicesPlugin diff --git a/core/utilities/import/views/importcategorizedview.cpp b/core/utilities/import/views/importcategorizedview.cpp index 6f33209caa..9ad3b80f6d 100644 --- a/core/utilities/import/views/importcategorizedview.cpp +++ b/core/utilities/import/views/importcategorizedview.cpp @@ -1,631 +1,631 @@ /* ============================================================ * * This file is a part of digiKam project * https://www.digikam.org * * Date : 2012-07-13 * Description : Qt categorized item view for camera items * * Copyright (C) 2012 by Islam Wazery * * This program is free software; you can redistribute it * and/or modify it under the terms of the GNU General * Public License as published by the Free Software Foundation; * either version 2, or (at your option) * any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * ============================================================ */ #include "importcategorizedview.h" // Qt includes #include // Local includes #include "digikam_debug.h" #include "camitemsortsettings.h" #include "iccsettings.h" #include "itemselectionoverlay.h" #include "importdelegate.h" #include "importtooltipfiller.h" #include "importsettings.h" #include "itemviewtooltip.h" #include "loadingcacheinterface.h" #include "thumbnailloadthread.h" namespace Digikam { class Q_DECL_HIDDEN ImportItemViewToolTip : public ItemViewToolTip { public: explicit ImportItemViewToolTip(ImportCategorizedView* const view) : ItemViewToolTip(view) { } ImportCategorizedView* view() const { return static_cast(ItemViewToolTip::view()); } protected: virtual QString tipContents() { CamItemInfo info = ImportItemModel::retrieveCamItemInfo(currentIndex()); return ImportToolTipFiller::CamItemInfoTipContents(info); } }; class Q_DECL_HIDDEN ImportCategorizedView::Private { public: explicit Private() : model(nullptr), filterModel(nullptr), delegate(nullptr), showToolTip(false), scrollToItemId(0), delayedEnterTimer(nullptr), currentMouseEvent(nullptr) { } - ImportItemModel* model; + ImportItemModel* model; ImportSortFilterModel* filterModel; ImportDelegate* delegate; bool showToolTip; qlonglong scrollToItemId; QTimer* delayedEnterTimer; QMouseEvent* currentMouseEvent; }; ImportCategorizedView::ImportCategorizedView(QWidget* const parent) : ItemViewCategorized(parent), d(new Private) { setToolTip(new ImportItemViewToolTip(this)); LoadingCacheInterface::connectToSignalFileChanged(this, SLOT(slotFileChanged(QString))); d->delayedEnterTimer = new QTimer(this); d->delayedEnterTimer->setInterval(10); d->delayedEnterTimer->setSingleShot(true); connect(d->delayedEnterTimer, SIGNAL(timeout()), this, SLOT(slotDelayedEnter())); connect(IccSettings::instance(), SIGNAL(settingsChanged(ICCSettingsContainer,ICCSettingsContainer)), this, SLOT(slotIccSettingsChanged(ICCSettingsContainer,ICCSettingsContainer))); } ImportCategorizedView::~ImportCategorizedView() { d->delegate->removeAllOverlays(); delete d; } void ImportCategorizedView::setModels(ImportItemModel* model, ImportSortFilterModel* filterModel) { if (d->delegate) { d->delegate->setAllOverlaysActive(false); } if (d->filterModel) { disconnect(d->filterModel, SIGNAL(layoutAboutToBeChanged()), this, SLOT(layoutAboutToBeChanged())); disconnect(d->filterModel, SIGNAL(layoutChanged()), this, SLOT(layoutWasChanged())); } if (d->model) { disconnect(d->model, SIGNAL(itemInfosAdded(QList)), this, SLOT(slotCamItemInfosAdded())); } d->model = model; d->filterModel = filterModel; setModel(d->filterModel); connect(d->filterModel, SIGNAL(layoutAboutToBeChanged()), this, SLOT(layoutAboutToBeChanged())); connect(d->filterModel, SIGNAL(layoutChanged()), this, SLOT(layoutWasChanged()), Qt::QueuedConnection); connect(d->model, SIGNAL(itemInfosAdded(QList)), this, SLOT(slotCamItemInfosAdded())); emit modelChanged(); if (d->delegate) { d->delegate->setAllOverlaysActive(true); } } ImportItemModel* ImportCategorizedView::importItemModel() const { return d->model; } ImportSortFilterModel* ImportCategorizedView::importSortFilterModel() const { return d->filterModel; } ImportFilterModel* ImportCategorizedView::importFilterModel() const { return d->filterModel->importFilterModel(); } ImportThumbnailModel* ImportCategorizedView::importThumbnailModel() const { return qobject_cast(d->model); } QSortFilterProxyModel* ImportCategorizedView::filterModel() const { return d->filterModel; } ImportDelegate* ImportCategorizedView::delegate() const { return d->delegate; } void ImportCategorizedView::setItemDelegate(ImportDelegate* delegate) { ThumbnailSize oldSize = thumbnailSize(); ImportDelegate* oldDelegate = d->delegate; if (oldDelegate) { hideIndexNotification(); d->delegate->setAllOverlaysActive(false); d->delegate->setViewOnAllOverlays(nullptr); // Note: Be precise, no wildcard disconnect! disconnect(d->delegate, SIGNAL(requestNotification(QModelIndex,QString)), this, SLOT(showIndexNotification(QModelIndex,QString))); disconnect(d->delegate, SIGNAL(hideNotification()), this, SLOT(hideIndexNotification())); } d->delegate = delegate; delegate->setThumbnailSize(oldSize); if (oldDelegate) { d->delegate->setSpacing(oldDelegate->spacing()); } ItemViewCategorized::setItemDelegate(d->delegate); setCategoryDrawer(d->delegate->categoryDrawer()); updateDelegateSizes(); d->delegate->setViewOnAllOverlays(this); d->delegate->setAllOverlaysActive(true); connect(d->delegate, SIGNAL(requestNotification(QModelIndex,QString)), this, SLOT(showIndexNotification(QModelIndex,QString))); connect(d->delegate, SIGNAL(hideNotification()), this, SLOT(hideIndexNotification())); } CamItemInfo ImportCategorizedView::currentInfo() const { return d->filterModel->camItemInfo(currentIndex()); } QUrl ImportCategorizedView::currentUrl() const { return currentInfo().url(); } QList ImportCategorizedView::selectedCamItemInfos() const { return d->filterModel->camItemInfos(selectedIndexes()); } QList ImportCategorizedView::selectedCamItemInfosCurrentFirst() const { QList indexes = selectedIndexes(); QModelIndex current = currentIndex(); - QList infos; + QList infos; - foreach(const QModelIndex& index, indexes) + foreach (const QModelIndex& index, indexes) { CamItemInfo info = d->filterModel->camItemInfo(index); if (index == current) { infos.prepend(info); } else { infos.append(info); } } return infos; } QList ImportCategorizedView::camItemInfos() const { return d->filterModel->camItemInfosSorted(); } QList ImportCategorizedView::urls() const { QList infos = camItemInfos(); - QList urls; + QList urls; - foreach(const CamItemInfo& info, infos) + foreach (const CamItemInfo& info, infos) { urls << info.url(); } return urls; } QList ImportCategorizedView::selectedUrls() const { QList infos = selectedCamItemInfos(); - QList urls; + QList urls; - foreach(const CamItemInfo& info, infos) + foreach (const CamItemInfo& info, infos) { urls << info.url(); } return urls; } void ImportCategorizedView::toIndex(const QUrl& url) { ItemViewCategorized::toIndex(d->filterModel->indexForPath(url.toLocalFile())); } CamItemInfo ImportCategorizedView::nextInOrder(const CamItemInfo& startingPoint, int nth) { QModelIndex index = d->filterModel->indexForCamItemInfo(startingPoint); if (!index.isValid()) { return CamItemInfo(); } return d->filterModel->camItemInfo(d->filterModel->index(index.row() + nth, 0, QModelIndex())); } QModelIndex ImportCategorizedView::nextIndexHint(const QModelIndex& anchor, const QItemSelectionRange& removed) const { QModelIndex hint = ItemViewCategorized::nextIndexHint(anchor, removed); - CamItemInfo info = d->filterModel->camItemInfo(anchor); + CamItemInfo info = d->filterModel->camItemInfo(anchor); //qCDebug(DIGIKAM_IMPORTUI_LOG) << "Having initial hint" << hint << "for" << anchor << d->model->numberOfIndexesForCamItemInfo(info); // Fixes a special case of multiple (face) entries for the same image. // If one is removed, any entry of the same image shall be preferred. if (d->model->numberOfIndexesForCamItemInfo(info) > 1) { // The hint is for a different info, but we may have a hint for the same info if (info != d->filterModel->camItemInfo(hint)) { - int minDiff = d->filterModel->rowCount(); + int minDiff = d->filterModel->rowCount(); QList indexesForCamItemInfo = d->filterModel->mapListFromSource(d->model->indexesForCamItemInfo(info)); - foreach(const QModelIndex& index, indexesForCamItemInfo) + foreach (const QModelIndex& index, indexesForCamItemInfo) { if (index == anchor || !index.isValid() || removed.contains(index)) { continue; } int distance = qAbs(index.row() - anchor.row()); if (distance < minDiff) { minDiff = distance; hint = index; //qCDebug(DIGIKAM_IMPORTUI_LOG) << "Chose index" << hint << "at distance" << minDiff << "to" << anchor; } } } } return hint; } ThumbnailSize ImportCategorizedView::thumbnailSize() const { /* ImportThumbnailModel *thumbModel = importThumbnailModel(); if (thumbModel) return thumbModel->thumbnailSize(); */ if (d->delegate) { return d->delegate->thumbnailSize(); } return ThumbnailSize(); } void ImportCategorizedView::setThumbnailSize(int size) { setThumbnailSize(ThumbnailSize(size)); } void ImportCategorizedView::setThumbnailSize(const ThumbnailSize& s) { // we abuse this pair of method calls to restore scroll position // TODO check if needed layoutAboutToBeChanged(); d->delegate->setThumbnailSize(s); layoutWasChanged(); } void ImportCategorizedView::setCurrentWhenAvailable(qlonglong camItemId) { d->scrollToItemId = camItemId; } void ImportCategorizedView::setCurrentUrl(const QUrl& url) { if (url.isEmpty()) { clearSelection(); setCurrentIndex(QModelIndex()); return; } QString path = url.toLocalFile(); QModelIndex index = d->filterModel->indexForPath(path); if (!index.isValid()) { return; } clearSelection(); setCurrentIndex(index); } void ImportCategorizedView::setCurrentInfo(const CamItemInfo& info) { QModelIndex index = d->filterModel->indexForCamItemInfo(info); clearSelection(); setCurrentIndex(index); } void ImportCategorizedView::setSelectedUrls(const QList& urlList) { QItemSelection mySelection; - for (QList::const_iterator it = urlList.constBegin(); it!=urlList.constEnd(); ++it) + for (QList::const_iterator it = urlList.constBegin() ; it!=urlList.constEnd() ; ++it) { - const QString path = it->toLocalFile(); + const QString path = it->toLocalFile(); const QModelIndex index = d->filterModel->indexForPath(path); if (!index.isValid()) { qCWarning(DIGIKAM_IMPORTUI_LOG) << "no QModelIndex found for" << *it; } else { // TODO: is there a better way? mySelection.select(index, index); } } clearSelection(); selectionModel()->select(mySelection, QItemSelectionModel::Select); } void ImportCategorizedView::setSelectedCamItemInfos(const QList& infos) { QItemSelection mySelection; - foreach(const CamItemInfo& info, infos) + foreach (const CamItemInfo& info, infos) { QModelIndex index = d->filterModel->indexForCamItemInfo(info); mySelection.select(index, index); } selectionModel()->select(mySelection, QItemSelectionModel::ClearAndSelect); } void ImportCategorizedView::hintAt(const CamItemInfo& info) { if (info.isNull()) { return; } QModelIndex index = d->filterModel->indexForCamItemInfo(info); if (!index.isValid()) { return; } selectionModel()->setCurrentIndex(index, QItemSelectionModel::NoUpdate); scrollTo(index); } void ImportCategorizedView::addOverlay(ItemDelegateOverlay* overlay, ImportDelegate* delegate) { if (!delegate) { delegate = d->delegate; } delegate->installOverlay(overlay); if (delegate == d->delegate) { overlay->setView(this); overlay->setActive(true); } } void ImportCategorizedView::removeOverlay(ItemDelegateOverlay* overlay) { ImportDelegate* delegate = dynamic_cast(overlay->delegate()); if (delegate) { delegate->removeOverlay(overlay); } overlay->setView(nullptr); } void ImportCategorizedView::updateGeometries() { ItemViewCategorized::updateGeometries(); d->delayedEnterTimer->start(); } void ImportCategorizedView::slotDelayedEnter() { // re-emit entered() for index under mouse (after layout). QModelIndex mouseIndex = indexAt(mapFromGlobal(QCursor::pos())); if (mouseIndex.isValid()) { emit DCategorizedView::entered(mouseIndex); } } void ImportCategorizedView::addSelectionOverlay(ImportDelegate* delegate) { addOverlay(new ItemSelectionOverlay(this), delegate); } void ImportCategorizedView::scrollToStoredItem() { if (d->scrollToItemId) { if (d->model->hasImage(d->scrollToItemId)) { QModelIndex index = d->filterModel->indexForCamItemId(d->scrollToItemId); setCurrentIndex(index); scrollToRelaxed(index, QAbstractItemView::PositionAtCenter); d->scrollToItemId = 0; } } } void ImportCategorizedView::slotCamItemInfosAdded() { if (d->scrollToItemId) { scrollToStoredItem(); } } void ImportCategorizedView::slotFileChanged(const QString& filePath) { QModelIndex index = d->filterModel->indexForPath(filePath); if (index.isValid()) { update(index); } } void ImportCategorizedView::indexActivated(const QModelIndex& index, Qt::KeyboardModifiers modifiers) { CamItemInfo info = d->filterModel->camItemInfo(index); if (!info.isNull()) { activated(info, modifiers); emit camItemInfoActivated(info); } } void ImportCategorizedView::currentChanged(const QModelIndex& index, const QModelIndex& previous) { ItemViewCategorized::currentChanged(index, previous); emit currentChanged(d->filterModel->camItemInfo(index)); } void ImportCategorizedView::selectionChanged(const QItemSelection& selectedItems, const QItemSelection& deselectedItems) { ItemViewCategorized::selectionChanged(selectedItems, deselectedItems); if (!selectedItems.isEmpty()) { emit selected(d->filterModel->camItemInfos(selectedItems.indexes())); } if (!deselectedItems.isEmpty()) { emit deselected(d->filterModel->camItemInfos(deselectedItems.indexes())); } } void ImportCategorizedView::activated(const CamItemInfo&, Qt::KeyboardModifiers) { // implemented in subclass } void ImportCategorizedView::showContextMenuOnIndex(QContextMenuEvent* event, const QModelIndex& index) { CamItemInfo info = d->filterModel->camItemInfo(index); showContextMenuOnInfo(event, info); } void ImportCategorizedView::showContextMenuOnInfo(QContextMenuEvent*, const CamItemInfo&) { // implemented in subclass } void ImportCategorizedView::paintEvent(QPaintEvent* e) { ItemViewCategorized::paintEvent(e); } QItemSelectionModel* ImportCategorizedView::getSelectionModel() const { return selectionModel(); } AbstractItemDragDropHandler* ImportCategorizedView::dragDropHandler() const { return d->model->dragDropHandler(); } void ImportCategorizedView::slotIccSettingsChanged(const ICCSettingsContainer&, const ICCSettingsContainer&) { viewport()->update(); } } // namespace Digikam diff --git a/core/utilities/import/views/importiconview.cpp b/core/utilities/import/views/importiconview.cpp index 6fb1addf1a..a89ad20b73 100644 --- a/core/utilities/import/views/importiconview.cpp +++ b/core/utilities/import/views/importiconview.cpp @@ -1,484 +1,484 @@ /* ============================================================ * * This file is a part of digiKam project * https://www.digikam.org * * Date : 2012-22-07 * Description : Icon view for import tool items * * Copyright (C) 2012 by Islam Wazery * Copyright (C) 2012-2019 by Gilles Caulier * * This program is free software you can redistribute it * and/or modify it under the terms of the GNU General * Public License as published by the Free Software Foundation * either version 2, or (at your option) * any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * ============================================================ */ #include "importiconview.h" #include "importiconview_p.h" // Qt includes #include #include #include // Local includes #include "importcategorizedview.h" #include "importoverlays.h" #include "importsettings.h" #include "camitemsortsettings.h" #include "fileactionmngr.h" #include "importdelegate.h" #include "advancedrenamedialog.h" #include "advancedrenameprocessdialog.h" #include "itemviewutilities.h" #include "importcontextmenu.h" #include "importdragdrop.h" namespace Digikam { ImportIconView::ImportIconView(QWidget* const parent) : ImportCategorizedView(parent), d(new Private(this)) { ImportThumbnailModel* const model = new ImportThumbnailModel(this); ImportFilterModel* const filterModel = new ImportFilterModel(this); filterModel->setSourceImportModel(model); filterModel->sort(0); // an initial sorting is necessary setModels(model, filterModel); d->normalDelegate = new ImportNormalDelegate(this); setItemDelegate(d->normalDelegate); setSpacing(10); ImportSettings* const settings = ImportSettings::instance(); setThumbnailSize(ThumbnailSize(settings->getDefaultIconSize())); importItemModel()->setDragDropHandler(new ImportDragDropHandler(importItemModel())); setDragEnabled(true); setAcceptDrops(true); setDropIndicatorShown(false); setToolTipEnabled(settings->showToolTipsIsValid()); // selection overlay addSelectionOverlay(d->normalDelegate); //TODO: addSelectionOverlay(d->faceDelegate); // rotation overlays d->rotateLeftOverlay = ImportRotateOverlay::left(this); d->rotateRightOverlay = ImportRotateOverlay::right(this); addOverlay(new ImportDownloadOverlay(this)); addOverlay(new ImportLockOverlay(this)); addOverlay(new ImportCoordinatesOverlay(this)); d->updateOverlays(); // rating overlay ImportRatingOverlay* const ratingOverlay = new ImportRatingOverlay(this); addOverlay(ratingOverlay); //TODO: GroupIndicatorOverlay* groupOverlay = new GroupIndicatorOverlay(this); //TODO: addOverlay(groupOverlay); connect(ratingOverlay, SIGNAL(ratingEdited(QList,int)), this, SLOT(assignRating(QList,int))); //TODO: connect(groupOverlay, SIGNAL(toggleGroupOpen(QModelIndex)), //this, SLOT(groupIndicatorClicked(QModelIndex))); //TODO: connect(groupOverlay, SIGNAL(showButtonContextMenu(QModelIndex,QContextMenuEvent*)), //this, SLOT(showGroupContextMenu(QModelIndex,QContextMenuEvent*))); //TODO: connect(importItemModel()->dragDropHandler(), SIGNAL(assignTags(QList,QList)), //FileActionMngr::instance(), SLOT(assignTags(QList,QList))); //TODO: connect(importItemModel()->dragDropHandler(), SIGNAL(addToGroup(CamItemInfo,QList)), //FileActionMngr::instance(), SLOT(addToGroup(CamItemInfo,QList))); connect(settings, SIGNAL(setupChanged()), this, SLOT(slotSetupChanged())); slotSetupChanged(); } ImportIconView::~ImportIconView() { delete d; } ItemViewUtilities* ImportIconView::utilities() const { return d->utilities; } void ImportIconView::setThumbnailSize(const ThumbnailSize& size) { ImportCategorizedView::setThumbnailSize(size); } int ImportIconView::fitToWidthIcons() { return delegate()->calculatethumbSizeToFit(viewport()->size().width()); } CamItemInfo ImportIconView::camItemInfo(const QString& folder, const QString& file) { QUrl url = QUrl::fromLocalFile(folder); url = url.adjusted(QUrl::StripTrailingSlash); url.setPath(url.path() + QLatin1Char('/') + file); QModelIndex indexForCamItemInfo = importFilterModel()->indexForPath(url.toLocalFile()); if (indexForCamItemInfo.isValid()) { return importFilterModel()->camItemInfo(indexForCamItemInfo); } return CamItemInfo(); } CamItemInfo& ImportIconView::camItemInfoRef(const QString& folder, const QString& file) { QUrl url = QUrl::fromLocalFile(folder); url = url.adjusted(QUrl::StripTrailingSlash); url.setPath(url.path() + QLatin1Char('/') + file); QModelIndex indexForCamItemInfo = importFilterModel()->indexForPath(url.toLocalFile()); QModelIndex mappedIndex = importFilterModel()->mapToSource(indexForCamItemInfo); return importItemModel()->camItemInfoRef(mappedIndex); } void ImportIconView::slotSetupChanged() { setToolTipEnabled(ImportSettings::instance()->showToolTipsIsValid()); setFont(ImportSettings::instance()->getIconViewFont()); d->updateOverlays(); ImportCategorizedView::slotSetupChanged(); } void ImportIconView::rename() { QList urls = selectedUrls(); NewNamesList newNamesList; QPointer dlg = new AdvancedRenameDialog(this); dlg->slotAddImages(urls); if (dlg->exec() == QDialog::Accepted) { newNamesList = dlg->newNames(); } delete dlg; if (!newNamesList.isEmpty()) { QPointer dlg = new AdvancedRenameProcessDialog(newNamesList); dlg->exec(); delete dlg; } } void ImportIconView::deleteSelected(bool /*permanently*/) { CamItemInfoList camItemInfoList = selectedCamItemInfos(); //FIXME: This way of deletion may not working with camera items. /* if (d->utilities->deleteImages(camItemInfoList, permanently)) { awayFromSelection(); } */ } void ImportIconView::deleteSelectedDirectly(bool /*permanently*/) { CamItemInfoList camItemInfoList = selectedCamItemInfos(); //FIXME: This way of deletion may not working with camera items. //d->utilities->deleteImagesDirectly(camItemInfoList, permanently); awayFromSelection(); } void ImportIconView::createGroupFromSelection() { //TODO: Implement grouping in import tool. /* QList selectedInfos = selectedCamItemInfosCurrentFirst(); CamItemInfo groupLeader = selectedInfos.takeFirst(); FileActionMngr::instance()->addToGroup(groupLeader, selectedInfos); */ } void ImportIconView::createGroupByTimeFromSelection() { //TODO: Implement grouping in import tool. /* QList selectedInfos = selectedCamItemInfosCurrentFirst(); while (selectedInfos.size() > 0) { QList group; CamItemInfo groupLeader = selectedInfos.takeFirst(); QDateTime dateTime = groupLeader.dateTime(); while (selectedInfos.size() > 0 && abs(dateTime.secsTo(selectedInfos.first().dateTime())) < 2) { group.push_back(selectedInfos.takeFirst()); } FileActionMngr::instance()->addToGroup(groupLeader, group); } */ } void ImportIconView::ungroupSelected() { //TODO: Implement grouping in import tool. //FileActionMngr::instance()->ungroup(selectedCamItemInfos()); } void ImportIconView::removeSelectedFromGroup() { //TODO: Implement grouping in import tool. //FileActionMngr::instance()->removeFromGroup(selectedCamItemInfos()); } void ImportIconView::slotRotateLeft(const QList& /*indexes*/) { /* QList imageInfos; foreach(const QModelIndex& index, indexes) { ItemInfo imageInfo(importFilterModel()->camItemInfo(index).url()); imageInfos << imageInfo; } FileActionMngr::instance()->transform(imageInfos, MetaEngineRotation::Rotate270); */ } void ImportIconView::slotRotateRight(const QList& /*indexes*/) { /* QList imageInfos; foreach(const QModelIndex& index, indexes) { ItemInfo imageInfo(importFilterModel()->camItemInfo(index).url()); imageInfos << imageInfo; } FileActionMngr::instance()->transform(imageInfos, MetaEngineRotation::Rotate90); */ } void ImportIconView::activated(const CamItemInfo& info, Qt::KeyboardModifiers) { if (info.isNull()) { return; } if (ImportSettings::instance()->getItemLeftClickAction() == ImportSettings::ShowPreview) { emit previewRequested(info, false); } else { //TODO: openFile(info); } } void ImportIconView::showContextMenuOnInfo(QContextMenuEvent* event, const CamItemInfo& /*info*/) { QList selectedInfos = selectedCamItemInfosCurrentFirst(); QList selectedItemIDs; - foreach(const CamItemInfo& info, selectedInfos) + foreach (const CamItemInfo& info, selectedInfos) { selectedItemIDs << info.id; } // -------------------------------------------------------- QMenu popmenu(this); ImportContextMenuHelper cmhelper(&popmenu); cmhelper.addAction(QLatin1String("importui_fullscreen")); cmhelper.addAction(QLatin1String("options_show_menubar")); cmhelper.addAction(QLatin1String("import_zoomfit2window")); cmhelper.addSeparator(); // -------------------------------------------------------- cmhelper.addAction(QLatin1String("importui_imagedownload")); cmhelper.addAction(QLatin1String("importui_imagemarkasdownloaded")); cmhelper.addAction(QLatin1String("importui_imagelock")); cmhelper.addAction(QLatin1String("importui_delete")); cmhelper.addSeparator(); cmhelper.addAction(QLatin1String("importui_item_view")); cmhelper.addServicesMenu(selectedUrls()); //TODO: cmhelper.addRotateMenu(selectedItemIDs); cmhelper.addSeparator(); // -------------------------------------------------------- cmhelper.addAction(QLatin1String("importui_selectall")); cmhelper.addAction(QLatin1String("importui_selectnone")); cmhelper.addAction(QLatin1String("importui_selectinvert")); cmhelper.addSeparator(); // -------------------------------------------------------- //cmhelper.addAssignTagsMenu(selectedItemIDs); //cmhelper.addRemoveTagsMenu(selectedItemIDs); //cmhelper.addSeparator(); // -------------------------------------------------------- cmhelper.addLabelsAction(); //if (!d->faceMode) //{ // cmhelper.addGroupMenu(selectedItemIDs); //} // special action handling -------------------------------- //connect(&cmhelper, SIGNAL(signalAssignTag(int)), // this, SLOT(assignTagToSelected(int))); //TODO: Implement tag view for import tool. //connect(&cmhelper, SIGNAL(signalPopupTagsView()), // this, SIGNAL(signalPopupTagsView())); //connect(&cmhelper, SIGNAL(signalRemoveTag(int)), // this, SLOT(removeTagFromSelected(int))); //connect(&cmhelper, SIGNAL(signalGotoTag(int)), //this, SIGNAL(gotoTagAndImageRequested(int))); connect(&cmhelper, SIGNAL(signalAssignPickLabel(int)), this, SLOT(assignPickLabelToSelected(int))); connect(&cmhelper, SIGNAL(signalAssignColorLabel(int)), this, SLOT(assignColorLabelToSelected(int))); connect(&cmhelper, SIGNAL(signalAssignRating(int)), this, SLOT(assignRatingToSelected(int))); //connect(&cmhelper, SIGNAL(signalAddToExistingQueue(int)), //this, SLOT(insertSelectedToExistingQueue(int))); //FIXME: connect(&cmhelper, SIGNAL(signalCreateGroup()), //this, SLOT(createGroupFromSelection())); //connect(&cmhelper, SIGNAL(signalUngroup()), //this, SLOT(ungroupSelected())); //connect(&cmhelper, SIGNAL(signalRemoveFromGroup()), //this, SLOT(removeSelectedFromGroup())); // -------------------------------------------------------- cmhelper.exec(event->globalPos()); } void ImportIconView::showContextMenu(QContextMenuEvent* event) { QMenu popmenu(this); ImportContextMenuHelper cmhelper(&popmenu); cmhelper.addAction(QLatin1String("importui_fullscreen")); cmhelper.addAction(QLatin1String("options_show_menubar")); cmhelper.addSeparator(); cmhelper.addAction(QLatin1String("importui_close")); // -------------------------------------------------------- cmhelper.exec(event->globalPos()); } void ImportIconView::assignTagToSelected(int tagID) { CamItemInfoList infos = selectedCamItemInfos(); - foreach(const CamItemInfo& info, infos) + foreach (const CamItemInfo& info, infos) { importItemModel()->camItemInfoRef(importItemModel()->indexForCamItemInfo(info)).tagIds.append(tagID); } } void ImportIconView::removeTagFromSelected(int tagID) { CamItemInfoList infos = selectedCamItemInfos(); - foreach(const CamItemInfo& info, infos) + foreach (const CamItemInfo& info, infos) { importItemModel()->camItemInfoRef(importItemModel()->indexForCamItemInfo(info)).tagIds.removeAll(tagID); } } void ImportIconView::assignPickLabel(const QModelIndex& index, int pickId) { importItemModel()->camItemInfoRef(index).pickLabel = pickId; } void ImportIconView::assignPickLabelToSelected(int pickId) { CamItemInfoList infos = selectedCamItemInfos(); - foreach(const CamItemInfo& info, infos) + foreach (const CamItemInfo& info, infos) { importItemModel()->camItemInfoRef(importItemModel()->indexForCamItemInfo(info)).pickLabel = pickId; } } void ImportIconView::assignColorLabel(const QModelIndex& index, int colorId) { importItemModel()->camItemInfoRef(index).colorLabel = colorId; } void ImportIconView::assignColorLabelToSelected(int colorId) { CamItemInfoList infos = selectedCamItemInfos(); - foreach(const CamItemInfo& info, infos) + foreach (const CamItemInfo& info, infos) { importItemModel()->camItemInfoRef(importItemModel()->indexForCamItemInfo(info)).colorLabel = colorId; } } void ImportIconView::assignRating(const QList& indexes, int rating) { - foreach(const QModelIndex& index, indexes) + foreach (const QModelIndex& index, indexes) { if (index.isValid()) { importItemModel()->camItemInfoRef(index).rating = rating; } } } void ImportIconView::assignRatingToSelected(int rating) { CamItemInfoList infos = selectedCamItemInfos(); - foreach(const CamItemInfo& info, infos) + foreach (const CamItemInfo& info, infos) { importItemModel()->camItemInfoRef(importItemModel()->indexForCamItemInfo(info)).rating = rating; } } } // namespace Digikam diff --git a/core/utilities/import/views/importstackedview.cpp b/core/utilities/import/views/importstackedview.cpp index 357562283c..c25b13ce02 100644 --- a/core/utilities/import/views/importstackedview.cpp +++ b/core/utilities/import/views/importstackedview.cpp @@ -1,518 +1,518 @@ /* ============================================================ * * This file is a part of digiKam project * https://www.digikam.org * * Date : 2012-05-07 * Description : QStackedWidget to handle different types of views * (icon view, image preview, media view) * * Copyright (C) 2012 by Islam Wazery * Copyright (C) 2012-2019 by Gilles Caulier * * This program is free software; you can redistribute it * and/or modify it under the terms of the GNU General * Public License as published by the Free Software Foundation; * either version 2, or (at your option) * any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * ============================================================ */ #include "importstackedview.h" // Qt includes #include // Local includes #include "digikam_debug.h" #include "previewlayout.h" #include "importsettings.h" namespace Digikam { class MediaPlayerView; class Q_DECL_HIDDEN ImportStackedView::Private { public: explicit Private() { dockArea = nullptr; splitter = nullptr; thumbBar = nullptr; thumbBarDock = nullptr; importIconView = nullptr; importPreviewView = nullptr; #ifdef HAVE_MARBLE mapWidgetView = nullptr; #endif // HAVE_MARBLE #ifdef HAVE_MEDIAPLAYER mediaPlayerView = nullptr; #endif // HAVE_MEDIAPLAYER syncingSelection = false; } QMainWindow* dockArea; QSplitter* splitter; ImportThumbnailBar* thumbBar; ThumbBarDock* thumbBarDock; ImportIconView* importIconView; ImportPreviewView* importPreviewView; #ifdef HAVE_MARBLE MapWidgetView* mapWidgetView; #endif // HAVE_MARBLE #ifdef HAVE_MEDIAPLAYER MediaPlayerView* mediaPlayerView; // Reuse of albumgui mediaplayer view. #endif // HAVE_MEDIAPLAYER bool syncingSelection; }; ImportStackedView::ImportStackedView(QWidget* const parent) : QStackedWidget(parent), d(new Private) { d->importIconView = new ImportIconView(this); d->importPreviewView = new ImportPreviewView(this); d->thumbBarDock = new ThumbBarDock(); d->thumbBar = new ImportThumbnailBar(d->thumbBarDock); d->thumbBar->setModelsFiltered(d->importIconView->importItemModel(), d->importIconView->importFilterModel()); d->thumbBar->installOverlays(); d->thumbBarDock->setWidget(d->thumbBar); d->thumbBarDock->setObjectName(QLatin1String("import_thumbbar")); d->thumbBarDock->setWindowTitle(i18n("Import Thumbnail Dock")); #ifdef HAVE_MARBLE // TODO refactor MapWidgetView not to require the models on startup? d->mapWidgetView = new MapWidgetView(d->importIconView->getSelectionModel(), d->importIconView->importFilterModel(), this, MapWidgetView::ApplicationImportUI); d->mapWidgetView->setObjectName(QLatin1String("import_mapwidgetview")); #endif // HAVE_MARBLE #ifdef HAVE_MEDIAPLAYER d->mediaPlayerView = new MediaPlayerView(this); #endif //HAVE_MEDIAPLAYER insertWidget(PreviewCameraMode, d->importIconView); insertWidget(PreviewImageMode, d->importPreviewView); #ifdef HAVE_MARBLE insertWidget(MapWidgetMode, d->mapWidgetView); #endif // HAVE_MARBLE #ifdef HAVE_MEDIAPLAYER insertWidget(MediaPlayerMode, d->mediaPlayerView); #endif //HAVE_MEDIAPLAYER setAttribute(Qt::WA_DeleteOnClose); readSettings(); // ----------------------------------------------------------------- //FIXME: connect(d->importPreviewView, SIGNAL(signalPopupTagsView()), //d->importIconView, SIGNAL(signalPopupTagsView())); //connect(d->importPreviewView, SIGNAL(signalGotoFolderAndItem(CamItemInfo)), //this, SIGNAL(signalGotoFolderAndItem(CamItemInfo))); //connect(d->importPreviewView, SIGNAL(signalGotoDateAndItem(CamItemInfo)), //this, SIGNAL(signalGotoDateAndItem(CamItemInfo))); //FIXME: connect(d->importPreviewView, SIGNAL(signalGotoTagAndItem(int)), //this, SIGNAL(signalGotoTagAndItem(int))); connect(d->importPreviewView, SIGNAL(signalNextItem()), this, SIGNAL(signalNextItem())); connect(d->importPreviewView, SIGNAL(signalPrevItem()), this, SIGNAL(signalPrevItem())); //FIXME: connect(d->importPreviewView, SIGNAL(signalDeleteItem()), //this, SIGNAL(signalDeleteItem())); connect(d->importPreviewView, SIGNAL(signalEscapePreview()), this, SIGNAL(signalEscapePreview())); // A workaround to assign pickLabel, colorLabel, and rating in the preview view. connect(d->importPreviewView, SIGNAL(signalAssignPickLabel(int)), d->importIconView, SLOT(assignPickLabelToSelected(int))); connect(d->importPreviewView, SIGNAL(signalAssignColorLabel(int)), d->importIconView, SLOT(assignColorLabelToSelected(int))); connect(d->importPreviewView, SIGNAL(signalAssignRating(int)), d->importIconView, SLOT(assignRatingToSelected(int))); connect(d->importPreviewView->layout(), SIGNAL(zoomFactorChanged(double)), this, SLOT(slotZoomFactorChanged(double))); //FIXME: connect(d->importPreviewView, SIGNAL(signalAddToExistingQueue(int)), //this, SIGNAL(signalAddToExistingQueue(int))); connect(d->thumbBar, SIGNAL(selectionChanged()), this, SLOT(slotThumbBarSelectionChanged())); connect(d->importIconView, SIGNAL(selectionChanged()), this, SLOT(slotIconViewSelectionChanged())); connect(d->thumbBarDock, SIGNAL(dockLocationChanged(Qt::DockWidgetArea)), d->thumbBar, SLOT(slotDockLocationChanged(Qt::DockWidgetArea))); connect(d->importPreviewView, SIGNAL(signalPreviewLoaded(bool)), this, SLOT(slotPreviewLoaded(bool))); #ifdef HAVE_MEDIAPLAYER connect(d->mediaPlayerView, SIGNAL(signalNextItem()), this, SIGNAL(signalNextItem())); connect(d->mediaPlayerView, SIGNAL(signalPrevItem()), this, SIGNAL(signalPrevItem())); connect(d->mediaPlayerView, SIGNAL(signalEscapePreview()), this, SIGNAL(signalEscapePreview())); #endif //HAVE_MEDIAPLAYER } ImportStackedView::~ImportStackedView() { delete d; } void ImportStackedView::readSettings() { ImportSettings* const settings = ImportSettings::instance(); bool showThumbbar = settings->getShowThumbbar(); d->thumbBarDock->setShouldBeVisible(showThumbbar); } void ImportStackedView::setDockArea(QMainWindow* dockArea) { // Attach the thumbbar dock to the given dock area and place it initially on top. d->dockArea = dockArea; d->thumbBarDock->setParent(d->dockArea); d->dockArea->addDockWidget(Qt::TopDockWidgetArea, d->thumbBarDock); d->thumbBarDock->setFloating(false); } ThumbBarDock* ImportStackedView::thumbBarDock() const { return d->thumbBarDock; } ImportThumbnailBar* ImportStackedView::thumbBar() const { return d->thumbBar; } void ImportStackedView::slotEscapePreview() { #ifdef HAVE_MEDIAPLAYER if (viewMode() == MediaPlayerMode) { d->mediaPlayerView->escapePreview(); } #endif //HAVE_MEDIAPLAYER } ImportIconView* ImportStackedView::importIconView() const { return d->importIconView; } ImportPreviewView* ImportStackedView::importPreviewView() const { return d->importPreviewView; } #ifdef HAVE_MARBLE MapWidgetView* ImportStackedView::mapWidgetView() const { return d->mapWidgetView; } #endif // HAVE_MARBLE #ifdef HAVE_MEDIAPLAYER MediaPlayerView* ImportStackedView::mediaPlayerView() const { return d->mediaPlayerView; } #endif //HAVE_MEDIAPLAYER bool ImportStackedView::isInSingleFileMode() const { return currentIndex() == PreviewImageMode || currentIndex() == MediaPlayerMode; } bool ImportStackedView::isInMultipleFileMode() const { return currentIndex() == PreviewCameraMode || currentIndex() == MapWidgetMode; } void ImportStackedView::setPreviewItem(const CamItemInfo& info, const CamItemInfo& previous, const CamItemInfo& next) { if (info.isNull()) { if (viewMode() == MediaPlayerMode) { #ifdef HAVE_MEDIAPLAYER d->mediaPlayerView->setCurrentItem(); #endif //HAVE_MEDIAPLAYER } else if (viewMode() == PreviewImageMode) { d->importPreviewView->setCamItemInfo(); } } else { if (identifyCategoryforMime(info.mime) == QLatin1String("audio") || identifyCategoryforMime(info.mime) == QLatin1String("video")) { // Stop image viewer if (viewMode() == PreviewImageMode) { d->importPreviewView->setCamItemInfo(); } #ifdef HAVE_MEDIAPLAYER setViewMode(MediaPlayerMode); d->mediaPlayerView->setCurrentItem(info.url(), !previous.isNull(), !next.isNull()); #endif //HAVE_MEDIAPLAYER } else { // Stop media player if running... if (viewMode() == MediaPlayerMode) { #ifdef HAVE_MEDIAPLAYER d->mediaPlayerView->setCurrentItem(); #endif //HAVE_MEDIAPLAYER } d->importPreviewView->setCamItemInfo(info, previous, next); // NOTE: No need to toggle immediately in PreviewImageMode here, // because we will receive a signal for that when the image preview will be loaded. // This will prevent a flicker effect with the old image preview loaded in stack. } // do not touch the selection, only adjust current info QModelIndex currentIndex = d->thumbBar->importSortFilterModel()->indexForCamItemInfo(info); d->thumbBar->selectionModel()->setCurrentIndex(currentIndex, QItemSelectionModel::NoUpdate); } } QString ImportStackedView::identifyCategoryforMime(const QString& mime) const { return mime.split(QLatin1Char('/')).at(0); } ImportStackedView::StackedViewMode ImportStackedView::viewMode() const { return (StackedViewMode)(indexOf(currentWidget())); } void ImportStackedView::setViewMode(const StackedViewMode mode) { if (mode != PreviewCameraMode && mode != PreviewImageMode && mode != MediaPlayerMode && mode != MapWidgetMode) { return; } if (mode == PreviewImageMode || mode == MediaPlayerMode) { d->thumbBarDock->restoreVisibility(); syncSelection(d->importIconView, d->thumbBar); } else { d->thumbBarDock->hide(); } if (mode == PreviewCameraMode || mode == MapWidgetMode) { setPreviewItem(); setCurrentIndex(mode); } else { setCurrentIndex(mode); } #ifdef HAVE_MARBLE d->mapWidgetView->setActive(mode == MapWidgetMode); #endif // HAVE_MARBLE if (mode == PreviewCameraMode) { d->importIconView->setFocus(); } #ifdef HAVE_MARBLE else if (mode == MapWidgetMode) { d->mapWidgetView->setFocus(); } #endif // HAVE_MARBLE emit signalViewModeChanged(); } void ImportStackedView::syncSelection(ImportCategorizedView* const from, ImportCategorizedView* const to) { ImportSortFilterModel* const fromModel = from->importSortFilterModel(); ImportSortFilterModel* const toModel = to->importSortFilterModel(); QModelIndex currentIndex = toModel->indexForCamItemInfo(from->currentInfo()); // sync selection QItemSelection selection = from->selectionModel()->selection(); QItemSelection newSelection; - foreach(const QItemSelectionRange& range, selection) + foreach (const QItemSelectionRange& range, selection) { QModelIndex topLeft = toModel->indexForCamItemInfo(fromModel->camItemInfo(range.topLeft())); QModelIndex bottomRight = toModel->indexForCamItemInfo(fromModel->camItemInfo(range.bottomRight())); newSelection.select(topLeft, bottomRight); } d->syncingSelection = true; if (currentIndex.isValid()) { // set current info to->setCurrentIndex(currentIndex); } to->selectionModel()->select(newSelection, QItemSelectionModel::ClearAndSelect); d->syncingSelection = false; } void ImportStackedView::slotThumbBarSelectionChanged() { if (currentIndex() != PreviewImageMode && currentIndex() != MediaPlayerMode) { return; } if (d->syncingSelection) { return; } syncSelection(d->thumbBar, d->importIconView); } void ImportStackedView::slotIconViewSelectionChanged() { if (currentIndex() != PreviewCameraMode) { return; } if (d->syncingSelection) { return; } syncSelection(d->importIconView, d->thumbBar); } void ImportStackedView::previewLoaded() { emit signalViewModeChanged(); } void ImportStackedView::slotZoomFactorChanged(double z) { if (viewMode() == PreviewImageMode) { emit signalZoomFactorChanged(z); } } void ImportStackedView::increaseZoom() { d->importPreviewView->layout()->increaseZoom(); } void ImportStackedView::decreaseZoom() { d->importPreviewView->layout()->decreaseZoom(); } void ImportStackedView::zoomTo100Percents() { d->importPreviewView->layout()->setZoomFactor(1.0); } void ImportStackedView::fitToWindow() { d->importPreviewView->layout()->fitToWindow(); } void ImportStackedView::toggleFitToWindowOr100() { d->importPreviewView->layout()->toggleFitToWindowOr100(); } bool ImportStackedView::maxZoom() const { return d->importPreviewView->layout()->atMaxZoom(); } bool ImportStackedView::minZoom() const { return d->importPreviewView->layout()->atMinZoom(); } void ImportStackedView::setZoomFactor(double z) { // Giving a null anchor means to use the current view center d->importPreviewView->layout()->setZoomFactor(z, QPoint()); } void ImportStackedView::setZoomFactorSnapped(double z) { d->importPreviewView->layout()->setZoomFactor(z, QPoint(), SinglePhotoPreviewLayout::SnapZoomFactor); } double ImportStackedView::zoomFactor() const { return d->importPreviewView->layout()->zoomFactor(); } double ImportStackedView::zoomMin() const { return d->importPreviewView->layout()->minZoomFactor(); } double ImportStackedView::zoomMax() const { return d->importPreviewView->layout()->maxZoomFactor(); } void ImportStackedView::slotPreviewLoaded(bool) { setViewMode(ImportStackedView::PreviewImageMode); previewLoaded(); } } // namespace Digikam diff --git a/core/utilities/import/views/importthumbnailbar.cpp b/core/utilities/import/views/importthumbnailbar.cpp index 7b10971e92..0a9caec5b9 100644 --- a/core/utilities/import/views/importthumbnailbar.cpp +++ b/core/utilities/import/views/importthumbnailbar.cpp @@ -1,225 +1,225 @@ /* ============================================================ * * This file is a part of digiKam project * https://www.digikam.org * * Date : 2012-20-07 * Description : Thumbnail bar for import tool * * Copyright (C) 2012 by Islam Wazery * Copyright (C) 2012-2019 by Gilles Caulier * * This program is free software you can redistribute it * and/or modify it under the terms of the GNU General * Public License as published by the Free Software Foundation * either version 2, or (at your option) * any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * ============================================================ */ #include "importthumbnailbar.h" // Local includes #include "digikam_debug.h" #include "applicationsettings.h" #include "importsettings.h" #include "importdelegate.h" #include "importfiltermodel.h" #include "importoverlays.h" namespace Digikam { class Q_DECL_HIDDEN ImportThumbnailBar::Private { public: explicit Private() { scrollPolicy = Qt::ScrollBarAlwaysOn; duplicatesFilter = nullptr; } Qt::ScrollBarPolicy scrollPolicy; NoDuplicatesImportFilterModel* duplicatesFilter; }; ImportThumbnailBar::ImportThumbnailBar(QWidget* const parent) : ImportCategorizedView(parent), d(new Private()) { setItemDelegate(new ImportThumbnailDelegate(this)); setSpacing(3); setUsePointingHandCursor(false); setScrollStepGranularity(5); setScrollBarPolicy(Qt::ScrollBarAlwaysOn); setDragEnabled(true); setAcceptDrops(true); setDropIndicatorShown(false); setScrollCurrentToCenter(ApplicationSettings::instance()->getScrollItemToCenter()); setToolTipEnabled(ImportSettings::instance()->showToolTipsIsValid()); connect(ImportSettings::instance(), SIGNAL(setupChanged()), this, SLOT(slotSetupChanged())); slotSetupChanged(); setFlow(LeftToRight); } ImportThumbnailBar::~ImportThumbnailBar() { delete d; } void ImportThumbnailBar::setModelsFiltered(ImportItemModel* model, ImportSortFilterModel* filterModel) { if (!d->duplicatesFilter) { d->duplicatesFilter = new NoDuplicatesImportFilterModel(this); } d->duplicatesFilter->setSourceFilterModel(filterModel); ImportCategorizedView::setModels(model, d->duplicatesFilter); } void ImportThumbnailBar::installOverlays() { ImportRatingOverlay* const ratingOverlay = new ImportRatingOverlay(this); addOverlay(ratingOverlay); connect(ratingOverlay, SIGNAL(ratingEdited(QList,int)), this, SLOT(assignRating(QList,int))); addOverlay(new ImportLockOverlay(this)); addOverlay(new ImportDownloadOverlay(this)); addOverlay(new ImportCoordinatesOverlay(this)); } void ImportThumbnailBar::slotDockLocationChanged(Qt::DockWidgetArea area) { if (area == Qt::LeftDockWidgetArea || area == Qt::RightDockWidgetArea) { setFlow(TopToBottom); } else { setFlow(LeftToRight); } scrollTo(currentIndex()); } void ImportThumbnailBar::setScrollBarPolicy(Qt::ScrollBarPolicy policy) { if (policy == Qt::ScrollBarAsNeeded) { // Delegate resizing will cause endless relayouting, see bug #228807 qCDebug(DIGIKAM_IMPORTUI_LOG) << "The Qt::ScrollBarAsNeeded policy is not supported by ImportThumbnailBar"; } d->scrollPolicy = policy; if (flow() == TopToBottom) { setVerticalScrollBarPolicy(d->scrollPolicy); setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); } else { setHorizontalScrollBarPolicy(d->scrollPolicy); setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); } } void ImportThumbnailBar::setFlow(QListView::Flow flow) { setWrapping(false); ImportCategorizedView::setFlow(flow); ImportThumbnailDelegate* del = static_cast(delegate()); del->setFlow(flow); // Reset the minimum and maximum sizes. setMinimumSize(QSize(0, 0)); setMaximumSize(QSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX)); // Adjust minimum and maximum width to thumbnail sizes. if (flow == TopToBottom) { int viewportFullWidgetOffset = size().width() - viewport()->size().width(); setMinimumWidth(del->minimumSize() + viewportFullWidgetOffset); setMaximumWidth(del->maximumSize() + viewportFullWidgetOffset); } else { int viewportFullWidgetOffset = size().height() - viewport()->size().height(); setMinimumHeight(del->minimumSize() + viewportFullWidgetOffset); setMaximumHeight(del->maximumSize() + viewportFullWidgetOffset); } setScrollBarPolicy(d->scrollPolicy); } void ImportThumbnailBar::slotSetupChanged() { setScrollCurrentToCenter(ApplicationSettings::instance()->getScrollItemToCenter()); setToolTipEnabled(ImportSettings::instance()->showToolTipsIsValid()); setFont(ImportSettings::instance()->getIconViewFont()); ImportCategorizedView::slotSetupChanged(); } void ImportThumbnailBar::assignRating(const QList& indexes, int rating) { QList mappedIndexes = importSortFilterModel()->mapListToSource(indexes); - foreach(const QModelIndex& index, mappedIndexes) + foreach (const QModelIndex& index, mappedIndexes) { if (index.isValid()) { importItemModel()->camItemInfoRef(index).rating = rating; } } } bool ImportThumbnailBar::event(QEvent* e) { // reset widget max/min sizes if (e->type() == QEvent::StyleChange || e->type() == QEvent::Show) { setFlow(flow()); } return ImportCategorizedView::event(e); } QModelIndex ImportThumbnailBar::nextIndex(const QModelIndex& index) const { return importFilterModel()->index(index.row() + 1, 0); } QModelIndex ImportThumbnailBar::previousIndex(const QModelIndex& index) const { return importFilterModel()->index(index.row() - 1, 0); } QModelIndex ImportThumbnailBar::firstIndex() const { return importFilterModel()->index(0, 0); } QModelIndex ImportThumbnailBar::lastIndex() const { return importFilterModel()->index(importFilterModel()->rowCount() - 1, 0); } } // namespace Digikam