This example demonstrates how to create a custom widget as a plugin in MT Showcase. It adds a grid of flip widgets containing images to Showcase.
Screenshot of the FlipGrid widget.
Create a custom version of Cornerstone's FlippingWidget.
#pragma once
#include <MultiWidgets/FlippingWidget.hpp>
namespace ShowcaseExamples
{
class CustomFlipWidget : public MultiWidgets::FlippingWidget
{
public:
CustomFlipWidget(MultiWidgets::WidgetPtr parent, QString contentPath, bool movable);
void flip();
};
}
We give the CustomFlipWidget a folder path in the constructor. It searches the folder for all .png and .jpg images, and creates a MultiWidgets::ImageWidget for each and uses MultiWidgets::FlippingWidget::addItem to add them to its contents.
We customize the behavior of the flip widget depending on if it's movable. A movable flip widget gets a CSS class to apply additional CSS rules, closes after it's been idle for a while, and its scale is limited. It flips when it's first created, and flips again when a user taps it.
Flip widget in grid is not movable. It flips on its own in the grid at regular intervals, but the interval and initial timer is randomized so each flip widget flips on its own pace for a playful effect. It creates a new movable flip widget when it's tapped.
#include "CustomFlipWidget.hpp"
#include <Showcase.hpp>
#include <MultiWidgets/AfterIdleOperator.hpp>
#include <MultiWidgets/FadeAnimator.hpp>
#include <MultiWidgets/ImageWidget.hpp>
#include <MultiWidgets/LimitScaleOperator.hpp>
#include <MultiWidgets/TimerOperator.hpp>
#include <Nimble/Random.hpp>
#include <QDir>
namespace ShowcaseExamples
{
CustomFlipWidget::CustomFlipWidget(MultiWidgets::WidgetPtr parent, QString contentPath, bool movable)
: FlippingWidget(parent)
{
addCSSClass("flip-grid");
QDir contentDir(contentPath);
QStringList nameFilters = QStringList() << "*.jpg" << "*.png";
QDir::Filters filters = QDir::Files | QDir::Readable;
QDir::SortFlags orderBy = QDir::Name | QDir::LocaleAware;
QFileInfoList entries = contentDir.entryInfoList(nameFilters, filters, orderBy);
for (const QFileInfo & entry : entries) {
MultiWidgets::ImageWidgetPtr image = MultiWidgets::create<MultiWidgets::ImageWidget>();
image->setSource(entry.absoluteFilePath());
addItem(image);
}
if (movable) {
addCSSClass("detached");
auto closeFunc = [] (MultiWidgets::Widget & widget) {
MultiWidgets::FadeAnimator::fadeOutAndDelete(widget, 0.2f);
};
auto timeout = std::make_shared<MultiWidgets::AfterIdleApplyLambdaOperator>(10.f, closeFunc, true);
addOperator(timeout);
addOperator(std::make_shared<MultiWidgets::LimitScaleOperator>());
flip();
eventAddListener("single-tap", [this] {
flip();
});
}
else {
auto flipFunc = [] (MultiWidgets::Widget & widget) {
if (auto flipWidget = dynamic_cast<CustomFlipWidget*>(&widget))
flipWidget->flip();
};
auto & random = Nimble::RandomUniform::instance();
auto flipperOp = std::make_shared<MultiWidgets::TimerOperator>(random.rand0X(10.f) + 10.f, flipFunc, false);
flipperOp->setTimer(random.rand0X(10.f));
addOperator(flipperOp);
eventAddListener("single-tap", [this, contentPath] {
auto hostWidget = Showcase::Showcase::instance()->ui()->mainWidget();
auto newWidget = MultiWidgets::create<CustomFlipWidget>(hostWidget, contentPath, true);
Nimble::Vector2f onMain = mapToParent(*hostWidget, Nimble::Vector2f(0.f, 0.f));
newWidget->setLocation(onMain + Nimble::Vector2f(10.f, 10.f));
newWidget->raiseToTop();
});
}
}
void CustomFlipWidget::flip()
{
setIndex(index() + 1);
}
}
Next create the grid widget, inherit from MultiWidgets::Widget.
#pragma once
#include <Showcase/WidgetComponent.hpp>
namespace ShowcaseExamples
{
class FlipGridWidget : public MultiWidgets::Widget
{
public:
FlipGridWidget();
virtual void update(const MultiWidgets::FrameInfo & frameInfo) override;
private:
void reloadGrid();
MultiWidgets::WidgetPtr m_container;
Valuable::AttributeString m_contentDir;
bool m_dirty;
};
}
Set up an attribute for the grid content folder. This attribute will be configurable in the Showcase Editor. We also create a container widget for the grid contents. It uses flex box layout to layout the widgets in a grid. When the content folder or the grid's size changes, reload the grid contents.
When reloading, we first clear the old content. Then search for all subfolders in the content folder. We will arrange the content into an N x N grid, calculate the grid size for the number of subfolders found. Then calculate the size for each item in the grid, while leaving some margin between the items.
Then create our custom flip widget for each subfolder, set its size and add it to the grid.
Finally we use the SHOWCASE_WIDGET_COMPONENT macro to define our FlipGridWidget as a Showcase component 'flip-grid'.
#include "FlipGridWidget.hpp"
#include "CustomFlipWidget.hpp"
#include <QDir>
namespace ShowcaseExamples
{
FlipGridWidget::FlipGridWidget()
: m_contentDir(this, "content-dir", QString())
, m_dirty(false)
{
setCSSType("FlipGridWidget");
m_container = MultiWidgets::create<MultiWidgets::Widget>(this);
m_container->addCSSClass("grid-container");
m_contentDir.addListener([this] {
m_dirty = true;
});
attribute("size")->addListener([this] {
m_dirty = true;
});
}
void FlipGridWidget::update(const MultiWidgets::FrameInfo &)
{
if (m_dirty) {
reloadGrid();
m_dirty = false;
}
}
void FlipGridWidget::reloadGrid()
{
m_container->removeChildren();
if (m_contentDir->isEmpty())
return;
QDir contentDir(m_contentDir);
QDir::Filters filters = QDir::Dirs | QDir::NoDotAndDotDot | QDir::Readable;
QDir::SortFlags orderBy = QDir::Name | QDir::LocaleAware;
QFileInfoList subDirs = contentDir.entryInfoList(filters, orderBy);
int itemCount = subDirs.size();
if (itemCount == 0)
return;
int gridSize = std::ceil(std::sqrt(itemCount));
float margin = 15.f;
float itemWidth = std::floor((width() - (gridSize * margin * 2)) / gridSize);
float itemHeigth = std::floor((height() - (gridSize * margin * 2)) / gridSize);
for (const QFileInfo & subDir : subDirs) {
auto item = MultiWidgets::create<CustomFlipWidget>(m_container, subDir.absoluteFilePath(), false);
item->setSize(itemWidth, itemHeigth);
item->setMargin(margin);
}
}
}
SHOWCASE_WIDGET_COMPONENT("flip-grid", ShowcaseExamples::FlipGridWidget)
We set up the style of the grid in CSS. Configure size, background color and input flags for the grid and flip widgets. We define display: flex for the container widget to make it use flex box layout. We also make the grid and movable flip widgets close if they're tossed out of the screen with remove-outside-parent.
FlipGridWidget {
size: 1000px 1000px;
background: transparent;
remove-outside-parent: true;
}
FlipGridWidget > .grid-container {
size: 100% 100%;
background: transparent;
input-flags: pass-to-children;
display: flex;
flex-wrap: wrap;
}
FlippingWidget.flip-grid {
background: transparent;
size: 500px 500px;
enable-flipping-gesture: false;
}
FlipGridWidget FlippingWidget.flip-grid {
input-flags: single-taps;
}
FlippingWidget.flip-grid.detached {
remove-outside-parent: true;
}
Now we create our plugin package. First create a schema file that defines the 'flip-grid' component's slots, roles and attributes. We'll use some ready schema templates to define common widget attributes. Include widget.schema to make widget's location and scale configurable. Include fixed-size.schema to make the size configurable. Also include with-behaviour-operators.schema to add attributes such as 'Close after idle' and 'Snap to angle', and with-toolbar.schema to include 'Toolbar' attribute. We'll add a 'main' role to the component to enable adding it to the main layer in structure. We'll also define our content folder attribute. Match the attribute name with the FlipGridWidget's attribute, and set the attribute type to asset and mime-filters to 'inode/directory' to allow dropping a folder from the media library to this attribute.
{
"includes" : [ "showcase-package:widget.schema",
"showcase-package:custom-schemas/fixed-size.schema",
"showcase-package:custom-schemas/with-behaviour-operators.schema",
"showcase-package:custom-schemas/with-toolbar.schema" ],
"roles" : [ "main" ],
"attributes" : [
{
"attribute-name": "content-dir",
"mime-filters" : [ "inode/directory" ],
"type" : "asset",
"ui-name": "Content folder",
"description": "Choose the media library folder that contains the source images for the grid. The folder should contain subfolders with the contents for the flip widgets. Each subfolder adds a flip widget to the grid, containing the images in that subfolder. Drag the folder from the media library into the input box."
}
]
}
We'll also include a description file with a screenshot to show a desciption of this widget in the Showcase Editor.
<img src="flip-grid.png" />
<p>This is an example of an MT Showcase plugin widget. The widget shows a
collection of flip widgets in a grid. Each flip widget contains a series of
images. User can tap a widget in the grid to launch a copy of it and then tap
the widget to flip through the images. The widgets in the grid also flip on
their own at random intervals.</p>
<p>The grid is populated from a folder. Choose a folder from the media library.
The folder should contain subfolders that contain the images for the flip
widgets. Each subfolder creates one flip widget in the grid. Widgets are ordered
in rows alphabetically by the subfolder name.</p>
Finally create the package file, and define the files for the component.
{
"name" : "Flip grid example",
"package-version" : "1.0.0",
"libraries" : [ "FlipGrid-SHOWCASE_VERSION" ],
"required-showcase-version" : "SHOWCASE_VERSION",
"components" : {
"flip-grid" : {
"type" : "ui",
"data-folders" : [ "data" ],
"css-files" : [ "flip-grid.css" ],
"schema" : "flip-grid.schema",
"description" : "flip-grid-description.html",
"ui-name" : "Flip grid"
}
}
}
The final folder structure of the ready plugin package should look like this.
package/
├── data
│ └── flip-grid.css
├── flip-grid.package
├── flip-grid.schema
├── flip-grid-description.html
├── flip-grid.png
└── libFlipGrid-[version].so