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