1 // This file is part of VSTGUI. It is subject to the license terms
2 // in the LICENSE file found in the top-level directory of this
3 // distribution and at http://github.com/steinbergmedia/vstgui/LICENSE
4 
5 #include "documentcontroller.h"
6 #include "imageframesview.h"
7 #include "vstgui/lib/cbitmap.h"
8 #include "vstgui/lib/cdatabrowser.h"
9 #include "vstgui/lib/cgradientview.h"
10 #include "vstgui/lib/coffscreencontext.h"
11 #include "vstgui/lib/controls/cmoviebitmap.h"
12 #include "vstgui/lib/cscrollview.h"
13 #include "vstgui/lib/csplitview.h"
14 #include "vstgui/lib/platform/iplatformbitmap.h"
15 #include "vstgui/standalone/include/helpers/uidesc/modelbinding.h"
16 #include "vstgui/standalone/include/helpers/value.h"
17 #include "vstgui/standalone/include/ialertbox.h"
18 #include "vstgui/standalone/include/iapplication.h"
19 #include "vstgui/standalone/include/iasync.h"
20 #include "vstgui/uidescription/cstream.h"
21 #include "vstgui/uidescription/delegationcontroller.h"
22 #include "vstgui/uidescription/iuidescription.h"
23 #include "vstgui/uidescription/uiattributes.h"
24 
25 //------------------------------------------------------------------------
26 namespace VSTGUI {
27 namespace ImageStitcher {
28 
29 using namespace VSTGUI::Standalone;
30 
31 static CFileExtension pngFileExtension ("PNG File", "png", "image/png", 0, "public.png");
32 
33 //------------------------------------------------------------------------
34 class ImageViewController : public DelegationController
35 {
36 public:
37 	using Proc = std::function<void (ImageFramesView*)>;
ImageViewController(Proc && proc,IController * parent)38 	ImageViewController (Proc&& proc, IController* parent)
39 	: DelegationController (parent), proc (std::move (proc))
40 	{
41 	}
42 
createView(const UIAttributes & attributes,const IUIDescription * description)43 	CView* createView (const UIAttributes& attributes, const IUIDescription* description) override
44 	{
45 		if (auto name = attributes.getAttributeValue (IUIDescription::kCustomViewName))
46 		{
47 			if (*name == "ImageView")
48 			{
49 				auto imageView = new ImageFramesView ();
50 				CColor color;
51 				if (description->getColor ("Focus", color))
52 					imageView->setSelectionColor (color);
53 				return imageView;
54 			}
55 		}
56 		return controller->createView (attributes, description);
57 	}
58 
verifyView(CView * view,const UIAttributes & attr,const IUIDescription * desc)59 	CView* verifyView (CView* view, const UIAttributes& attr, const IUIDescription* desc) override
60 	{
61 		if (auto name = attr.getAttributeValue (IUIDescription::kCustomViewName))
62 		{
63 			if (*name == "ImageView")
64 			{
65 				proc (dynamic_cast<ImageFramesView*> (view));
66 				return view;
67 			}
68 		}
69 		return controller->verifyView (view, attr, desc);
70 	}
71 
72 private:
73 	Proc proc;
74 };
75 
76 //------------------------------------------------------------------------
77 class MovieBitmapController : public DelegationController
78 {
79 public:
80 	using Proc = std::function<void (CMovieBitmap*)>;
MovieBitmapController(Proc && proc,IController * parent)81 	MovieBitmapController (Proc&& proc, IController* parent)
82 	: DelegationController (parent), proc (std::move (proc))
83 	{
84 	}
85 
verifyView(CView * view,const UIAttributes & attr,const IUIDescription * desc)86 	CView* verifyView (CView* view, const UIAttributes& attr, const IUIDescription* desc) override
87 	{
88 		if (auto mb = dynamic_cast<CMovieBitmap*> (view))
89 		{
90 			proc (mb);
91 			return view;
92 		}
93 		return controller->verifyView (view, attr, desc);
94 	}
95 
96 private:
97 	Proc proc;
98 };
99 
100 //------------------------------------------------------------------------
101 class SplitViewController : public DelegationController, public ISplitViewController
102 {
103 public:
SplitViewController(IController * parent,const IUIDescription * desc)104 	SplitViewController (IController* parent, const IUIDescription* desc)
105 	: DelegationController (parent), desc (desc)
106 	{
107 	}
108 
getSplitViewSizeConstraint(int32_t index,CCoord & minSize,CCoord & maxSize,CSplitView * splitView)109 	bool getSplitViewSizeConstraint (int32_t index, CCoord& minSize, CCoord& maxSize,
110 	                                 CSplitView* splitView) override
111 	{
112 		if (index == 0)
113 		{
114 			minSize = 45;
115 			maxSize = -1;
116 			return true;
117 		}
118 		if (index == 1)
119 		{
120 			minSize = 250;
121 			maxSize = -1;
122 			return true;
123 		}
124 		return false;
125 	}
getSplitViewSeparatorDrawer(CSplitView * splitView)126 	ISplitViewSeparatorDrawer* getSplitViewSeparatorDrawer (CSplitView* splitView) override
127 	{
128 		return nullptr;
129 	}
storeViewSize(int32_t index,const CCoord & size,CSplitView * splitView)130 	bool storeViewSize (int32_t index, const CCoord& size, CSplitView* splitView) override
131 	{
132 		return false;
133 	}
restoreViewSize(int32_t index,CCoord & size,CSplitView * splitView)134 	bool restoreViewSize (int32_t index, CCoord& size, CSplitView* splitView) override
135 	{
136 		if (!gradientAdded)
137 		{
138 			if (auto view = desc->createView ("SplitViewSeperatorView", this))
139 			{
140 				if (auto container = view->asViewContainer ())
141 				{
142 					auto gradientView = container->getView (0);
143 					gradientView->removeAttribute ('cvcr');
144 					container->removeView (gradientView, false);
145 					auto viewSize = splitView->getViewSize ();
146 					auto sepWidth = splitView->getSeparatorWidth ();
147 					gradientView->setViewSize (CRect (0, 0, sepWidth, viewSize.getHeight ()));
148 					splitView->addViewToSeparator (0, gradientView);
149 					gradientAdded = true;
150 				}
151 				view->forget ();
152 			}
153 		}
154 		return false;
155 	}
156 
157 private:
158 	const IUIDescription* desc {nullptr};
159 	bool gradientAdded {false};
160 };
161 
162 //------------------------------------------------------------------------
make(const DocumentContextPtr & doc)163 std::shared_ptr<DocumentWindowController> DocumentWindowController::make (
164     const DocumentContextPtr& doc)
165 {
166 	auto controller = std::make_shared<DocumentWindowController> (doc);
167 
168 	UIDesc::Config config;
169 	config.uiDescFileName = "Window.uidesc";
170 	config.viewName = "Window";
171 	config.windowConfig.title = getDisplayFilename (doc->getPath ());
172 	config.windowConfig.autoSaveFrameName = "DocumentController";
173 	config.windowConfig.groupIdentifier = "Document";
174 	config.windowConfig.style.border ().close ().size ().centered ();
175 	config.customization = controller;
176 	config.modelBinding = controller->createModelBinding ();
177 
178 	controller->window = UIDesc::makeWindow (config);
179 	return controller;
180 }
181 
182 //------------------------------------------------------------------------
DocumentWindowController(const DocumentContextPtr & doc)183 DocumentWindowController::DocumentWindowController (const DocumentContextPtr& doc)
184 : docContext (doc)
185 {
186 	for (auto index = 0u; index < docContext->getImagePaths ().size (); ++index)
187 		onImagePathAdded (docContext->getImagePaths ()[index], index);
188 	docContext->addListener (this);
189 	docIsDirty = false;
190 }
191 
192 //------------------------------------------------------------------------
~DocumentWindowController()193 DocumentWindowController::~DocumentWindowController () noexcept
194 {
195 	docContext->removeListener (this);
196 }
197 
198 //------------------------------------------------------------------------
showWindow()199 void DocumentWindowController::showWindow ()
200 {
201 	if (window)
202 		window->show ();
203 }
204 
205 //------------------------------------------------------------------------
closeWindow()206 void DocumentWindowController::closeWindow ()
207 {
208 	if (window)
209 		window->close ();
210 }
211 
212 //------------------------------------------------------------------------
registerWindowListener(IWindowListener * listener)213 void DocumentWindowController::registerWindowListener (IWindowListener* listener)
214 {
215 	if (window)
216 		window->registerWindowListener (listener);
217 }
218 
219 //------------------------------------------------------------------------
createModelBinding()220 UIDesc::ModelBindingPtr DocumentWindowController::createModelBinding ()
221 {
222 	auto binding = UIDesc::ModelBindingCallbacks::make ();
223 	binding->addValue (Value::make ("AddPath"), UIDesc::ValueCalls::onAction ([this] (auto& v) {
224 		                   this->doAddPathCommand ();
225 		                   v.performEdit (0.);
226 	                   }));
227 	binding->addValue (Value::make ("RemovePath"), UIDesc::ValueCalls::onAction ([this] (auto& v) {
228 		                   this->doRemovePathCommand ();
229 		                   v.performEdit (0.);
230 	                   }));
231 
232 	displayFrameValue = Value::makeStepValue ("DisplayFrame", 0, 1);
233 	binding->addValue (displayFrameValue);
234 
235 	auto animationRunning = Value::make ("RunAnimation");
236 	binding->addValue (animationRunning, UIDesc::ValueCalls::onPerformEdit ([this] (auto& v) {
237 		                   if (v.getValue () >= 0.5)
238 			                   this->doStartAnimation ();
239 		                   else
240 			                   this->doStopAnimation ();
241 	                   }));
242 	animationTimeValue = Value::make ("AnimationTime", 0, Value::makeRangeConverter (16, 500, 0));
243 	binding->addValue (animationTimeValue,
244 	                   UIDesc::ValueCalls::onPerformEdit ([this, animationRunning] (auto& v) {
245 		                   if (animationRunning->getValue () >= 0.5)
246 			                   this->doStartAnimation ();
247 	                   }));
248 	return binding;
249 }
250 
251 //------------------------------------------------------------------------
createController(const UTF8StringView & name,IController * parent,const IUIDescription * uiDesc)252 IController* DocumentWindowController::createController (const UTF8StringView& name,
253                                                          IController* parent,
254                                                          const IUIDescription* uiDesc)
255 {
256 	if (name == "ImageViewController")
257 		return new ImageViewController (
258 		    [&] (ImageFramesView* view) {
259 			    imageView = view;
260 			    imageView->setImageList (&imageList);
261 			    imageView->setDocContext (docContext);
262 		    },
263 		    parent);
264 	if (name == "MovieBitmapController")
265 		return new MovieBitmapController ([&] (CMovieBitmap* view) { movieBitmapView = view; },
266 		                                  parent);
267 	if (name == "SplitViewController")
268 		return new SplitViewController (parent, uiDesc);
269 	return nullptr;
270 }
271 
272 //------------------------------------------------------------------------
onUIDescriptionParsed(const IUIDescription * uiDesc)273 void DocumentWindowController::onUIDescriptionParsed (const IUIDescription* uiDesc)
274 {
275 }
276 
277 //------------------------------------------------------------------------
onSetContentView(IWindow & w,const SharedPointer<CFrame> & cv)278 void DocumentWindowController::onSetContentView (IWindow& w, const SharedPointer<CFrame>& cv)
279 {
280 	contentView = cv;
281 	if (imageView)
282 		imageView->setImageList (&imageList);
283 }
284 
285 //------------------------------------------------------------------------
onClosed(const IWindow & w)286 void DocumentWindowController::onClosed (const IWindow& w)
287 {
288 	assert (&w == window.get ());
289 	window = nullptr;
290 }
291 
292 //------------------------------------------------------------------------
canClose(const IWindow &)293 bool DocumentWindowController::canClose (const IWindow&)
294 {
295 	if (docIsDirty)
296 	{
297 		AlertBoxConfig alert;
298 		alert.headline = "Do you want to save the changes made to the document \"";
299 		alert.headline += getDisplayFilename (docContext->getPath ());
300 		alert.headline += "\"?";
301 		alert.description = "Your changes will be lost if you don't save them.";
302 		alert.defaultButton = "Save";
303 		alert.secondButton = "Cancel";
304 		alert.thirdButton = "Don't Save";
305 		auto result = IApplication::instance ().showAlertBox (alert);
306 		switch (result)
307 		{
308 			case AlertResult::DefaultButton:
309 			{
310 				if (pathIsAbsolute (docContext->getPath ()))
311 				{
312 					doSave ();
313 					return true;
314 				}
315 				doSaveAs ([this] (bool success) {
316 					if (success)
317 						window->close ();
318 				});
319 				return false;
320 			}
321 			case AlertResult::SecondButton: return false;
322 			default: return true;
323 		}
324 	}
325 	return true;
326 }
327 
328 //------------------------------------------------------------------------
exportImage(const SharedPointer<CBitmap> & image,UTF8StringPtr path)329 static bool exportImage (const SharedPointer<CBitmap>& image, UTF8StringPtr path)
330 {
331 	auto platformBitmap = image->getPlatformBitmap ();
332 	assert (platformBitmap);
333 	auto buffer = IPlatformBitmap::createMemoryPNGRepresentation (platformBitmap);
334 	CFileStream stream;
335 	if (!stream.open (path, CFileStream::kWriteMode | CFileStream::kBinaryMode |
336 	                            CFileStream::kTruncateMode))
337 		return false;
338 	return stream.writeRaw (buffer.data (), static_cast<uint32_t> (buffer.size ())) ==
339 	       buffer.size ();
340 }
341 
342 //------------------------------------------------------------------------
doExport()343 void DocumentWindowController::doExport ()
344 {
345 	auto fs =
346 	    owned (CNewFileSelector::create (contentView, CNewFileSelector::Style::kSelectSaveFile));
347 	if (!fs)
348 		return;
349 	fs->setTitle ("Save Stitched Image");
350 	fs->setDefaultExtension (pngFileExtension);
351 	// TODO: set filename depending on doc name
352 	fs->run ([this] (CNewFileSelector* fs) {
353 		if (fs->getNumSelectedFiles () == 0)
354 			return;
355 		if (auto image = createStitchedBitmap ())
356 		{
357 			if (!exportImage (image, fs->getSelectedFile (0)))
358 			{
359 				AlertBoxForWindowConfig alert;
360 				alert.window = window;
361 				alert.headline = "Export failed";
362 				IApplication::instance ().showAlertBoxForWindow (alert);
363 			}
364 		}
365 	});
366 }
367 
368 //------------------------------------------------------------------------
doSave()369 void DocumentWindowController::doSave ()
370 {
371 	if (docContext->save ())
372 		docIsDirty = false;
373 }
374 
375 //------------------------------------------------------------------------
doSaveAs(std::function<void (bool saved)> && customAction)376 void DocumentWindowController::doSaveAs (std::function<void (bool saved)>&& customAction)
377 {
378 	auto fs =
379 	    owned (CNewFileSelector::create (contentView, CNewFileSelector::Style::kSelectSaveFile));
380 	if (!fs)
381 		return;
382 	fs->setTitle ("Choose Save Destination");
383 	fs->setDefaultExtension (imageStitchExtension);
384 	fs->setInitialDirectory (docContext->getPath ().data ());
385 	fs->setDefaultSaveName (getDisplayFilename (docContext->getPath ()).data ());
386 	fs->run ([this, customAction = std::move (customAction)] (CNewFileSelector * fs) {
387 		if (fs->getNumSelectedFiles () == 0)
388 		{
389 			customAction (false);
390 			return;
391 		}
392 		docContext->setPath (fs->getSelectedFile (0));
393 		if (docContext->save ())
394 		{
395 			window->setTitle (getDisplayFilename (docContext->getPath ()));
396 			window->setRepresentedPath (UTF8String (docContext->getPath ()));
397 			docIsDirty = false;
398 			customAction (true);
399 		}
400 		else
401 			customAction (false);
402 	});
403 }
404 
405 //------------------------------------------------------------------------
onImagePathAdded(const Path & newPath,size_t index)406 void DocumentWindowController::onImagePathAdded (const Path& newPath, size_t index)
407 {
408 	auto platformBitmap = IPlatformBitmap::createFromPath (newPath.data ());
409 	if (!platformBitmap)
410 	{
411 		CPoint size (docContext->getWidth (), docContext->getHeight ());
412 		platformBitmap = IPlatformBitmap::create (&size);
413 	}
414 	auto it = imageList.begin ();
415 	if (index >= imageList.size ())
416 		it = imageList.end ();
417 	else
418 		std::advance (it, index);
419 	imageList.insert (it, {makeOwned<CBitmap> (platformBitmap), newPath, false});
420 	setDirty ();
421 }
422 
423 //------------------------------------------------------------------------
onImagePathRemoved(const Path & newPath,size_t index)424 void DocumentWindowController::onImagePathRemoved (const Path& newPath, size_t index)
425 {
426 	auto it = imageList.begin ();
427 	std::advance (it, index);
428 	imageList.erase (it);
429 	setDirty ();
430 }
431 
432 //------------------------------------------------------------------------
doOpenDocument(std::function<void (bool saved)> && customAction)433 void DocumentWindowController::doOpenDocument (std::function<void (bool saved)>&& customAction)
434 {
435 	auto fs = owned (CNewFileSelector::create (contentView));
436 	if (!fs)
437 		return;
438 	fs->setTitle ("Choose Document");
439 	fs->setDefaultExtension (imageStitchExtension);
440 	fs->run ([this, customAction = std::move (customAction)] (CNewFileSelector * fs) {
441 		if (fs->getNumSelectedFiles () == 0)
442 		{
443 			customAction (false);
444 			return;
445 		}
446 		if (auto newDocContext = DocumentContext::loadDocument (fs->getSelectedFile (0)))
447 		{
448 			docContext->replaceDocument (newDocContext->getDocument ());
449 			window->setTitle (getDisplayFilename (docContext->getPath ()));
450 			window->setRepresentedPath (UTF8String (docContext->getPath ()));
451 			docIsDirty = false;
452 			customAction (true);
453 		}
454 		else
455 			customAction (false);
456 	});
457 }
458 
459 //------------------------------------------------------------------------
doAddPathCommand()460 void DocumentWindowController::doAddPathCommand ()
461 {
462 	auto fs = owned (CNewFileSelector::create (contentView));
463 	if (!fs)
464 		return;
465 	fs->setAllowMultiFileSelection (true);
466 	fs->setTitle ("Choose Images");
467 	fs->setDefaultExtension (pngFileExtension);
468 	fs->run ([this] (CNewFileSelector* fs) {
469 		auto numFiles = fs->getNumSelectedFiles ();
470 		if (numFiles == 0)
471 			return;
472 		std::string alertDescription;
473 		size_t pos = lastSelectedPos ();
474 		doDeselectAllCommand ();
475 		for (auto i = 0u; i < numFiles; ++i)
476 		{
477 			auto path = fs->getSelectedFile (i);
478 			auto result = docContext->insertImagePathAtIndex (pos, path);
479 			if (result != DocumentContextResult::Success)
480 			{
481 				switch (result)
482 				{
483 					case DocumentContextResult::ImageSizeMismatch:
484 					{
485 						alertDescription += "Image Size Mismatch :";
486 						alertDescription += path;
487 						alertDescription += "\n";
488 						break;
489 					}
490 					case DocumentContextResult::InvalidImage:
491 					{
492 						alertDescription += "Invalid Image :";
493 						alertDescription += path;
494 						alertDescription += "\n";
495 						break;
496 					}
497 					case DocumentContextResult::InvalidIndex:
498 					{
499 						alertDescription +=
500 						    "Internal Error (DocumentContextResult::InvalidIndex) adding ";
501 						alertDescription += path;
502 						alertDescription += "\n";
503 						break;
504 					}
505 					case DocumentContextResult::Success: break;
506 				}
507 			}
508 			else
509 			{
510 				imageList[pos].selected = true;
511 				++pos;
512 			}
513 		}
514 		if (!alertDescription.empty ())
515 		{
516 			AlertBoxForWindowConfig alert;
517 			alert.window = window;
518 			alert.headline = "Error adding images!";
519 			alert.description = alertDescription;
520 			IApplication::instance ().showAlertBoxForWindow (alert);
521 		}
522 	});
523 }
524 
525 //------------------------------------------------------------------------
doRemovePathCommand()526 void DocumentWindowController::doRemovePathCommand ()
527 {
528 	std::vector<size_t> indices;
529 	for (auto index = 0u; index < imageList.size (); ++index)
530 	{
531 		if (imageList[index].selected)
532 		{
533 			indices.push_back (index);
534 		}
535 	}
536 	for (auto it = indices.rbegin (); it != indices.rend (); ++it)
537 		docContext->removeImagePathAtIndex (*it);
538 }
539 
540 //------------------------------------------------------------------------
doStartAnimation()541 void DocumentWindowController::doStartAnimation ()
542 {
543 	auto& converter = animationTimeValue->getConverter ();
544 	auto time =
545 	    static_cast<uint32_t> (converter.normalizedToPlain (animationTimeValue->getValue ()));
546 	timer = makeOwned<CVSTGUITimer> (
547 	    [this] (auto) {
548 		    auto v = displayFrameValue->getValue ();
549 		    v += 1. / imageList.size ();
550 		    if (v + std::numeric_limits<double>::epsilon () >= 1.)
551 			    v = 0.;
552 		    displayFrameValue->beginEdit ();
553 		    displayFrameValue->performEdit (v);
554 		    displayFrameValue->endEdit ();
555 	    },
556 	    time);
557 }
558 
559 //------------------------------------------------------------------------
doStopAnimation()560 void DocumentWindowController::doStopAnimation ()
561 {
562 	if (!timer)
563 		return;
564 
565 	timer->stop ();
566 	timer = nullptr;
567 }
568 
569 //------------------------------------------------------------------------
doSelectAllCommand()570 void DocumentWindowController::doSelectAllCommand ()
571 {
572 	for (auto& image : imageList)
573 		image.selected = true;
574 	if (imageView)
575 		imageView->invalid ();
576 }
577 
578 //------------------------------------------------------------------------
doDeselectAllCommand()579 void DocumentWindowController::doDeselectAllCommand ()
580 {
581 	for (auto& image : imageList)
582 		image.selected = false;
583 	if (imageView)
584 		imageView->invalid ();
585 }
586 
587 //------------------------------------------------------------------------
somethingSelected() const588 bool DocumentWindowController::somethingSelected () const
589 {
590 	for (auto& image : imageList)
591 	{
592 		if (image.selected)
593 			return true;
594 	}
595 	return false;
596 }
597 
598 //------------------------------------------------------------------------
lastSelectedPos() const599 size_t DocumentWindowController::lastSelectedPos () const
600 {
601 	auto it =
602 	    std::find_if (imageList.rbegin (), imageList.rend (), [] (auto& e) { return e.selected; });
603 	if (it == imageList.rend ())
604 		return imageList.size ();
605 	return std::distance (imageList.begin (), it.base ());
606 }
607 
608 //------------------------------------------------------------------------
canHandleCommand(const Command & command)609 bool DocumentWindowController::canHandleCommand (const Command& command)
610 {
611 	if (command == Commands::Delete)
612 		return somethingSelected ();
613 	if (command == Commands::SelectAll)
614 		return !imageList.empty ();
615 	if (command == ExportCommand)
616 		return !imageList.empty ();
617 	if (command == Commands::SaveDocumentAs)
618 		return !imageList.empty ();
619 	if (command == Commands::SaveDocument)
620 		return !imageList.empty () && pathIsAbsolute (docContext->getPath ());
621 	return false;
622 }
623 
624 //------------------------------------------------------------------------
handleCommand(const Command & command)625 bool DocumentWindowController::handleCommand (const Command& command)
626 {
627 	if (command == Commands::Delete)
628 	{
629 		doRemovePathCommand ();
630 		return true;
631 	}
632 	if (command == Commands::SelectAll)
633 	{
634 		doSelectAllCommand ();
635 		return true;
636 	}
637 	if (command == ExportCommand)
638 	{
639 		doExport ();
640 		return true;
641 	}
642 	if (command == Commands::SaveDocument)
643 	{
644 		doSave ();
645 		return true;
646 	}
647 	if (command == Commands::SaveDocumentAs)
648 	{
649 		doSaveAs ();
650 		return true;
651 	}
652 	return false;
653 }
654 
655 //------------------------------------------------------------------------
createStitchedBitmap()656 SharedPointer<CBitmap> DocumentWindowController::createStitchedBitmap ()
657 {
658 	if (!contentView || docContext->getImagePaths ().empty ())
659 		return nullptr;
660 	CRect r;
661 	CPoint size (docContext->getWidth (), docContext->getHeight ());
662 	r.setSize (size);
663 	size.y *= docContext->getImagePaths ().size ();
664 
665 	auto offscreen = COffscreenContext::create (contentView, size.x, size.y);
666 	if (!offscreen)
667 		return nullptr;
668 
669 	offscreen->beginDraw ();
670 	for (auto image : imageList)
671 	{
672 		image.bitmap->draw (offscreen, r);
673 		r.offset (0, docContext->getHeight ());
674 	}
675 	offscreen->endDraw ();
676 
677 	return shared (offscreen->getBitmap ());
678 }
679 
680 //------------------------------------------------------------------------
setDirty()681 void DocumentWindowController::setDirty ()
682 {
683 	docIsDirty = true;
684 	if (asyncUpdateTriggered)
685 		return;
686 	asyncUpdateTriggered = true;
687 	Async::schedule (Async::mainQueue (), [this] () {
688 		if (auto v = displayFrameValue->dynamicCast<IMutableStepValue> ())
689 			v->setNumSteps (static_cast<uint32_t> (imageList.size ()));
690 		if (imageView)
691 			imageView->setImageList (&imageList);
692 		if (movieBitmapView)
693 		{
694 			movieBitmapView->setHeightOfOneImage (docContext->getHeight ());
695 			movieBitmapView->setNumSubPixmaps (static_cast<int32_t> (imageList.size ()));
696 			movieBitmapView->setBackground (createStitchedBitmap ());
697 			auto size = movieBitmapView->getViewSize ();
698 			size.setWidth (docContext->getWidth ());
699 			size.setHeight (docContext->getHeight ());
700 			movieBitmapView->setViewSize (size);
701 		}
702 		asyncUpdateTriggered = false;
703 	});
704 }
705 
706 //------------------------------------------------------------------------
707 } // ImageStitcher
708 } // VSTGUI
709