Commit 660782be authored by Mario Frank's avatar Mario Frank
Browse files

Added an overall improvement of fuzzy, duplicates and extended search.

1) Users can now restrict the results of fuzzy image/drop/sketch search to be in specific albums.
2) Users can now select both tags and albums in duplicates search and restrict the set of images in which duplicates are searched with logical operations (in selected albums or/and tags, in selected albums but not tags and vice versa and only in
selected objects of current tab)
3) In extended search, the tags selection can also be restricted such that images must be either tagged with at least one of or with all selected tags.

BUGS: 353331 300565 183425
parent 29ee560d
......@@ -3,6 +3,9 @@ digiKam 5.6.0 - Release date: 2017-05-07
NEW FEATURES:
Database : We activated database shrinking for MySQL as part of the maintenance since now we can test the affected tables for integrity.
Searches : Users can now restrict the results of fuzzy image/drop/sketch search to be in specific albums.
Searches : Users can now select both tags and albums in duplicates search and restrict the set of images in which duplicates are searched with logical operations (in selected albums or/and tags, in selected albums but not tags and vice versa and only in selected objects of current tab)
Searches : In extended search, the tags selection can also be restricted such that images must be either tagged with at least one of or with all selected tags.
BUGFIXES FROM BUGZILLA:
......@@ -15,4 +18,7 @@ BUGFIXES FROM BUGZILLA:
007 ==> 372972 - Find duplicates "search in drop down" only shows 1 item.
008 ==> 378360 - digiKam 5.5 crashes on startup after update.
009 ==> 378626 - Export menu empty.
010 ==>
010 ==> 353331 - can’t limit fuzzy-search to albums [patch].
011 ==> 300565 - When selecting folders, subfolders are not selected.
012 ==> 183425 - Advanced search by tags : "any tags" option does not work
013 ==>
......@@ -215,6 +215,7 @@ SAlbum* SearchModificationHelper::slotCreateTimeLineSearch(const QString& desire
SAlbum* SearchModificationHelper::createFuzzySearchFromSketch(const QString& proposedName,
SketchWidget* sketchWidget,
unsigned int numberOfResults,
QList<int>& targetAlbums,
bool overwriteIfExisting)
{
if (sketchWidget->isClear())
......@@ -245,6 +246,13 @@ SAlbum* SearchModificationHelper::createFuzzySearchFromSketch(const QString& pro
writer.writeValue(haarIface.signatureAsText(sketchWidget->sketchImage()));
sketchWidget->sketchImageToXML(writer);
writer.finishField();
// Add the target albums, i.e. define that the found similar images
// must be located in one of the target albums.
writer.writeField(QLatin1String("noeffect_targetAlbums"), SearchXml::OneOf);
writer.writeValue(targetAlbums);
writer.finishField();
writer.finishGroup();
writer.finish();
......@@ -257,15 +265,17 @@ SAlbum* SearchModificationHelper::createFuzzySearchFromSketch(const QString& pro
void SearchModificationHelper::slotCreateFuzzySearchFromSketch(const QString& proposedName,
SketchWidget* sketchWidget,
unsigned int numberOfResults,
QList<int>& targetAlbums,
bool overwriteIfExisting)
{
createFuzzySearchFromSketch(proposedName, sketchWidget, numberOfResults, overwriteIfExisting);
createFuzzySearchFromSketch(proposedName, sketchWidget, numberOfResults, targetAlbums, overwriteIfExisting);
}
SAlbum* SearchModificationHelper::createFuzzySearchFromDropped(const QString& proposedName,
const QString& filePath,
float threshold,
float maxThreshold,
QList<int>& targetAlbums,
bool overwriteIfExisting)
{
QString name = proposedName;
......@@ -291,6 +301,13 @@ SAlbum* SearchModificationHelper::createFuzzySearchFromDropped(const QString& pr
writer.writeAttribute(QLatin1String("sketchtype"), QLatin1String("scanned"));
writer.writeValue(filePath);
writer.finishField();
// Add the target albums, i.e. define that the found similar images
// must be located in one of the target albums.
writer.writeField(QLatin1String("noeffect_targetAlbums"), SearchXml::OneOf);
writer.writeValue(targetAlbums);
writer.finishField();
writer.finishGroup();
writer.finish();
......@@ -304,15 +321,17 @@ void SearchModificationHelper::slotCreateFuzzySearchFromDropped(const QString& p
const QString& filePath,
float threshold,
float maxThreshold,
QList<int>& targetAlbums,
bool overwriteIfExisting)
{
createFuzzySearchFromDropped(proposedName, filePath, threshold, maxThreshold, overwriteIfExisting);
createFuzzySearchFromDropped(proposedName, filePath, threshold, maxThreshold, targetAlbums, overwriteIfExisting);
}
SAlbum* SearchModificationHelper::createFuzzySearchFromImage(const QString& proposedName,
const ImageInfo& image,
float threshold,
float maxThreshold,
QList<int>& targetAlbums,
bool overwriteIfExisting)
{
if (image.isNull())
......@@ -342,6 +361,13 @@ SAlbum* SearchModificationHelper::createFuzzySearchFromImage(const QString& prop
writer.writeAttribute(QLatin1String("sketchtype"), QLatin1String("scanned"));
writer.writeValue(image.id());
writer.finishField();
// Add the target albums, i.e. define that the found similar images
// must be located in one of the target albums.
writer.writeField(QLatin1String("noeffect_targetAlbums"), SearchXml::OneOf);
writer.writeValue(targetAlbums);
writer.finishField();
writer.finishGroup();
writer.finish();
......@@ -354,9 +380,11 @@ SAlbum* SearchModificationHelper::createFuzzySearchFromImage(const QString& prop
void SearchModificationHelper::slotCreateFuzzySearchFromImage(const QString& proposedName,
const ImageInfo& image,
float threshold,
float maxThreshold,
QList<int>& targetAlbums,
bool overwriteIfExisting)
{
createFuzzySearchFromImage(proposedName, image, threshold, overwriteIfExisting);
createFuzzySearchFromImage(proposedName, image, threshold, maxThreshold, targetAlbums, overwriteIfExisting);
}
} // namespace Digikam
......@@ -81,6 +81,7 @@ public:
SAlbum* createFuzzySearchFromSketch(const QString& name,
SketchWidget* sketchWidget,
unsigned int numberOfResults,
QList<int>& targetAlbums,
bool overwriteIfExisting = false);
/**
......@@ -91,6 +92,7 @@ public:
const QString& filePath,
float threshold,
float maxThreshold,
QList<int>& targetAlbums,
bool overwriteIfExisting = false);
/**
......@@ -101,6 +103,7 @@ public:
const ImageInfo& image,
float threshold,
float maxThreshold,
QList<int>& targetAlbums,
bool overwriteIfExisting = false);
public Q_SLOTS:
......@@ -142,6 +145,7 @@ public Q_SLOTS:
* @param name name of the new sketch search
* @param sketchWidget the widget containing the sketch of the user
* @param numberOfResults max number of results to display
* @param targetAlbums The image must be in one of these albums
* @param overwriteIfExisting if true, an existing search with the desired
* name will be overwritten without prompting the
* user for a new name
......@@ -149,6 +153,7 @@ public Q_SLOTS:
void slotCreateFuzzySearchFromSketch(const QString& name,
SketchWidget* sketchWidget,
unsigned int numberOfResults,
QList<int>& targetAlbums,
bool overwriteIfExisting = false);
/**
......@@ -158,6 +163,8 @@ public Q_SLOTS:
* @param name of the new search
* @param image image to base this search on
* @param threshold threshold for image search, 0 <= threshold <= 1
* @param maxThreshold the maximum threshold of similarity.
* @param targetAlbums The image must be in one of these albums
* @param overwriteIfExisting if true, an existing search with the desired
* name will be overwritten without prompting the
* user for a new name
......@@ -165,6 +172,8 @@ public Q_SLOTS:
void slotCreateFuzzySearchFromImage(const QString& name,
const ImageInfo& image,
float threshold,
float maxThreshold,
QList<int>& targetAlbums,
bool overwriteIfExisting = false);
/**
......@@ -176,6 +185,7 @@ public Q_SLOTS:
* @param filePath path of the image to base this search on
* @param threshold minimum threshold for image search
* @param maxThreshold maximum threshold for image search
* @param targetAlbums The image must be in one of these albums
* @param overwriteIfExisting if true, an existing search with the desired
* name will be overwritten without prompting the
* user for a new name
......@@ -184,6 +194,7 @@ public Q_SLOTS:
const QString& filePath,
float threshold,
float maxThreshold,
QList<int>& targetAlbums,
bool overwriteIfExisting);
private:
......
#include "albumselectioncomponent.h"
// Qt includes
#include <QApplication>
#include <QLayout>
#include <QLabel>
#include <QCheckBox>
#include <QRadioButton>
#include <QButtonGroup>
#include <QIcon>
// KDE includes
#include <klocalizedstring.h>
// Local includes
#include "digikam_debug.h"
#include "albummodel.h"
#include "albummanager.h"
#include "albumselectcombobox.h"
#include "albumtreeview.h"
#include "searchutilities.h"
namespace Digikam
{
class ModelClearButton : public AnimatedClearButton
{
public:
explicit ModelClearButton(AbstractCheckableAlbumModel* const model)
{
setPixmap(QIcon::fromTheme(qApp->isLeftToRight() ? QLatin1String("edit-clear-locationbar-rtl")
: QLatin1String("edit-clear-locationbar-ltr")).pixmap(style()->pixelMetric(QStyle::PM_SmallIconSize)));
stayVisibleWhenAnimatedOut(true);
connect(this, SIGNAL(clicked()),
model, SLOT(resetAllCheckedAlbums()));
}
};
// ------------------------------------------------------------------------------------------
class SelectionComponent::Private
{
public:
Private()
{
wholeElements = 0;
clearButton = 0;
}
static const QString configUseWholeEntry;
QString configName;
QCheckBox* wholeElements;
ModelClearButton* clearButton;
};
SelectionComponent::SelectionComponent(const QString& label, const QString& configName, QWidget* const parent)
: QWidget(parent),
d(new Private)
{
}
SelectionComponent::~SelectionComponent()
{
delete d;
}
void SelectionComponent::slotWholeAlbums(bool b)
{
d->albumSelectCB->setEnabled(!b);
d->clearButton->setEnabled(!b);
}
bool SelectionComponent::wholeElementsChecked() const
{
return d->wholeElements->isChecked();
}
QList<int> SelectionComponent::selectedElementIds() const
{
QList<int> albumIds;
AlbumList albums = selectedElements();
foreach (Album* const album, albums)
{
albumIds << album->id();
}
return albumIds;
}
void SelectionComponent::setElementSelected(Album* const album, bool singleSelection)
{
if (singleSelection)
{
d->albumSelectCB->model()->resetCheckedAlbums();
}
d->albumSelectCB->model()->setChecked(album, true);
d->wholeElements->setChecked(false);
}
void SelectionComponent::resetSelection()
{
if (d->albumSelection)
{
d->albumSelectCB->model()->resetCheckedAlbums();
}
if (d->tagSelection)
{
d->tagSelectCB->model()->resetCheckedAlbums();
}
}
void AlbumSelectionComponent::loadState()
{
KSharedConfig::Ptr config = KSharedConfig::openConfig();
KConfigGroup group = config->group(d->configName);
if (d->albumSelection)
{
d->wholeAlbums->setChecked(group.readEntry(d->configUseWholeAlbumsEntry, true));
d->albumSelectCB->view()->loadState();
d->albumClearButton->animateVisible(!d->albumSelectCB->model()->checkedAlbums().isEmpty());
slotWholeAlbums(wholeAlbumsChecked());
}
if (d->tagSelection)
{
d->wholeTags->setChecked(group.readEntry(d->configUseWholeTagsEntry, true));
d->tagSelectCB->view()->loadState();
d->tagClearButton->animateVisible(!d->tagSelectCB->model()->checkedAlbums().isEmpty());
slotWholeTags(wholeTagsChecked());
}
if (d->selectionMode == All)
{
d->tabWidget->setCurrentIndex(group.readEntry(d->configAlbumTypeEntry, (int)PhysAlbum));
}
}
void AlbumSelectionComponent::saveState()
{
KSharedConfig::Ptr config = KSharedConfig::openConfig();
KConfigGroup group = config->group(d->configName);
if (d->albumSelection)
{
group.writeEntry(d->configUseWholeAlbumsEntry, wholeAlbumsChecked());
d->albumSelectCB->view()->saveState();
}
if (d->tagSelection)
{
group.writeEntry(d->configUseWholeTagsEntry, wholeTagsChecked());
d->tagSelectCB->view()->saveState();
}
if (d->selectionMode == All)
{
group.writeEntry(d->configAlbumTypeEntry, typeSelection());
}
}
/// ------------------------------------------------------------------
class AlbumSelectionComponent::Private
{
public:
Private()
{
albumSelectCB = 0;
}
AlbumTreeViewSelectComboBox* albumSelectCB;
};
AlbumSelectionComponent::AlbumSelectionComponent(const QString& label, const QString& configName, QWidget* const parent)
: SelectionComponent(label,configName, parent),
da(new Private)
{
d->wholeElements = new QCheckBox(i18n("Whole albums collection"), this);
da->albumSelectCB = new AlbumTreeViewSelectComboBox(this);
da->albumSelectCB->setToolTip(i18nc("@info:tooltip", "Select all albums that should be processed."));
da->albumSelectCB->setDefaultModel();
da->albumSelectCB->setNoSelectionText(i18nc("@info:status", "Any albums"));
da->albumSelectCB->addCheckUncheckContextMenuActions();
d->clearButton = new ModelClearButton(da->albumSelectCB->view()->albumModel());
d->clearButton->setToolTip(i18nc("@info:tooltip", "Reset selected albums"));
QGridLayout* const pAlbumsGrid = new QGridLayout(this);
pAlbumsGrid->addWidget(d->wholeElements, 0, 0, 1, 2);
pAlbumsGrid->addWidget(da->albumSelectCB, 1, 0, 1, 1);
pAlbumsGrid->addWidget(d->clearButton, 1, 1, 1, 1);
pAlbumsGrid->setSpacing(0);
connect(d->wholeElements, SIGNAL(toggled(bool)),
this, SLOT(slotWholeAlbums(bool)));
connect(d->wholeElements, SIGNAL(toggled(bool)),
this, SIGNAL(signalSelectionChanged()));
connect(da->albumSelectCB->view()->albumModel(), SIGNAL(checkStateChanged(Album*, Qt::CheckState)),
this, SLOT(slotUpdateClearButtons()));
da->albumSelectCB->view()->setObjectName(d->configName);
da->albumSelectCB->view()->setEntryPrefix(QLatin1String("AlbumComboBox-"));
da->albumSelectCB->view()->setRestoreCheckState(true);
}
AlbumList AlbumSelectionComponent::selectedElements() const
{
AlbumList albums;
if (wholeElementsChecked())
{
albums << AlbumManager::instance()->allPAlbums();
}
else
{
albums << da->albumSelectCB->model()->checkedAlbums();
}
return albums;
}
/// ------------------------------------------------------------------
class TagSelectionComponent::Private
{
public:
Private()
{
tagSelectCB = 0;
}
TagTreeViewSelectComboBox* tagSelectCB;
};
TagSelectionComponent::TagSelectionComponent(const QString& label, const QString& configName, QWidget* const parent)
: SelectionComponent(label,configName, parent),
dt(new Private)
{
d->wholeElements = new QCheckBox(i18n("Whole albums collection"), this);
dt->tagSelectCB = new TagTreeViewSelectComboBox(this);
dt->tagSelectCB->setToolTip(i18nc("@info:tooltip", "Select all albums that should be processed."));
dt->tagSelectCB->setDefaultModel();
dt->tagSelectCB->setNoSelectionText(i18nc("@info:status", "Any albums"));
dt->tagSelectCB->addCheckUncheckContextMenuActions();
d->clearButton = new ModelClearButton(dt->tagSelectCB->view()->albumModel());
d->clearButton->setToolTip(i18nc("@info:tooltip", "Reset selected albums"));
QGridLayout* const pAlbumsGrid = new QGridLayout(this);
pAlbumsGrid->addWidget(d->wholeElements, 0, 0, 1, 2);
pAlbumsGrid->addWidget(dt->tagSelectCB, 1, 0, 1, 1);
pAlbumsGrid->addWidget(d->clearButton, 1, 1, 1, 1);
pAlbumsGrid->setSpacing(0);
connect(d->wholeElements, SIGNAL(toggled(bool)),
this, SLOT(slotWholeAlbums(bool)));
connect(d->wholeElements, SIGNAL(toggled(bool)),
this, SIGNAL(signalSelectionChanged()));
connect(dt->tagSelectCB->view()->albumModel(), SIGNAL(checkStateChanged(Album*, Qt::CheckState)),
this, SLOT(slotUpdateClearButtons()));
dt->tagSelectCB->view()->setObjectName(d->configName);
dt->tagSelectCB->view()->setEntryPrefix(QLatin1String("TagComboBox-"));
dt->tagSelectCB->view()->setRestoreCheckState(true);
}
AlbumList TagSelectionComponent::selectedElements() const
{
AlbumList albums;
if (wholeElementsChecked())
{
albums << AlbumManager::instance()->allPAlbums();
}
else
{
albums << dt->tagSelectCB->model()->checkedAlbums();
}
return albums;
}
} // namespace Digikam
/* ============================================================
*
* This file is a part of digiKam project
* http://www.digikam.org
*
* Date : 2017-03-03
* Description : A widget to select Physical or virtual albums with combo-box
*
* Copyright (C) 2016 by Mario Frank <mario dot frank at uni minus potsdam dot de>
*
* 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.
*
* ============================================================ */
#ifndef ALBUMSELECTIONCOMPONENT_H
#define ALBUMSELECTIONCOMPONENT_H
// Qt includes
#include <QWidget>
namespace Digikam
{
class Album;
typedef QList<Album*> AlbumList;
class SelectionComponent : public QWidget
{
Q_OBJECT
public:
~SelectionComponent();
/** Return list of selected element ids
*/
QList<int> selectedElementIds() const;
void resetSelection();
/** Return true if whole Albums collection option is checked.
*/
bool wholeElementsChecked() const;
/** Select Physical Album from list. If singleSelection is true, only this one is
* selected from tree-view and all others are deselected.
*/
void setElementSelected(Album* const album, bool singleSelection=true);
Q_SIGNALS:
void signalSelectionChanged();
protected:
/** Default Contructor. 'label' is front text of label which title widget. 'configName' is name used to store
* Albums configuration in settings file. 'parent' is parent widget.
*/
explicit SelectionComponent(const QString& label, const QString& configName, QWidget* const parent = 0);
virtual AlbumList selectedElements() const = 0;
private Q_SLOTS:
void slotUpdateClearButtons();
void slotWholeAlbums(bool);
void slotWholeTags(bool);
private:
QWidget* initAlbumSelection(QWidget* parent);
QWidget* initTagSelection(QWidget* parent);
protected:
class Private;
Private* const d;
};
class AlbumSelectionComponent : public SelectionComponent
{
public:
explicit AlbumSelectionComponent(const QString& label, const QString& configName, QWidget* const parent = 0);
AlbumList selectedElements() const;
private:
class Private;
Private* const da;
};
class TagSelectionComponent : public SelectionComponent
{
public:
explicit TagSelectionComponent(const QString& label, const QString& configName, QWidget* const parent = 0);
AlbumList selectedElements() const;
private:
class Private;
Private* const dt;
};
} // namespace Digikam