diff --git a/Grinder/Grinder.pro b/Grinder/Grinder.pro index 1e5fbc4fa4b4e51425ba6c614dafcc58478370a6..dc51a21b870af908566063e347ddaac31056c72d 100644 --- a/Grinder/Grinder.pro +++ b/Grinder/Grinder.pro @@ -486,7 +486,9 @@ SOURCES += \ ui/image/commands/ConvertDraftItemsUndoCommand.cpp \ ui/image/commands/ConvertPixelsUndoCommand.cpp \ ui/image/commands/CreateLayerUndoCommand.cpp \ - ui/image/commands/RemoveLayerUndoCommand.cpp + ui/image/commands/RemoveLayerUndoCommand.cpp \ + pipeline/blocks/MaskInputBlock.cpp \ + engine/processors/MaskInputProcessor.cpp HEADERS += \ ui/mainwnd/GrinderWindow.h \ @@ -1065,7 +1067,10 @@ HEADERS += \ ui/image/commands/LayersPixelsUndoCommand.h \ ui/image/commands/LayersPixelsUndoCommand.impl.h \ ui/image/commands/CreateLayerUndoCommand.h \ - ui/image/commands/RemoveLayerUndoCommand.h + ui/image/commands/RemoveLayerUndoCommand.h \ + util/MathUtils.impl.h \ + pipeline/blocks/MaskInputBlock.h \ + engine/processors/MaskInputProcessor.h FORMS += \ ui/mainwnd/GrinderWindow.ui \ diff --git a/Grinder/Version.h b/Grinder/Version.h index 6d6c9fc0b6d95de6eb25a44aada9c8f34baea67c..777de0e247f9697a94d862a665e418dabd3fbce5 100644 --- a/Grinder/Version.h +++ b/Grinder/Version.h @@ -10,14 +10,14 @@ #define GRNDR_INFO_TITLE "Grinder" #define GRNDR_INFO_COPYRIGHT "Copyright (c) WWU Muenster" -#define GRNDR_INFO_DATE "13.11.2019" +#define GRNDR_INFO_DATE "30.12.2019" #define GRNDR_INFO_COMPANY "WWU Muenster" #define GRNDR_INFO_WEBSITE "http://www.uni-muenster.de" #define GRNDR_VERSION_MAJOR 0 #define GRNDR_VERSION_MINOR 16 #define GRNDR_VERSION_REVISION 0 -#define GRNDR_VERSION_BUILD 414 +#define GRNDR_VERSION_BUILD 419 namespace grndr { diff --git a/Grinder/common/properties/PropertyID.cpp b/Grinder/common/properties/PropertyID.cpp index ff16eaae3f01780dbf4a0e9d5c9ac10c97ce5ba5..edcc88574436708153898f6756db741106e71109 100644 --- a/Grinder/common/properties/PropertyID.cpp +++ b/Grinder/common/properties/PropertyID.cpp @@ -73,4 +73,6 @@ const char* PropertyID::Network = "Network"; const char* PropertyID::MaxIterations = "MaxIterations"; const char* PropertyID::DisplayInterval = "DisplayInterval"; const char* PropertyID::SnapshotInterval = "SnapshotInterval"; +const char* PropertyID::Renderable = "Renderable"; const char* PropertyID::State = "State"; +const char* PropertyID::Filename = "Filename"; diff --git a/Grinder/common/properties/PropertyID.h b/Grinder/common/properties/PropertyID.h index 879f6a396d1be0ad5c81deb4617a5812cc32c4e4..9ca6d4c9c5054808bece8c6b73b22a79aa4a71e6 100644 --- a/Grinder/common/properties/PropertyID.h +++ b/Grinder/common/properties/PropertyID.h @@ -80,7 +80,9 @@ namespace grndr static const char* MaxIterations; static const char* DisplayInterval; static const char* SnapshotInterval; + static const char* Renderable; static const char* State; + static const char* Filename; public: using QString::QString; diff --git a/Grinder/controller/ImageEditorController.cpp b/Grinder/controller/ImageEditorController.cpp index 2f70c89697fbeb7c79c2b09cb7c94b9b37786691..7a3feea81efe53aab93452e13b1518d257f9e6de 100644 --- a/Grinder/controller/ImageEditorController.cpp +++ b/Grinder/controller/ImageEditorController.cpp @@ -865,6 +865,14 @@ void ImageEditorController::removeSelectedNodes() const } } +void ImageEditorController::createImageTag(QString name, QColor color) const +{ + callControllerFunction("Creating an image tag", [this, name, color]() { + _activeImageBuild->generateImageTag(color, name); + return true; + }); +} + void ImageEditorController::assignImageTags(ImageTagsAllotmentProperty* imageTagsAllotment, const QStringList& imageTagNames, bool assign, bool clearFirst) const { callControllerFunction("Assigning tags", [this](ImageTagsAllotmentProperty* imageTagsAllotment, const QStringList& imageTagNames, bool assign, bool clearFirst) { @@ -901,6 +909,35 @@ void ImageEditorController::assignImageTags(ImageTagsAllotmentProperty* imageTag } } +void ImageEditorController::autoGenerateImageTags(const Layer* layer, unsigned int* createdCount) const +{ + if (layer) + { + auto count = callControllerFunction("Auto-generating image tags", [this](const Layer* layer) { + unsigned int count = 0; + + if (auto inputImageTags = _activeImageBuild->inputImageTags()) + { + auto colors = layer->layerPixels().data().colors(); + + for (auto color : colors) + { + if (!inputImageTags->tags().selectByColor(color)) + { + createImageTag(color.name(), color); + count += 1; + } + } + } + + return count; + }, layer); + + if (createdCount) + *createdCount = count; + } +} + void ImageEditorController::controllerFunctionCalled() const { // Redraw the scene after calling a controller function diff --git a/Grinder/controller/ImageEditorController.h b/Grinder/controller/ImageEditorController.h index 4300d4016c0d0b4c24f31926fbca9fdf6b20f320..11ef383a1e3a457c0cd4de828508e58a9799c278 100644 --- a/Grinder/controller/ImageEditorController.h +++ b/Grinder/controller/ImageEditorController.h @@ -95,8 +95,10 @@ namespace grndr void cutSelectedNodes() const; void removeSelectedNodes() const; + void createImageTag(QString name, QColor color) const; void assignImageTags(ImageTagsAllotmentProperty* imageTagsAllotment, const QStringList& imageTagNames, bool assign = true, bool clearFirst = false) const; void assignImageTags(ImageTagsAllotmentProperty* imageTagsAllotment, const std::set<ImageTag*>& imageTags, bool assign = true, bool clearFirst = false) const; + void autoGenerateImageTags(const Layer* layer, unsigned int* createdCount = nullptr) const; public: Restrictions& restrictions() { return _restrictions; } diff --git a/Grinder/controller/PipelineController.h b/Grinder/controller/PipelineController.h index d1a4dab7efffea41012de8fa0d37d115bea444fa..5e10cdb2c1e5f9823fe6fb968705282902e58631 100644 --- a/Grinder/controller/PipelineController.h +++ b/Grinder/controller/PipelineController.h @@ -23,7 +23,6 @@ namespace grndr class GraphView; class GraphScene; class ImageReference; - class BatchInputProviderBlock; class PipelineController : public GenericController { diff --git a/Grinder/engine/processors/CanvasProcessor.cpp b/Grinder/engine/processors/CanvasProcessor.cpp index 9422d1b053304a3ffa852fc06b13a3e68360da00..777818672d5d47d458770acab6c4d76f645deae7 100644 --- a/Grinder/engine/processors/CanvasProcessor.cpp +++ b/Grinder/engine/processors/CanvasProcessor.cpp @@ -42,7 +42,7 @@ void CanvasProcessor::execute(EngineExecutionContext& ctx) } if (!layersData.empty()) - imageBuild->generateLayers(layersData, *_block->colorKey(), _block->tolerance()->getRelativeValue(), *_block->usePerceivedDistance()); + imageBuild->generateLayers(layersData, *_block->inLayersRenderable(), *_block->colorKey(), _block->tolerance()->getRelativeValue(), *_block->usePerceivedDistance()); // Render the entire image build to provide it as this block's output cv::Mat renderedImage = imageBuild->renderImageBuild(); diff --git a/Grinder/engine/processors/MaskInputProcessor.cpp b/Grinder/engine/processors/MaskInputProcessor.cpp new file mode 100644 index 0000000000000000000000000000000000000000..f9241c0f7d7e28382987baf04a638307c4309998 --- /dev/null +++ b/Grinder/engine/processors/MaskInputProcessor.cpp @@ -0,0 +1,49 @@ +/****************************************************************************** + * File: MaskInputProcessor.cpp + * Date: 30.12.2019 + *****************************************************************************/ + +#include "Grinder.h" +#include "MaskInputProcessor.h" +#include "project/ImageReference.h" + +#include <opencv2/highgui.hpp> + +MaskInputProcessor::MaskInputProcessor(const Block* block) : Processor(block) +{ + +} + +void MaskInputProcessor::execute(EngineExecutionContext& ctx) +{ + Processor::execute(ctx); + + if (auto imageReference = ctx.activeImageReference()) + { + // Format the mask filename according to the provided pattern + QString filename = *_block->maskFilename(); + + if (filename.isEmpty()) + throwProcessorException("No mask filename pattern provided"); + + QDir imageDir = QFileInfo{imageReference->getImageFilePath()}.dir(); + QFileInfo imageFilename = imageReference->getImageFileName(); + QString imageBasename = imageFilename.completeBaseName(); + QString imageExt = imageFilename.suffix(); + + filename.replace("$filename", imageFilename.filePath(), Qt::CaseInsensitive); + filename.replace("$basename", imageBasename, Qt::CaseInsensitive); + filename.replace("$ext", imageExt, Qt::CaseInsensitive); + + QFileInfo maskFile{imageDir, filename}; + + if (!maskFile.exists()) + throwProcessorException(QString{"The mask file '%1' doesn't exist"}.arg(maskFile.filePath())); + + // Load and forward the mask file + auto maskImage = cv::imread(maskFile.canonicalFilePath().toStdString(), cv::IMREAD_COLOR); + ctx.data().set(_block->outPort(), DataBlob{getPortDataDescriptor(_block->outPort()), std::move(maskImage)}); + } + else + throwProcessorException("There currently is no active image"); +} diff --git a/Grinder/engine/processors/MaskInputProcessor.h b/Grinder/engine/processors/MaskInputProcessor.h new file mode 100644 index 0000000000000000000000000000000000000000..3dd0b36b14fa6350952e63931dfd1e373a9dc183 --- /dev/null +++ b/Grinder/engine/processors/MaskInputProcessor.h @@ -0,0 +1,26 @@ +/****************************************************************************** + * File: MaskInputProcessor.h + * Date: 30.12.2019 + *****************************************************************************/ + +#ifndef MASKINPUTPROCESSOR_H +#define MASKINPUTPROCESSOR_H + +#include "engine/Processor.h" +#include "pipeline/blocks/MaskInputBlock.h" + +namespace grndr +{ + class MaskInputProcessor : public Processor<MaskInputBlock> + { + Q_OBJECT + + public: + MaskInputProcessor(const Block* block); + + public: + virtual void execute(EngineExecutionContext& ctx) override; + }; +} + +#endif diff --git a/Grinder/image/ImageBuild.cpp b/Grinder/image/ImageBuild.cpp index 1d4ede5345bda905013f657ec96be491736d95cb..96ea208c8a654f0f5652e08e7ce7be22f8d80831 100644 --- a/Grinder/image/ImageBuild.cpp +++ b/Grinder/image/ImageBuild.cpp @@ -130,20 +130,33 @@ void ImageBuild::moveLayer(const Layer* layer, bool up) throw ImageBuildException{this, _EXCPT("Tried to move a layer not belonging to this image build")}; } -void ImageBuild::generateLayers(const LayersGenerationData& layersData, QColor colorKey, float tolerance, bool perceivedDifference) +void ImageBuild::generateLayers(const LayersGenerationData& layersData, bool renderable, QColor colorKey, float tolerance, bool perceivedDifference) { for (auto layerData : layersData) { auto layer = _layers.selectByGeneratingBlock(layerData.first); if (!layer) - layer = createLayer(QString{"In: %1"}.arg(layerData.first->getName()), Layer::Type::AutoGenerated, Layer::Flag::Renderable, nullptr, layerData.first); + { + Layer::Flags layerFlags = Layer::Flag::None; + + if (renderable) + layerFlags |= Layer::Flag::Renderable; + + layer = createLayer(QString{"In: %1"}.arg(layerData.first->getName()), Layer::Type::AutoGenerated, layerFlags, nullptr, layerData.first); + } if (layer && !layer->hasFlag(Layer::Flag::PreventUpdates)) layer->layerPixels().data().fromMatrix(layerData.second->getMatrix(), colorKey, tolerance, perceivedDifference); } } +void ImageBuild::generateImageTag(QColor color, QString name) +{ + if (auto imageTags = inputImageTags()) + imageTags->autoGenerateNamedTag(color, name); +} + void ImageBuild::autoGenerateImageTag(QColor color) { if (auto autoGenerate = _block->portProperty<BoolProperty>(PortType::ImageTagsIn, PropertyID::AutoGenerate)) diff --git a/Grinder/image/ImageBuild.h b/Grinder/image/ImageBuild.h index 495e9906d3a851650bec7df4bb12346911f34569..b118b1d7a42fdceb90602ddad4346053d72b7811 100644 --- a/Grinder/image/ImageBuild.h +++ b/Grinder/image/ImageBuild.h @@ -42,9 +42,10 @@ namespace grndr void moveLayer(const Layer* layer, bool up); - void generateLayers(const LayersGenerationData& layersData, QColor colorKey, float tolerance, bool perceivedDifference); + void generateLayers(const LayersGenerationData& layersData, bool renderable, QColor colorKey, float tolerance, bool perceivedDifference); public: + void generateImageTag(QColor color, QString name); void autoGenerateImageTag(QColor color); public: diff --git a/Grinder/image/ImageTags.cpp b/Grinder/image/ImageTags.cpp index 81cf1223ae3b50cf281102c4c9649b31e19d02f3..f59ab64f485b97f309d803533ef2c185685b175c 100644 --- a/Grinder/image/ImageTags.cpp +++ b/Grinder/image/ImageTags.cpp @@ -65,7 +65,7 @@ void ImageTags::removeAllTags() removeTag(tag.get()); } -std::shared_ptr<ImageTag> ImageTags::autoGenerateTag(QColor color, bool checkTagLimit) +std::shared_ptr<ImageTag> ImageTags::autoGenerateNamedTag(QColor color, QString name, bool checkTagLimit) { auto imageTag = _tags.selectByColor(color); @@ -73,7 +73,7 @@ std::shared_ptr<ImageTag> ImageTags::autoGenerateTag(QColor color, bool checkTag { if (!checkTagLimit || _tags.size() < Maximum_Tags_Count) { - imageTag = makeTag(color.name(), color); + imageTag = makeTag(name, color); addTag(imageTag); } } diff --git a/Grinder/image/ImageTags.h b/Grinder/image/ImageTags.h index f1b57c2838eef81e0e8a7adb68c050943c0c0f87..1b155b41d9a1902e15896c25735bc2eb8299a16c 100644 --- a/Grinder/image/ImageTags.h +++ b/Grinder/image/ImageTags.h @@ -32,7 +32,8 @@ namespace grndr void removeAllTags(); std::shared_ptr<ImageTag> makeTag(QString name, QColor color = QColor{}) { return std::make_shared<ImageTag>(this, name, color); } - std::shared_ptr<ImageTag> autoGenerateTag(QColor color, bool checkTagLimit = true); + std::shared_ptr<ImageTag> autoGenerateTag(QColor color, bool checkTagLimit = true) { return autoGenerateNamedTag(color, color.name(), checkTagLimit); } + std::shared_ptr<ImageTag> autoGenerateNamedTag(QColor color, QString name, bool checkTagLimit = true); public: const ImageTagVector& tags() const { return _tags; } diff --git a/Grinder/pipeline/BlockCatalog.cpp b/Grinder/pipeline/BlockCatalog.cpp index 3333da8db55a386660aa0dd1960c2befbef23f7d..4ee8c5086aab60e56538e237aaae725a04ecd0fe 100644 --- a/Grinder/pipeline/BlockCatalog.cpp +++ b/Grinder/pipeline/BlockCatalog.cpp @@ -8,6 +8,7 @@ #include "blocks/InputBlock.h" #include "blocks/BatchInputBlock.h" +#include "blocks/MaskInputBlock.h" #include "blocks/CanvasBlock.h" #include "blocks/ConvertToGrayscaleBlock.h" #include "blocks/ConvertToColorBlock.h" @@ -125,6 +126,7 @@ void BlockCatalog::registerStandardBlocks() { REGISTER_BLOCK_TYPE(InputBlock, "Provides an image of the image list."); REGISTER_BLOCK_TYPE(BatchInputBlock, "Provides multiple images of the image list for batch processing."); + REGISTER_BLOCK_TYPE(MaskInputBlock, "Provides an additional mask for the currently active image."); REGISTER_BLOCK_TYPE(CanvasBlock, "Provides the image for outputting and also allows manual editing in the image editor."); REGISTER_BLOCK_TYPE(ConvertToGrayscaleBlock, "Converts its input to a grayscale image."); REGISTER_BLOCK_TYPE(ConvertToColorBlock, "Converts its input to a color image of various color spaces."); diff --git a/Grinder/pipeline/BlockType.cpp b/Grinder/pipeline/BlockType.cpp index 687aff702929305933baac92a1d92d9883095084..99efd1a32327ab3c0157f2508f4261cd021aa572 100644 --- a/Grinder/pipeline/BlockType.cpp +++ b/Grinder/pipeline/BlockType.cpp @@ -10,6 +10,7 @@ const char* BlockType::Undefined = ""; const char* BlockType::Input = "Input"; const char* BlockType::BatchInput = "BatchInput"; +const char* BlockType::MaskInput = "MaskInput"; const char* BlockType::_Output_ = "Output"; const char* BlockType::Canvas = "Canvas"; diff --git a/Grinder/pipeline/BlockType.h b/Grinder/pipeline/BlockType.h index 37de58364314c3f484b339e837045bb159705ebf..3bbc9eb6e1c5ff88765be1c808783e62a68617dd 100644 --- a/Grinder/pipeline/BlockType.h +++ b/Grinder/pipeline/BlockType.h @@ -17,6 +17,7 @@ namespace grndr static const char* Input; static const char* BatchInput; + static const char* MaskInput; static const char* _Output_; /* Obsolete */ static const char* Canvas; diff --git a/Grinder/pipeline/blocks/CanvasBlock.cpp b/Grinder/pipeline/blocks/CanvasBlock.cpp index e37287646b4e80309b33842b1dc32a1d1fac62df..c4c584a7f6c3911f54edba66559368d9324c0b47 100644 --- a/Grinder/pipeline/blocks/CanvasBlock.cpp +++ b/Grinder/pipeline/blocks/CanvasBlock.cpp @@ -26,6 +26,9 @@ void CanvasBlock::createProperties() setPropertyGroup("Layers"); + _inLayersRenderable = createProperty<BoolProperty>(PropertyID::Renderable, "Renderable", true); + inLayersRenderable()->setDescription("Whether layers generated from incoming images connected to the Layers port are renderable by default."); + _colorKey = createProperty<ColorProperty>(PropertyID::Color, "Background color", QColor{255, 0, 255}); colorKey()->setDescription("The background color to skip when generating layers from incoming images connected to the Layers port."); diff --git a/Grinder/pipeline/blocks/CanvasBlock.h b/Grinder/pipeline/blocks/CanvasBlock.h index d479c9df89662be00fba07b4f0f15dbe7b77ad85..d31378436a79bb34f5c0bce0fdbc69293b395cdb 100644 --- a/Grinder/pipeline/blocks/CanvasBlock.h +++ b/Grinder/pipeline/blocks/CanvasBlock.h @@ -25,6 +25,8 @@ namespace grndr virtual std::unique_ptr<ProcessorBase> createProcessor() const override; public: + auto inLayersRenderable() { return dynamic_cast<BoolProperty*>(_inLayersRenderable.get()); } + auto inLayersRenderable() const { return dynamic_cast<const BoolProperty*>(_inLayersRenderable.get()); } auto colorKey() { return dynamic_cast<ColorProperty*>(_colorKey.get()); } auto colorKey() const { return dynamic_cast<const ColorProperty*>(_colorKey.get()); } auto tolerance() { return dynamic_cast<PercentProperty*>(_tolerance.get()); } @@ -52,6 +54,7 @@ namespace grndr virtual void createPorts() override; private: + std::shared_ptr<PropertyBase> _inLayersRenderable; std::shared_ptr<PropertyBase> _colorKey; std::shared_ptr<PropertyBase> _tolerance; std::shared_ptr<PropertyBase> _usePerceivedDistance; diff --git a/Grinder/pipeline/blocks/MaskInputBlock.cpp b/Grinder/pipeline/blocks/MaskInputBlock.cpp new file mode 100644 index 0000000000000000000000000000000000000000..e75d834bc65098ad23ddf23a39773f3072798a6e --- /dev/null +++ b/Grinder/pipeline/blocks/MaskInputBlock.cpp @@ -0,0 +1,46 @@ +/****************************************************************************** + * File: MaskInputBlock.cpp + * Date: 30.12.2019 + *****************************************************************************/ + +#include "Grinder.h" +#include "MaskInputBlock.h" +#include "engine/processors/MaskInputProcessor.h" + +const BlockType MaskInputBlock::type_value = BlockType::MaskInput; +const BlockCategory MaskInputBlock::category_value = BlockCategory::Input; + +MaskInputBlock::MaskInputBlock(Pipeline* pipeline, QString name) : Block(pipeline, type_value, category_value, name) +{ + _bypassPossible = false; +} + +std::unique_ptr<ProcessorBase> MaskInputBlock::createProcessor() const +{ + return std::make_unique<MaskInputProcessor>(this); +} + +void MaskInputBlock::createProperties() +{ + Block::createProperties(); + + setPropertyGroup("Mask"); + + QMap<QString, QString> placeholders; + placeholders["basename"] = "Base filename (w/o extension)"; + placeholders["ext"] = "Filename extension (w/o dot)"; + placeholders["filename"] = "Full filename (w/ extension)"; + + QStringList placeholdersList; + + for (auto placeholder : placeholders.keys()) + placeholdersList << QString{"<em>$%1:</em> %2"}.arg(placeholder).arg(placeholders[placeholder]); + + _maskFilename = createProperty<StringProperty>(PropertyID::Filename, "Filename", ""); + maskFilename()->setDescription(QString{"The filename pattern for the mask; the following placeholders are supported:<br><br>%1"}.arg(placeholdersList.join("<br>"))); +} + +void MaskInputBlock::createPorts() +{ + _outPort = createPort(PortType::ImageOut, Port::Direction::Out, {DataDescriptor::imageDescriptor()}, "Out"); +} diff --git a/Grinder/pipeline/blocks/MaskInputBlock.h b/Grinder/pipeline/blocks/MaskInputBlock.h new file mode 100644 index 0000000000000000000000000000000000000000..199e3671d292c19a67444487bb90381904e2d5fa --- /dev/null +++ b/Grinder/pipeline/blocks/MaskInputBlock.h @@ -0,0 +1,45 @@ +/****************************************************************************** + * File: MaskInputBlock.h + * Date: 30.12.2019 + *****************************************************************************/ + +#ifndef MASKINPUTBLOCK_H +#define MASKINPUTBLOCK_H + +#include "pipeline/Block.h" + +namespace grndr +{ + class MaskInputBlock : public Block + { + Q_OBJECT + + public: + static const BlockType type_value; + static const BlockCategory category_value; + + public: + MaskInputBlock(Pipeline* pipeline, QString name = ""); + + public: + virtual std::unique_ptr<ProcessorBase> createProcessor() const override; + + public: + auto maskFilename() { return dynamic_cast<StringProperty*>(_maskFilename.get()); } + auto maskFilename() const { return dynamic_cast<const StringProperty*>(_maskFilename.get()); } + + Port* outPort() { return _outPort.get(); } + const Port* outPort() const { return _outPort.get(); } + + protected: + virtual void createProperties() override; + virtual void createPorts() override; + + private: + std::shared_ptr<PropertyBase> _maskFilename; + + std::shared_ptr<Port> _outPort; + }; +} + +#endif diff --git a/Grinder/ui/image/ImageEditorWidget.cpp b/Grinder/ui/image/ImageEditorWidget.cpp index 48182d806a1548db1e9b0b1c89accd2dd21b9cfd..3b6599aefed9d40e545056c9e800603e8fda298a 100644 --- a/Grinder/ui/image/ImageEditorWidget.cpp +++ b/Grinder/ui/image/ImageEditorWidget.cpp @@ -232,7 +232,26 @@ void ImageEditorWidget::on_splitterLayersTags_splitterMoved(int pos, int index) void ImageEditorWidget::on_primaryColorWidget_customContextMenuRequested(const QPoint& pos) { - QMenu menu; + QMenu menu; + auto createTagAction = menu.addAction("&Create image tag", this, SLOT(createImageTagFromPrimaryColor())); + + // Only enable the create tag action if there is no such image tag yet + bool enableAction = false; + + if (auto imageBuild = _imageEditor->controller().activeImageBuild()) + { + if (auto imageTags = imageBuild->inputImageTags()) + { + if (!imageTags->tags().selectByColor(_imageEditor->environment().getPrimaryColor())) + enableAction = true; + } + } + + createTagAction->setEnabled(enableAction); + + menu.addSeparator(); + + // Create the add to presets menu auto presetsMenu = menu.addMenu("&Assign to preset"); for (unsigned int i = 0; i < ui->colorPresetsWidget->getPresetsCount(); ++i) @@ -321,6 +340,16 @@ void ImageEditorWidget::assignPrimaryColorToPreset() } } +void ImageEditorWidget::createImageTagFromPrimaryColor() +{ + QColor color = _imageEditor->environment().getPrimaryColor(); + bool ok = false; + QString tagName = QInputDialog::getText(this, "Generate image tag", "Image tag name:", QLineEdit::EchoMode::Normal, color.name(), &ok); + + if (ok && !tagName.isEmpty()) + _imageEditor->controller().createImageTag(tagName, color); +} + void ImageEditorWidget::undoCommand() const { _imageEditor->controller().undoStack().undo(); diff --git a/Grinder/ui/image/ImageEditorWidget.h b/Grinder/ui/image/ImageEditorWidget.h index 8f7754255a07269f2ff2c0acfcdb6a925841a608..bbf13fc15ec299b5086d9ea2f6904c6ed4e220ce 100644 --- a/Grinder/ui/image/ImageEditorWidget.h +++ b/Grinder/ui/image/ImageEditorWidget.h @@ -73,6 +73,7 @@ namespace grndr void primaryColorSelected(QColor color, bool byUser); void colorPresetSelected(QColor color); void assignPrimaryColorToPreset(); + void createImageTagFromPrimaryColor(); void undoCommand() const; diff --git a/Grinder/ui/image/LayersListWidget.cpp b/Grinder/ui/image/LayersListWidget.cpp index 1bf3c5ca4bea6c8004b186fe2a8c6c1a78b0b20d..ac81490aa3871033474c62c970be98cdb7ee530d 100644 --- a/Grinder/ui/image/LayersListWidget.cpp +++ b/Grinder/ui/image/LayersListWidget.cpp @@ -9,6 +9,7 @@ #include "ImageEditor.h" #include "ImageTagsListWidget.h" #include "image/ImageBuild.h" +#include "image/ImageTags.h" #include "core/GrinderApplication.h" #include "ui/widgets/ControlBar.h" #include "ui/widgets/SliderValueWidget.h" @@ -44,6 +45,7 @@ LayersListWidget::LayersListWidget(QWidget* parent) : MetaWidget(parent), _preventUpdatesAction = UIUtils::createAction(this, "Prevent updates", FILE_ICON_PREVENTUPDATES, SLOT(setLayerPreventUpdates()), "Prevent changes to the selected layer when executing the pipeline"); _preventUpdatesAction->setCheckable(true); _editLayerTagsAction = UIUtils::createAction(this, "Edit layer tags...", FILE_ICON_TAGS, SLOT(editLayerTags()), "Edit the tags assigned to the layer"); + _autoGenerateImageTagsAction = UIUtils::createAction(this, "Auto-generate image tags", "", SLOT(autoGenerateImageTags()), "Automatically generate image tags for all colors in the layer"); // Create the alpha slider action auto sliderAction = new QWidgetAction{this}; @@ -79,6 +81,7 @@ void LayersListWidget::assignUiComponents(ImageEditor* imageEditor) _imageEditor = imageEditor; // Listen for controller events in order to update the UI accordingly + connect(&imageEditor->controller(), &ImageEditorController::imageBuildSwitching, this, &LayersListWidget::imageBuildSwitching); connect(&imageEditor->controller(), &ImageEditorController::imageBuildSwitched, this, &LayersListWidget::imageBuildSwitched); connect(&imageEditor->controller(), &ImageEditorController::layerSwitching, this, &LayersListWidget::layerSwitching); connect(&imageEditor->controller(), &ImageEditorController::layerSwitched, this, &LayersListWidget::layerSwitched); @@ -143,6 +146,10 @@ std::vector<QAction*> LayersListWidget::getActions(MetaWidget::AddActionsMode mo actions.push_back(_editLayerAction); actions.push_back(_editLayerTagsAction); + + if (mode == AddActionsMode::ContextMenu) + actions.push_back(_autoGenerateImageTagsAction); + actions.push_back(nullptr); actions.push_back(_convertPixelsToItemsAction); actions.push_back(nullptr); @@ -362,10 +369,33 @@ void LayersListWidget::editLayerTags() } } +void LayersListWidget::autoGenerateImageTags() +{ + if (auto layer = currentObject()) + { + unsigned int count = 0; + _imageEditor->controller().autoGenerateImageTags(layer, &count); + + if (count > 0) + QMessageBox::information(this, "Image tags generation", QString{"%1 image tag(s) have been created."}.arg(count)); + else + QMessageBox::information(this, "Image tags generation", "No image tags have been created."); + } +} + +void LayersListWidget::imageBuildSwitching(ImageBuild* imageBuild) +{ + if (imageBuild) + disconnect(imageBuild, &ImageBuild::inputImageTagsChanged, this, &LayersListWidget::updateActions); +} + void LayersListWidget::imageBuildSwitched(ImageBuild* imageBuild) { // A new image build is shown in the editor, so populate its layers populateList(imageBuild->layers(), true, true); + + // Whenever the input image tags have changed, update the actions + connect(imageBuild, &ImageBuild::inputImageTagsChanged, this, &LayersListWidget::updateActions); } void LayersListWidget::layerCheckChanged(QListWidgetItem* item) const @@ -410,6 +440,7 @@ void LayersListWidget::updateActions() { auto currentLayer = currentObject(); bool layerSelected = (currentLayer != nullptr); + bool imageTagsAvailable = (currentLayer && currentLayer->imageBuild()->inputImageTags()); int removableLayersCount = 0; for (int i = 0; i < count(); ++i) @@ -431,6 +462,7 @@ void LayersListWidget::updateActions() _renderableAction->setEnabled(layerSelected); _preventUpdatesAction->setEnabled(layerSelected); _editLayerTagsAction->setEnabled(layerSelected); + _autoGenerateImageTagsAction->setEnabled(layerSelected && imageTagsAvailable); _removeAllLayersAction->setEnabled(removableLayersCount > 0); _sliderValueWidget->setValue(currentLayer ? currentLayer->getAlpha() : 0); diff --git a/Grinder/ui/image/LayersListWidget.h b/Grinder/ui/image/LayersListWidget.h index 4f4909b509814ff62cf58ba581a1041ff3882182..857f53dca6d6a2f3b3c2ee3be890d2e5400a03a1 100644 --- a/Grinder/ui/image/LayersListWidget.h +++ b/Grinder/ui/image/LayersListWidget.h @@ -72,7 +72,9 @@ namespace grndr void setLayerRenderable(); void setLayerPreventUpdates(); void editLayerTags(); + void autoGenerateImageTags(); + void imageBuildSwitching(ImageBuild* imageBuild); void imageBuildSwitched(ImageBuild* imageBuild); void layerCheckChanged(QListWidgetItem* item) const; @@ -108,6 +110,7 @@ namespace grndr QAction* _renderableAction{nullptr}; QAction* _preventUpdatesAction{nullptr}; QAction* _editLayerTagsAction{nullptr}; + QAction* _autoGenerateImageTagsAction{nullptr}; }; } diff --git a/Grinder/ui/mainwnd/ImageReferencesListWidget.cpp b/Grinder/ui/mainwnd/ImageReferencesListWidget.cpp index 28be1bb953e3cd8af2401a2af20e210ccb337566..7bc28975e19a790b767dc3c9be87f38bd13ed2b4 100644 --- a/Grinder/ui/mainwnd/ImageReferencesListWidget.cpp +++ b/Grinder/ui/mainwnd/ImageReferencesListWidget.cpp @@ -1,207 +1,232 @@ -/****************************************************************************** - * File: ImageReferencesListWidget.cpp - * Date: 10.2.2018 - *****************************************************************************/ - -#include "Grinder.h" -#include "ImageReferencesListWidget.h" -#include "core/GrinderApplication.h" -#include "project/Project.h" -#include "ui/widgets/ControlBar.h" -#include "ui/UIUtils.h" -#include "image/ImageUtils.h" -#include "util/FileUtils.h" -#include "res/Resources.h" - -ImageReferencesListWidget::ImageReferencesListWidget(QWidget* parent) : MetaWidget(parent) -{ - setAcceptDrops(true); - - // Create labels actions - _switchImageReferenceAction = UIUtils::createAction(this, "&Activate", FILE_ICON_ACTIVATE, SLOT(switchImageReference()), "Activate the selected image", "Return"); - _nextImageReferenceAction = UIUtils::createAction(this, "Switch to &next image", "", SLOT(nextImageReference()), "Activate the next image", "Ctrl+PgDown", Qt::WindowShortcut); - _previousImageReferenceAction = UIUtils::createAction(this, "Switch to &previous image", "", SLOT(previousImageReference()), "Activate the previous image", "Ctrl+PgUp", Qt::WindowShortcut); - _viewImageReferenceAction = UIUtils::createAction(this, "&View image", FILE_ICON_VIEWIMAGE, SLOT(viewImageReference()), "View the selected image"); - _addImageReferencesAction = UIUtils::createAction(this, "Add &images", FILE_ICON_ADD, SLOT(addImageReferences()), "Add one or more images to the project", "Ctrl+I", Qt::WindowShortcut); - _removeImageReferenceAction = UIUtils::createAction(this, "&Remove image", FILE_ICON_DELETE, SLOT(removeImageReference()), "Remove the selected image", "Del"); - _removeAllImageReferencesAction = UIUtils::createAction(this, "Remove all images", "", SLOT(removeAllImageReferences()), "Remove all images"); - - // Listen for item selections in order to update the actions - connect(this, &ImageReferencesListWidget::itemSelectionChanged, this, &ImageReferencesListWidget::updateActions); - - // Listen for label switching in order to update the active state flags - connect(&grinder()->projectController(), &ProjectController::imageReferenceSwitched, this, &ImageReferencesListWidget::imageReferenceSwitched); - - updateActions(); -} - -std::unique_ptr<QMenu> ImageReferencesListWidget::createContextMenu() const -{ - auto menu = widget_type::createContextMenu(); - menu->setDefaultAction(_switchImageReferenceAction); - return menu; -} - -std::vector<QAction*> ImageReferencesListWidget::getActions(AddActionsMode mode) const -{ - std::vector<QAction*> actions; - - if (mode == AddActionsMode::ContextMenu || mode == AddActionsMode::Toolbar) - { - actions.push_back(_switchImageReferenceAction); - - if (mode == AddActionsMode::ContextMenu) - actions.push_back(_viewImageReferenceAction); - - actions.push_back(nullptr); - } - - if (mode == AddActionsMode::MainMenu || mode == AddActionsMode::ContextMenu) - { - actions.push_back(_nextImageReferenceAction); - actions.push_back(_previousImageReferenceAction); - actions.push_back(nullptr); - } - - actions.push_back(_addImageReferencesAction); - actions.push_back(_removeImageReferenceAction); - - if (mode == AddActionsMode::ContextMenu) - { - actions.push_back(nullptr); - actions.push_back(_removeAllImageReferencesAction); - } - - return actions; -} - -void ImageReferencesListWidget::switchToObjectItem(ImageReferencesListItem* item, bool selectItem) -{ - if (item) - { - grinder()->projectController().switchImageReference(item->object()); - - if (selectItem) - setCurrentItem(item); - } - else - grinder()->projectController().switchImageReference(nullptr); -} - -void ImageReferencesListWidget::dragEnterEvent(QDragEnterEvent* event) -{ - if (event->mimeData()->hasUrls()) - { - event->setDropAction(Qt::CopyAction); - event->accept(); - } - else - event->ignore(); -} - -void ImageReferencesListWidget::dragMoveEvent(QDragMoveEvent* event) -{ - if (event->mimeData()->hasUrls()) - { - event->setDropAction(Qt::CopyAction); - event->accept(); - } - else - event->ignore(); -} - -void ImageReferencesListWidget::dropEvent(QDropEvent* event) -{ - if (event->mimeData()->hasUrls()) - { - auto urls = event->mimeData()->urls(); - QStringList files; - - for (auto url : urls) - files << url.toLocalFile(); - - addImageReferences(files); - - event->setDropAction(Qt::CopyAction); - event->accept(); - } - else - event->ignore(); -} - -void ImageReferencesListWidget::nextImageReference() const -{ - grinder()->projectController().switchToNextImageReference(); -} - -void ImageReferencesListWidget::previousImageReference() const -{ - grinder()->projectController().switchToPreviousImageReference(); -} - -void ImageReferencesListWidget::viewImageReference() -{ - if (auto item = currentObjectItem()) - item->viewImage(); -} - -void ImageReferencesListWidget::addImageReferences() -{ - auto files = UIUtils::askFileNames("ImageReferences", this, "Add images", QString{"Images (%1);;All files (*.*)"}.arg(ImageUtils::getSupportedFormatFilters().join(" "))); - addImageReferences(files); -} - -void ImageReferencesListWidget::addImageReferences(QStringList files) -{ - // Expand the file list, recursively finding all image files, and create image references for them - files = FileUtils::expandFileList(files, ImageUtils::getSupportedFormatFilters()); - auto imageRefs = grinder()->projectController().createImageReferences(files); - - // If no active image exists, switch to the last added one - if (!grinder()->projectController().activeImageReference()) - { - if (!imageRefs.empty()) - { - auto item = findObjectItem(imageRefs[imageRefs.size() - 1].get()); - - if (item) - switchToObjectItem(item); - } - } -} - -void ImageReferencesListWidget::removeImageReference() -{ - if (auto imageRef = currentObject()) - { - grinder()->projectController().removeImageReference(imageRef); - - // Switch to the image reference that is currently selected if no active one exists - if (!grinder()->projectController().activeImageReference()) - switchToObjectItem(currentObjectItem()); - } -} - -void ImageReferencesListWidget::removeAllImageReferences() -{ - grinder()->projectController().removeAllImageReferences(); - updateActions(); -} - -void ImageReferencesListWidget::imageReferenceSwitched(ImageReference* imageRef) -{ - objectSwitched(imageRef); - updateActions(); -} - -void ImageReferencesListWidget::updateActions() -{ - bool imageSelected = (currentObjectItem() != nullptr); - - _switchImageReferenceAction->setEnabled(imageSelected && !currentObjectItem()->isActive()); - _nextImageReferenceAction->setEnabled(count() > 1); - _previousImageReferenceAction->setEnabled(count() > 1); - _viewImageReferenceAction->setEnabled(imageSelected); - _removeImageReferenceAction->setEnabled(imageSelected); - _removeAllImageReferencesAction->setEnabled(count() > 0); -} +/****************************************************************************** + * File: ImageReferencesListWidget.cpp + * Date: 10.2.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "ImageReferencesListWidget.h" +#include "core/GrinderApplication.h" +#include "project/Project.h" +#include "ui/widgets/ControlBar.h" +#include "ui/UIUtils.h" +#include "image/ImageUtils.h" +#include "util/FileUtils.h" +#include "res/Resources.h" + +ImageReferencesListWidget::ImageReferencesListWidget(QWidget* parent) : MetaWidget(parent) +{ + setAcceptDrops(true); + + // Create labels actions + _switchImageReferenceAction = UIUtils::createAction(this, "&Activate", FILE_ICON_ACTIVATE, SLOT(switchImageReference()), "Activate the selected image", "Return"); + _nextImageReferenceAction = UIUtils::createAction(this, "Switch to &next image", "", SLOT(nextImageReference()), "Activate the next image", "Ctrl+PgDown", Qt::WindowShortcut); + _previousImageReferenceAction = UIUtils::createAction(this, "Switch to &previous image", "", SLOT(previousImageReference()), "Activate the previous image", "Ctrl+PgUp", Qt::WindowShortcut); + _viewImageReferenceAction = UIUtils::createAction(this, "&View image", FILE_ICON_VIEWIMAGE, SLOT(viewImageReference()), "View the selected image"); + _addImageReferencesAction = UIUtils::createAction(this, "Add &images", FILE_ICON_ADD, SLOT(addImageReferences()), "Add one or more images to the project", "Ctrl+I", Qt::WindowShortcut); + _removeImageReferenceAction = UIUtils::createAction(this, "&Remove image", FILE_ICON_DELETE, SLOT(removeImageReference()), "Remove the selected image", "Del"); + _removeAllImageReferencesAction = UIUtils::createAction(this, "Remove all images", "", SLOT(removeAllImageReferences()), "Remove all images"); + +#if defined(Q_OS_WIN) + _showInFileBrowserAction = UIUtils::createAction(this, "&Show in explorer", FILE_ICON_OPEN, SLOT(showInFileBrowser()), "Show the image file in Windows Explorer"); +#else + _showInFileBrowserAction = UIUtils::createAction(this, "&Show in finder", FILE_ICON_OPEN, SLOT(showInFileBrowser()), "Show the image file in Finder"); +#endif + + // Listen for item selections in order to update the actions + connect(this, &ImageReferencesListWidget::itemSelectionChanged, this, &ImageReferencesListWidget::updateActions); + + // Listen for label switching in order to update the active state flags + connect(&grinder()->projectController(), &ProjectController::imageReferenceSwitched, this, &ImageReferencesListWidget::imageReferenceSwitched); + + updateActions(); +} + +std::unique_ptr<QMenu> ImageReferencesListWidget::createContextMenu() const +{ + auto menu = widget_type::createContextMenu(); + menu->setDefaultAction(_switchImageReferenceAction); + return menu; +} + +std::vector<QAction*> ImageReferencesListWidget::getActions(AddActionsMode mode) const +{ + std::vector<QAction*> actions; + + if (mode == AddActionsMode::ContextMenu || mode == AddActionsMode::Toolbar) + { + actions.push_back(_switchImageReferenceAction); + + if (mode == AddActionsMode::ContextMenu) + { + actions.push_back(_viewImageReferenceAction); + +#if defined(Q_OS_WIN) || defined(Q_OS_MAC) + actions.push_back(nullptr); + actions.push_back(_showInFileBrowserAction); +#endif + } + + actions.push_back(nullptr); + } + + if (mode == AddActionsMode::MainMenu || mode == AddActionsMode::ContextMenu) + { + actions.push_back(_nextImageReferenceAction); + actions.push_back(_previousImageReferenceAction); + actions.push_back(nullptr); + } + + actions.push_back(_addImageReferencesAction); + actions.push_back(_removeImageReferenceAction); + + if (mode == AddActionsMode::ContextMenu) + { + actions.push_back(nullptr); + actions.push_back(_removeAllImageReferencesAction); + } + + return actions; +} + +void ImageReferencesListWidget::switchToObjectItem(ImageReferencesListItem* item, bool selectItem) +{ + if (item) + { + grinder()->projectController().switchImageReference(item->object()); + + if (selectItem) + setCurrentItem(item); + } + else + grinder()->projectController().switchImageReference(nullptr); +} + +void ImageReferencesListWidget::dragEnterEvent(QDragEnterEvent* event) +{ + if (event->mimeData()->hasUrls()) + { + event->setDropAction(Qt::CopyAction); + event->accept(); + } + else + event->ignore(); +} + +void ImageReferencesListWidget::dragMoveEvent(QDragMoveEvent* event) +{ + if (event->mimeData()->hasUrls()) + { + event->setDropAction(Qt::CopyAction); + event->accept(); + } + else + event->ignore(); +} + +void ImageReferencesListWidget::dropEvent(QDropEvent* event) +{ + if (event->mimeData()->hasUrls()) + { + auto urls = event->mimeData()->urls(); + QStringList files; + + for (auto url : urls) + files << url.toLocalFile(); + + addImageReferences(files); + + event->setDropAction(Qt::CopyAction); + event->accept(); + } + else + event->ignore(); +} + +void ImageReferencesListWidget::nextImageReference() const +{ + grinder()->projectController().switchToNextImageReference(); +} + +void ImageReferencesListWidget::previousImageReference() const +{ + grinder()->projectController().switchToPreviousImageReference(); +} + +void ImageReferencesListWidget::viewImageReference() +{ + if (auto item = currentObjectItem()) + item->viewImage(); +} + +void ImageReferencesListWidget::addImageReferences() +{ + auto files = UIUtils::askFileNames("ImageReferences", this, "Add images", QString{"Images (%1);;All files (*.*)"}.arg(ImageUtils::getSupportedFormatFilters().join(" "))); + addImageReferences(files); +} + +void ImageReferencesListWidget::addImageReferences(QStringList files) +{ + // Expand the file list, recursively finding all image files, and create image references for them + files = FileUtils::expandFileList(files, ImageUtils::getSupportedFormatFilters()); + auto imageRefs = grinder()->projectController().createImageReferences(files); + + // If no active image exists, switch to the last added one + if (!grinder()->projectController().activeImageReference()) + { + if (!imageRefs.empty()) + { + auto item = findObjectItem(imageRefs[imageRefs.size() - 1].get()); + + if (item) + switchToObjectItem(item); + } + } +} + +void ImageReferencesListWidget::removeImageReference() +{ + if (auto imageRef = currentObject()) + { + grinder()->projectController().removeImageReference(imageRef); + + // Switch to the image reference that is currently selected if no active one exists + if (!grinder()->projectController().activeImageReference()) + switchToObjectItem(currentObjectItem()); + } +} + +void ImageReferencesListWidget::removeAllImageReferences() +{ + grinder()->projectController().removeAllImageReferences(); + updateActions(); +} + +void ImageReferencesListWidget::showInFileBrowser() +{ + if (auto imageRef = currentObject()) + FileUtils::openInFileBrowser(imageRef->getImageFilePath()); +} + +void ImageReferencesListWidget::imageReferenceSwitched(ImageReference* imageRef) +{ + objectSwitched(imageRef); + updateActions(); +} + +void ImageReferencesListWidget::updateActions() +{ + bool imageSelected = (currentObjectItem() != nullptr); + + _switchImageReferenceAction->setEnabled(imageSelected && !currentObjectItem()->isActive()); + _nextImageReferenceAction->setEnabled(count() > 1); + _previousImageReferenceAction->setEnabled(count() > 1); + _viewImageReferenceAction->setEnabled(imageSelected); + _removeImageReferenceAction->setEnabled(imageSelected); + _removeAllImageReferencesAction->setEnabled(count() > 0); + +#if defined(Q_OS_WIN) || defined(Q_OS_MAC) + _showInFileBrowserAction->setEnabled(imageSelected); +#else + _showInFileBrowserAction->setEnabled(false); +#endif +} diff --git a/Grinder/ui/mainwnd/ImageReferencesListWidget.h b/Grinder/ui/mainwnd/ImageReferencesListWidget.h index 3c31592925c937ec00cfd8e73276e6e34d962c3a..fc433da534ac4502bfd1dfd015207897bfe3fd0b 100644 --- a/Grinder/ui/mainwnd/ImageReferencesListWidget.h +++ b/Grinder/ui/mainwnd/ImageReferencesListWidget.h @@ -1,65 +1,67 @@ -/****************************************************************************** - * File: ImageReferencesListWidget.h - * Date: 10.2.2018 - *****************************************************************************/ - -#ifndef IMAGEREFERENCESLISTWIDGET_H -#define IMAGEREFERENCESLISTWIDGET_H - -#include "ui/widgets/MetaWidget.h" -#include "ui/widgets/ActiveObjectListWidget.h" -#include "ImageReferencesListItem.h" - -namespace grndr -{ - class ControlBar; - class ImageReference; - - using ImageReferenceObjectListWidget = ActiveObjectListWidget<ImageReference, ImageReferencesListItem>; - - class ImageReferencesListWidget : public MetaWidget<ImageReferenceObjectListWidget> - { - Q_OBJECT - - public: - ImageReferencesListWidget(QWidget* parent = nullptr); - - protected: - virtual std::unique_ptr<QMenu> createContextMenu() const override; - - virtual std::vector<QAction*> getActions(AddActionsMode mode) const override; - - protected: - virtual void switchToObjectItem(ImageReferencesListItem* item, bool selectItem = true) override; - - protected: - virtual void dragEnterEvent(QDragEnterEvent* event) override; - virtual void dragMoveEvent(QDragMoveEvent* event) override; - virtual void dropEvent(QDropEvent* event) override; - - private slots: - void switchImageReference() { switchToObjectItem(currentObjectItem()); } - void nextImageReference() const; - void previousImageReference() const; - void viewImageReference(); - void addImageReferences(); - void addImageReferences(QStringList files); - void removeImageReference(); - void removeAllImageReferences(); - - void imageReferenceSwitched(ImageReference* imageRef); - - virtual void updateActions() override; - - private: - QAction* _switchImageReferenceAction{nullptr}; - QAction* _nextImageReferenceAction{nullptr}; - QAction* _previousImageReferenceAction{nullptr}; - QAction* _viewImageReferenceAction{nullptr}; - QAction* _addImageReferencesAction{nullptr}; - QAction* _removeImageReferenceAction{nullptr}; - QAction* _removeAllImageReferencesAction{nullptr}; - }; -} - -#endif +/****************************************************************************** + * File: ImageReferencesListWidget.h + * Date: 10.2.2018 + *****************************************************************************/ + +#ifndef IMAGEREFERENCESLISTWIDGET_H +#define IMAGEREFERENCESLISTWIDGET_H + +#include "ui/widgets/MetaWidget.h" +#include "ui/widgets/ActiveObjectListWidget.h" +#include "ImageReferencesListItem.h" + +namespace grndr +{ + class ControlBar; + class ImageReference; + + using ImageReferenceObjectListWidget = ActiveObjectListWidget<ImageReference, ImageReferencesListItem>; + + class ImageReferencesListWidget : public MetaWidget<ImageReferenceObjectListWidget> + { + Q_OBJECT + + public: + ImageReferencesListWidget(QWidget* parent = nullptr); + + protected: + virtual std::unique_ptr<QMenu> createContextMenu() const override; + + virtual std::vector<QAction*> getActions(AddActionsMode mode) const override; + + protected: + virtual void switchToObjectItem(ImageReferencesListItem* item, bool selectItem = true) override; + + protected: + virtual void dragEnterEvent(QDragEnterEvent* event) override; + virtual void dragMoveEvent(QDragMoveEvent* event) override; + virtual void dropEvent(QDropEvent* event) override; + + private slots: + void switchImageReference() { switchToObjectItem(currentObjectItem()); } + void nextImageReference() const; + void previousImageReference() const; + void viewImageReference(); + void addImageReferences(); + void addImageReferences(QStringList files); + void removeImageReference(); + void removeAllImageReferences(); + void showInFileBrowser(); + + void imageReferenceSwitched(ImageReference* imageRef); + + virtual void updateActions() override; + + private: + QAction* _switchImageReferenceAction{nullptr}; + QAction* _nextImageReferenceAction{nullptr}; + QAction* _previousImageReferenceAction{nullptr}; + QAction* _viewImageReferenceAction{nullptr}; + QAction* _showInFileBrowserAction{nullptr}; + QAction* _addImageReferencesAction{nullptr}; + QAction* _removeImageReferenceAction{nullptr}; + QAction* _removeAllImageReferencesAction{nullptr}; + }; +} + +#endif diff --git a/Grinder/ui/widgets/BaseListWidget.cpp b/Grinder/ui/widgets/BaseListWidget.cpp index 8c762b5ebe79e82c831b8764fe9215a7bfba1f48..54a719c9beddd2a8d78b39b156391c7b3b0c11ce 100644 --- a/Grinder/ui/widgets/BaseListWidget.cpp +++ b/Grinder/ui/widgets/BaseListWidget.cpp @@ -1,66 +1,86 @@ -/****************************************************************************** - * File: BaseListWidget.cpp - * Date: 26.4.2018 - *****************************************************************************/ - -#include "Grinder.h" -#include "BaseListWidget.h" -#include "BaseListItem.h" - -void BaseListWidget::invertSelection() -{ - for (int i = 0; i < count(); ++i) - item(i)->setSelected(!item(i)->isSelected()); -} - -void BaseListWidget::toggleCheckSelectedItems() -{ - auto selItems = selectedItems(); - - if (!selItems.isEmpty()) - { - bool checkItems = (selItems[0]->checkState() != Qt::Checked); - - for (auto item : selItems) - item->setCheckState(checkItems ? Qt::Checked : Qt::Unchecked); - } -} - -void BaseListWidget::checkAllItems() -{ - for (int i = 0; i < count(); ++i) - item(i)->setCheckState(Qt::Checked); -} - -void BaseListWidget::uncheckAllItems() -{ - for (int i = 0; i < count(); ++i) - item(i)->setCheckState(Qt::Unchecked); -} - -void BaseListWidget::mousePressEvent(QMouseEvent* event) -{ - QListWidget::mousePressEvent(event); - - // Check if any knob was clicked - if (event->button() == Qt::LeftButton && event->modifiers() == 0) - { - if (auto itemSelected = dynamic_cast<BaseListItem*>(item(currentRow()))) - { - auto itemRect = visualItemRect(itemSelected); - - if (itemRect.contains(event->pos())) - { - int knobIndex = itemSelected->hitTestKnob(event->pos() - itemRect.topLeft()); - - if (knobIndex != -1) - knobPressed(static_cast<unsigned int>(knobIndex)); - } - } - } -} - -BaseListItem* BaseListWidget::listItem(const QModelIndex& index) const -{ - return dynamic_cast<BaseListItem*>(itemFromIndex(index)); -} +/****************************************************************************** + * File: BaseListWidget.cpp + * Date: 26.4.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "BaseListWidget.h" +#include "BaseListItem.h" +#include "util/MathUtils.h" + +BaseListItem* BaseListWidget::listItem(const QModelIndex& index) const +{ + return dynamic_cast<BaseListItem*>(itemFromIndex(index)); +} + +void BaseListWidget::invertSelection() +{ + for (int i = 0; i < count(); ++i) + item(i)->setSelected(!item(i)->isSelected()); +} + +void BaseListWidget::toggleCheckSelectedItems() +{ + auto selItems = selectedItems(); + + if (!selItems.isEmpty()) + { + bool checkItems = (selItems[0]->checkState() != Qt::Checked); + + for (auto item : selItems) + item->setCheckState(checkItems ? Qt::Checked : Qt::Unchecked); + } +} + +void BaseListWidget::checkAllItems() +{ + for (int i = 0; i < count(); ++i) + item(i)->setCheckState(Qt::Checked); +} + +void BaseListWidget::uncheckAllItems() +{ + for (int i = 0; i < count(); ++i) + item(i)->setCheckState(Qt::Unchecked); +} + +void BaseListWidget::checkRandomizedItems() +{ + bool ok = false; + int itemsToCheck = QInputDialog::getInt(this, "Item count", "Number of items to check:", 1, 1, count(), 1, &ok); + + if (ok && itemsToCheck > 0) + { + auto numbersToCheck = MathUtils::uniqueRandomNumbers(itemsToCheck, 0, count() - 1); + + for (int i = 0; i < count(); ++i) + { + if (std::find(numbersToCheck.cbegin(), numbersToCheck.cend(), i) != numbersToCheck.cend()) + item(i)->setCheckState(Qt::Checked); + else + item(i)->setCheckState(Qt::Unchecked); + } + } +} + +void BaseListWidget::mousePressEvent(QMouseEvent* event) +{ + QListWidget::mousePressEvent(event); + + // Check if any knob was clicked + if (event->button() == Qt::LeftButton && event->modifiers() == 0) + { + if (auto itemSelected = dynamic_cast<BaseListItem*>(item(currentRow()))) + { + auto itemRect = visualItemRect(itemSelected); + + if (itemRect.contains(event->pos())) + { + int knobIndex = itemSelected->hitTestKnob(event->pos() - itemRect.topLeft()); + + if (knobIndex != -1) + knobPressed(static_cast<unsigned int>(knobIndex)); + } + } + } +} diff --git a/Grinder/ui/widgets/BaseListWidget.h b/Grinder/ui/widgets/BaseListWidget.h index 1c765c7cc54f3783058409b52cd3d8635e9d4090..10d6e3fb94088430ed3d51c23337c9058bb8cdbf 100644 --- a/Grinder/ui/widgets/BaseListWidget.h +++ b/Grinder/ui/widgets/BaseListWidget.h @@ -1,40 +1,41 @@ -/****************************************************************************** - * File: BaseListWidget.h - * Date: 26.4.2018 - *****************************************************************************/ - -#ifndef BASELISTWIDGET_H -#define BASELISTWIDGET_H - -#include <QListWidget> - -namespace grndr -{ - class BaseListItem; - - class BaseListWidget : public QListWidget - { - Q_OBJECT - - public: - using QListWidget::QListWidget; - - public: - BaseListItem* listItem(const QModelIndex& index) const; - - public slots: - void invertSelection(); - - void toggleCheckSelectedItems(); - void checkAllItems(); - void uncheckAllItems(); - - protected: - virtual void mousePressEvent(QMouseEvent* event) override; - - protected: - virtual void knobPressed(unsigned int knobIndex) { Q_UNUSED(knobIndex); } - }; -} - -#endif +/****************************************************************************** + * File: BaseListWidget.h + * Date: 26.4.2018 + *****************************************************************************/ + +#ifndef BASELISTWIDGET_H +#define BASELISTWIDGET_H + +#include <QListWidget> + +namespace grndr +{ + class BaseListItem; + + class BaseListWidget : public QListWidget + { + Q_OBJECT + + public: + using QListWidget::QListWidget; + + public: + BaseListItem* listItem(const QModelIndex& index) const; + + public slots: + void invertSelection(); + + void toggleCheckSelectedItems(); + void checkAllItems(); + void uncheckAllItems(); + void checkRandomizedItems(); + + protected: + virtual void mousePressEvent(QMouseEvent* event) override; + + protected: + virtual void knobPressed(unsigned int knobIndex) { Q_UNUSED(knobIndex); } + }; +} + +#endif diff --git a/Grinder/ui/widgets/CheckListWidget.h b/Grinder/ui/widgets/CheckListWidget.h index 08f55268b96681cc8076d481983e51a898ce8a22..658d8aa7f141f08885ee7cb12a864a021880041f 100644 --- a/Grinder/ui/widgets/CheckListWidget.h +++ b/Grinder/ui/widgets/CheckListWidget.h @@ -1,47 +1,48 @@ -/****************************************************************************** - * File: CheckListWidget.h - * Date: 25.4.2018 - *****************************************************************************/ - -#ifndef CHECKLISTWIDGET_H -#define CHECKLISTWIDGET_H - -#include "ObjectListWidget.h" - -namespace grndr -{ - template<typename ObjectType, typename ItemType> - class CheckListWidget : public ObjectListWidget<ObjectType, ItemType> - { - public: - using object_type = ObjectType; - using item_type = ItemType; - - public: - CheckListWidget(QWidget* parent = nullptr); - - template<typename ContainerType> - void populateList(const ContainerType& container); - - template<typename ObjectReturnType = ObjectType> - std::vector<ObjectReturnType*> getCheckedObjects() const; - template<typename ObjectReturnType = ObjectType> - void setCheckedObjects(const std::vector<ObjectReturnType*>& checkedObjects); - - protected: - virtual void contextMenuEvent(QContextMenuEvent* event) override; - - private: - QAction* _unCheckSelectedItems{nullptr}; - QAction* _checkAllItems{nullptr}; - QAction* _uncheckAllItems{nullptr}; - - QAction* _selectAllAction{nullptr}; - QAction* _deselectAllAction{nullptr}; - QAction* _invertSelectionAction{nullptr}; - }; -} - -#include "CheckListWidget.impl.h" - -#endif +/****************************************************************************** + * File: CheckListWidget.h + * Date: 25.4.2018 + *****************************************************************************/ + +#ifndef CHECKLISTWIDGET_H +#define CHECKLISTWIDGET_H + +#include "ObjectListWidget.h" + +namespace grndr +{ + template<typename ObjectType, typename ItemType> + class CheckListWidget : public ObjectListWidget<ObjectType, ItemType> + { + public: + using object_type = ObjectType; + using item_type = ItemType; + + public: + CheckListWidget(QWidget* parent = nullptr); + + template<typename ContainerType> + void populateList(const ContainerType& container); + + template<typename ObjectReturnType = ObjectType> + std::vector<ObjectReturnType*> getCheckedObjects() const; + template<typename ObjectReturnType = ObjectType> + void setCheckedObjects(const std::vector<ObjectReturnType*>& checkedObjects); + + protected: + virtual void contextMenuEvent(QContextMenuEvent* event) override; + + private: + QAction* _unCheckSelectedItems{nullptr}; + QAction* _checkAllItems{nullptr}; + QAction* _uncheckAllItems{nullptr}; + QAction* _checkRandomizedItems{nullptr}; + + QAction* _selectAllAction{nullptr}; + QAction* _deselectAllAction{nullptr}; + QAction* _invertSelectionAction{nullptr}; + }; +} + +#include "CheckListWidget.impl.h" + +#endif diff --git a/Grinder/ui/widgets/CheckListWidget.impl.h b/Grinder/ui/widgets/CheckListWidget.impl.h index ab4b3632d4865925d4bee4b987d09dd476124bd1..ff78e7e92b7727b44fbb1130d7f269d82e415e38 100644 --- a/Grinder/ui/widgets/CheckListWidget.impl.h +++ b/Grinder/ui/widgets/CheckListWidget.impl.h @@ -1,107 +1,110 @@ -/****************************************************************************** - * File: CheckListWidget.impl.h - * Date: 25.4.2018 - *****************************************************************************/ - -#include "Grinder.h" -#include "CheckListWidget.h" -#include "ui/UIUtils.h" -#include "res/Resources.h" - -template<typename ObjectType, typename ItemType> -CheckListWidget<ObjectType, ItemType>::CheckListWidget(QWidget* parent) : ObjectListWidget<ObjectType, ItemType>(parent) -{ - _unCheckSelectedItems = UIUtils::createAction(this, "&Check selected", FILE_ICON_CHECK, SLOT(toggleCheckSelectedItems()), "Check the selected items", "Space"); - _checkAllItems = UIUtils::createAction(this, "Check &all", FILE_ICON_CHECKALL, SLOT(checkAllItems()), "Check all items", "Ctrl+Shift+A"); - _uncheckAllItems = UIUtils::createAction(this, "Unchec&k all", "", SLOT(uncheckAllItems()), "Uncheck all items"); - - _selectAllAction = UIUtils::createAction(this, "&Select all", FILE_ICON_SELECTALL, SLOT(selectAll()), "Select all items", "Ctrl+A"); - _deselectAllAction = UIUtils::createAction(this, "&Deselect all", "", SLOT(clearSelection()), "Deselect all items"); - _invertSelectionAction = UIUtils::createAction(this, "&Invert selection", "", SLOT(invertSelection()), "Invert the current selection", "Ctrl+I"); -} - -template<typename ObjectType, typename ItemType> -template<typename ContainerType> -void CheckListWidget<ObjectType, ItemType>::populateList(const ContainerType& container) -{ - ObjectListWidget<ObjectType, ItemType>::populateList(container); - - for (int i = 0; i < this->count(); ++i) - { - if (auto item = this->item(i)) - { - item->setFlags(item->flags()|Qt::ItemIsUserCheckable); - item->setCheckState(Qt::Unchecked); - } - } -} - -template<typename ObjectType, typename ItemType> -template<typename ObjectReturnType> -std::vector<ObjectReturnType*> CheckListWidget<ObjectType, ItemType>::getCheckedObjects() const -{ - std::vector<ObjectReturnType*> checkedItems; - - for (int i = 0; i < this->count(); ++i) - { - if (auto item = dynamic_cast<ItemType*>(this->item(i))) - { - if (item->checkState() == Qt::Checked) - checkedItems.push_back(static_cast<ObjectReturnType*>(item->object())); - } - } - - return checkedItems; -} - -template<typename ObjectType, typename ItemType> -template<typename ObjectReturnType> -void CheckListWidget<ObjectType, ItemType>::setCheckedObjects(const std::vector<ObjectReturnType*>& checkedObjects) -{ - for (int i = 0; i < this->count(); ++i) - { - if (auto item = dynamic_cast<ItemType*>(this->item(i))) - { - if (std::find(checkedObjects.cbegin(), checkedObjects.cend(), static_cast<ObjectReturnType*>(item->object())) != checkedObjects.cend()) - item->setCheckState(Qt::Checked); - else - item->setCheckState(Qt::Unchecked); - } - } -} - -template<typename ObjectType, typename ItemType> -void CheckListWidget<ObjectType, ItemType>::contextMenuEvent(QContextMenuEvent* event) -{ - // Update the text of the check/uncheck selected action - auto selItems = this->selectedItems(); - - if (!selItems.isEmpty()) - { - bool checkItems = (selItems[0]->checkState() != Qt::Checked); - - if (checkItems) - { - _unCheckSelectedItems->setText("&Check selected"); - _unCheckSelectedItems->setStatusTip("Check the selected items"); - } - else - { - _unCheckSelectedItems->setText("&Uncheck selected"); - _unCheckSelectedItems->setStatusTip("Uncheck the selected items"); - } - } - - QMenu menu; - - menu.addAction(_unCheckSelectedItems); - menu.addSeparator(); - menu.addAction(_checkAllItems); - menu.addAction(_uncheckAllItems); - menu.addSeparator(); - menu.addAction(_selectAllAction); - menu.addAction(_deselectAllAction); - menu.addAction(_invertSelectionAction); - - menu.exec(event->globalPos()); -} +/****************************************************************************** + * File: CheckListWidget.impl.h + * Date: 25.4.2018 + *****************************************************************************/ + +#include "Grinder.h" +#include "CheckListWidget.h" +#include "ui/UIUtils.h" +#include "res/Resources.h" + +template<typename ObjectType, typename ItemType> +CheckListWidget<ObjectType, ItemType>::CheckListWidget(QWidget* parent) : ObjectListWidget<ObjectType, ItemType>(parent) +{ + _unCheckSelectedItems = UIUtils::createAction(this, "&Check selected", FILE_ICON_CHECK, SLOT(toggleCheckSelectedItems()), "Check the selected items", "Space"); + _checkAllItems = UIUtils::createAction(this, "Check &all", FILE_ICON_CHECKALL, SLOT(checkAllItems()), "Check all items", "Ctrl+Shift+A"); + _uncheckAllItems = UIUtils::createAction(this, "Unchec&k all", "", SLOT(uncheckAllItems()), "Uncheck all items"); + _checkRandomizedItems = UIUtils::createAction(this, "Check &randomized...", "", SLOT(checkRandomizedItems()), "Check a randomized selection of items"); + + _selectAllAction = UIUtils::createAction(this, "&Select all", FILE_ICON_SELECTALL, SLOT(selectAll()), "Select all items", "Ctrl+A"); + _deselectAllAction = UIUtils::createAction(this, "&Deselect all", "", SLOT(clearSelection()), "Deselect all items"); + _invertSelectionAction = UIUtils::createAction(this, "&Invert selection", "", SLOT(invertSelection()), "Invert the current selection", "Ctrl+I"); +} + +template<typename ObjectType, typename ItemType> +template<typename ContainerType> +void CheckListWidget<ObjectType, ItemType>::populateList(const ContainerType& container) +{ + ObjectListWidget<ObjectType, ItemType>::populateList(container); + + for (int i = 0; i < this->count(); ++i) + { + if (auto item = this->item(i)) + { + item->setFlags(item->flags()|Qt::ItemIsUserCheckable); + item->setCheckState(Qt::Unchecked); + } + } +} + +template<typename ObjectType, typename ItemType> +template<typename ObjectReturnType> +std::vector<ObjectReturnType*> CheckListWidget<ObjectType, ItemType>::getCheckedObjects() const +{ + std::vector<ObjectReturnType*> checkedItems; + + for (int i = 0; i < this->count(); ++i) + { + if (auto item = dynamic_cast<ItemType*>(this->item(i))) + { + if (item->checkState() == Qt::Checked) + checkedItems.push_back(static_cast<ObjectReturnType*>(item->object())); + } + } + + return checkedItems; +} + +template<typename ObjectType, typename ItemType> +template<typename ObjectReturnType> +void CheckListWidget<ObjectType, ItemType>::setCheckedObjects(const std::vector<ObjectReturnType*>& checkedObjects) +{ + for (int i = 0; i < this->count(); ++i) + { + if (auto item = dynamic_cast<ItemType*>(this->item(i))) + { + if (std::find(checkedObjects.cbegin(), checkedObjects.cend(), static_cast<ObjectReturnType*>(item->object())) != checkedObjects.cend()) + item->setCheckState(Qt::Checked); + else + item->setCheckState(Qt::Unchecked); + } + } +} + +template<typename ObjectType, typename ItemType> +void CheckListWidget<ObjectType, ItemType>::contextMenuEvent(QContextMenuEvent* event) +{ + // Update the text of the check/uncheck selected action + auto selItems = this->selectedItems(); + + if (!selItems.isEmpty()) + { + bool checkItems = (selItems[0]->checkState() != Qt::Checked); + + if (checkItems) + { + _unCheckSelectedItems->setText("&Check selected"); + _unCheckSelectedItems->setStatusTip("Check the selected items"); + } + else + { + _unCheckSelectedItems->setText("&Uncheck selected"); + _unCheckSelectedItems->setStatusTip("Uncheck the selected items"); + } + } + + QMenu menu; + + menu.addAction(_unCheckSelectedItems); + menu.addSeparator(); + menu.addAction(_checkAllItems); + menu.addAction(_uncheckAllItems); + menu.addSeparator(); + menu.addAction(_checkRandomizedItems); + menu.addSeparator(); + menu.addAction(_selectAllAction); + menu.addAction(_deselectAllAction); + menu.addAction(_invertSelectionAction); + + menu.exec(event->globalPos()); +} diff --git a/Grinder/util/FileUtils.cpp b/Grinder/util/FileUtils.cpp index 4a3e3702d2290bc8d48ccfa7a62f086d2e3a5e39..410c54a19631da43af3c004d1cd6c9ede98a5d2a 100644 --- a/Grinder/util/FileUtils.cpp +++ b/Grinder/util/FileUtils.cpp @@ -71,3 +71,28 @@ QString FileUtils::getTemporaryFileName(QString filename) dir.mkpath(dir.path()); return dir.filePath(filename); } + +void FileUtils::openInFileBrowser(QString filename) +{ + QStringList args; + + if (QFile::exists(filename)) + { +#if defined(Q_OS_WIN) + args << "/select," << QDir::toNativeSeparators(filename); + + QProcess::startDetached("explorer", args); +#elif defined(Q_OS_MAC) + args << "-e"; + args << "tell application \"Finder\""; + args << "-e"; + args << "activate"; + args << "-e"; + args << "select POSIX file \"" + filename + "\""; + args << "-e"; + args << "end tell"; + + QProcess::startDetached("osascript", args); +#endif + } +} diff --git a/Grinder/util/FileUtils.h b/Grinder/util/FileUtils.h index 9898e5269fda75839c859357c5a05b0b4961a216..b248a4e42ac9674cc1f7fbf8c6941c4a176b0383 100644 --- a/Grinder/util/FileUtils.h +++ b/Grinder/util/FileUtils.h @@ -20,6 +20,8 @@ namespace grndr static QString getTemporaryFileName(QString filename); + static void openInFileBrowser(QString filename); + private: FileUtils(); }; diff --git a/Grinder/util/MathUtils.h b/Grinder/util/MathUtils.h index f637a67995fd134e74ceb14bf67bbf75166a0c47..244d0da32d6126a5ab68146a24910a4aac6a7b0f 100644 --- a/Grinder/util/MathUtils.h +++ b/Grinder/util/MathUtils.h @@ -6,6 +6,8 @@ #ifndef MATHUTILS_H #define MATHUTILS_H +#include <vector> + #include <QPoint> #include <QLine> @@ -25,9 +27,14 @@ namespace grndr static QPoint ceil(QPointF pos); static QPoint floor(QPointF pos); + template<typename T> + static std::vector<T> uniqueRandomNumbers(unsigned int count, T min, T max); + private: MathUtils() { } }; } +#include "MathUtils.impl.h" + #endif diff --git a/Grinder/util/MathUtils.impl.h b/Grinder/util/MathUtils.impl.h new file mode 100644 index 0000000000000000000000000000000000000000..b4fe9cb3b1f4e79d643d91142f7166fcea4e9fe2 --- /dev/null +++ b/Grinder/util/MathUtils.impl.h @@ -0,0 +1,26 @@ +/****************************************************************************** + * File: MathUtils.impl.h + * Date: 30.12.2019 + *****************************************************************************/ + +#include "Grinder.h" +#include "MathUtils.h" + +template<typename T> +std::vector<T> MathUtils::uniqueRandomNumbers(unsigned int count, T min, T max) +{ + std::vector<T> numbers; + + // Add all numbers from min to max to an array and shuffle it + for (T i = min; i <= max; ++i) + numbers.push_back(i); + + unsigned seed = std::chrono::system_clock::now().time_since_epoch().count(); + std::shuffle(numbers.begin(), numbers.end(), std::default_random_engine{seed}); + + if (count > numbers.size()) + count = numbers.size(); + + // Return the first count items of the shuffled array + return std::vector<T>{numbers.begin(), numbers.begin() + count}; +}