99#include " Utility/CachedTextRender.h"
1010#include " Utility/NanoVGGraphicsContext.h"
1111#include " Components/BouncingViewport.h"
12+ #include " Utility/PatchInfo.h"
1213
1314class WelcomePanel : public Component
1415 , public NVGComponent
@@ -121,13 +122,13 @@ class WelcomePanel : public Component
121122
122123 auto lB = bounds.toFloat ().expanded (0 .5f );
123124 {
124- auto bgCol = !isHovered ? convertColour (findColour (PlugDataColour::canvasBackgroundColourId )) : convertColour (findColour (PlugDataColour::toolbarBackgroundColourId));
125+ auto bgCol = !isHovered ? convertColour (findColour (PlugDataColour::panelForegroundColourId )) : convertColour (findColour (PlugDataColour::toolbarBackgroundColourId));
125126
126127 // Draw border around
127128 nvgDrawRoundedRect (nvg, lB.getX (), lB.getY (), lB.getWidth (), lB.getHeight (), bgCol, convertColour (findColour (PlugDataColour::toolbarOutlineColourId)), Corners::largeCornerRadius);
128129 }
129130
130- auto const bgColour = findColour (PlugDataColour::canvasBackgroundColourId );
131+ auto const bgColour = findColour (PlugDataColour::panelForegroundColourId );
131132 auto const bgCol = convertColour (bgColour);
132133 auto const newOpenIconCol = convertColour (bgColour.contrasting ().withAlpha (0 .32f ));
133134 auto const iconSize = 48 ;
@@ -255,22 +256,22 @@ class WelcomePanel : public Component
255256 enum TileType
256257 {
257258 Patch,
258- Library
259+ LibraryPatch
259260 };
260261
261262 TileType tileType = Patch;
262263
263264 public:
264- WelcomePanelTile (WelcomePanel& welcomePanel, File& patchFile, float scale, bool favourited, Image const & thumbImage = Image())
265+ WelcomePanelTile (WelcomePanel& welcomePanel, File& patchFile, String patchAuthor, float scale, bool favourited, Image const & thumbImage = Image())
265266 : isFavourited(favourited)
266267 , parent(welcomePanel)
267268 , snapshotScale(scale)
268269 , thumbnailImageData(thumbImage)
270+ , patchFile(patchFile)
269271 {
270272 tileName = patchFile.getFileNameWithoutExtension ();
271-
272- tileType = Library;
273-
273+ tileSubtitle = patchAuthor;
274+ tileType = LibraryPatch;
274275 resized ();
275276 }
276277
@@ -282,7 +283,7 @@ class WelcomePanel : public Component
282283 {
283284 patchFile = File (subTree.getProperty (" Path" ).toString ());
284285 tileName = patchFile.getFileNameWithoutExtension ();
285-
286+
286287 auto is24Hour = OSUtils::is24HourTimeFormat ();
287288
288289 auto formatTimeDescription = [is24Hour](const Time& openTime, bool showDayAndDate = false ) {
@@ -309,9 +310,9 @@ class WelcomePanel : public Component
309310
310311 };
311312
312- auto const accessedInPlugdasta = Time (static_cast <int64>(subTree.getProperty (" Time" )));
313+ auto const accessedInPlugdata = Time (static_cast <int64>(subTree.getProperty (" Time" )));
313314
314- tileSubtitle = formatTimeDescription (accessedInPlugdasta );
315+ tileSubtitle = formatTimeDescription (accessedInPlugdata );
315316
316317 auto const fileSize = patchFile.getSize ();
317318
@@ -329,7 +330,7 @@ class WelcomePanel : public Component
329330 // We need to show the time accessed from plugdata, which is saved in the settings XML
330331 // We want to show this again as well as in the subtile, but format it differently (with both Today/Yesterday and date)
331332 // because the popup menu may occlude the tile + subtitle
332- accessedTimeDescription = formatTimeDescription (accessedInPlugdasta , true );
333+ accessedTimeDescription = formatTimeDescription (accessedInPlugdata , true );
333334
334335 updateGeneratedThumbnailIfNeeded (thumbImage, svgImage);
335336 }
@@ -369,27 +370,70 @@ class WelcomePanel : public Component
369370
370371 PopupMenu tileMenu;
371372
372- tileMenu.addItem (PlatformStrings::getBrowserTip (), [this ]() {
373- if (patchFile.existsAsFile ())
374- patchFile.revealToUser ();
375- });
376- tileMenu.addSeparator ();
377- tileMenu.addItem (isFavourited ? " Remove from favourites" : " Add to favourites" , [this ]() {
378- isFavourited = !isFavourited;
379- onFavourite (isFavourited);
380- });
381- tileMenu.addSeparator ();
382- PopupMenu patchInfoSubMenu;
383- patchInfoSubMenu.addItem (String (" Size: " + fileSizeDescription), false , false , nullptr );
384- patchInfoSubMenu.addSeparator ();
385- patchInfoSubMenu.addItem (String (" Created: " + creationTimeDescription), false , false , nullptr );
386- patchInfoSubMenu.addItem (String (" Modified: " + modifiedTimeDescription), false , false , nullptr );
387- patchInfoSubMenu.addItem (String (" Accessed: " + accessedTimeDescription), false , false , nullptr );
388- tileMenu.addSubMenu (String (tileName + " .pd file info" ), patchInfoSubMenu, true );
389- tileMenu.addSeparator ();
390- // TODO: we may want to be clearer about this - that it doesn't delete the file on disk
391- // Put this at he bottom, so it's not accidentally clicked on
392- tileMenu.addItem (" Remove from recently opened" , onRemove);
373+ if (tileType == LibraryPatch) {
374+ tileMenu.addItem (PlatformStrings::getBrowserTip (), [this ]() {
375+ if (patchFile.existsAsFile ())
376+ patchFile.revealToUser ();
377+ });
378+
379+ tileMenu.addSeparator ();
380+
381+ auto metaFile = patchFile.getParentDirectory ().getChildFile (" meta.json" );
382+ if (metaFile.existsAsFile ()) {
383+
384+ auto json = JSON::fromString (metaFile.loadFileAsString ());
385+ auto patchInfo = PatchInfo (json);
386+
387+ PopupMenu patchInfoSubMenu;
388+ patchInfoSubMenu.addItem (" Title: " + patchInfo.title , false , false , nullptr );
389+ patchInfoSubMenu.addItem (" Author: " + patchInfo.author , false , false , nullptr );
390+ patchInfoSubMenu.addItem (" Released: " + patchInfo.releaseDate , false , false , nullptr );
391+ patchInfoSubMenu.addItem (" About: " + patchInfo.description , false , false , nullptr );
392+
393+ tileMenu.addSubMenu (String (tileName + " info" ), patchInfoSubMenu, true );
394+ } else {
395+ tileMenu.addItem (" Patch info not provided" , false , false , nullptr );
396+ }
397+
398+ tileMenu.addSeparator ();
399+
400+ // Put this at the bottom, so it's not accidentally clicked on
401+ tileMenu.addItem (" Delete from library..." , [this ]() {
402+ Dialogs::showMultiChoiceDialog (&parent.confirmationDialog , parent.getParentComponent (), " Are you sure you want to delete: " + patchFile.getFileNameWithoutExtension (), [this ](int choice) {
403+ if (choice == 0 ) {
404+ patchFile.getParentDirectory ().deleteRecursively (true );
405+ parent.triggerAsyncUpdate ();
406+ }
407+ }, { " Yes" , " No" }, Icons::Warning);
408+ });
409+ } else {
410+ if (tileType == Patch) {
411+ tileMenu.addItem (PlatformStrings::getBrowserTip (), [this ]() {
412+ if (patchFile.existsAsFile ())
413+ patchFile.revealToUser ();
414+ });
415+
416+ tileMenu.addSeparator ();
417+ tileMenu.addItem (isFavourited ? " Remove from favourites" : " Add to favourites" , [this ]() {
418+ isFavourited = !isFavourited;
419+ onFavourite (isFavourited);
420+ });
421+
422+ tileMenu.addSeparator ();
423+ PopupMenu patchInfoSubMenu;
424+ patchInfoSubMenu.addItem (String (" Size: " + fileSizeDescription), false , false , nullptr );
425+ patchInfoSubMenu.addSeparator ();
426+ patchInfoSubMenu.addItem (String (" Created: " + creationTimeDescription), false , false , nullptr );
427+ patchInfoSubMenu.addItem (String (" Modified: " + modifiedTimeDescription), false , false , nullptr );
428+ patchInfoSubMenu.addItem (String (" Accessed: " + accessedTimeDescription), false , false , nullptr );
429+ tileMenu.addSubMenu (String (tileName + " .pd file info" ), patchInfoSubMenu, true );
430+ }
431+ tileMenu.addSeparator ();
432+
433+ // TODO: we may want to be clearer about this - that it doesn't delete the file on disk
434+ // Put this at he bottom, so it's not accidentally clicked on
435+ tileMenu.addItem (" Remove from recently opened" , onRemove);
436+ }
393437
394438 PopupMenu::Options options;
395439 options.withTargetComponent (this );
@@ -447,7 +491,7 @@ class WelcomePanel : public Component
447491 });
448492 }
449493 } else {
450- if (tileType == Patch && snapshot && !snapshotImage.isValid ()) {
494+ if (tileType != LibraryPatch && snapshot && !snapshotImage.isValid ()) {
451495 snapshotImage = NVGImage (nvg, bounds.getWidth () * 2 , (bounds.getHeight () - 32 ) * 2 , [this ](Graphics& g) {
452496 g.addTransform (AffineTransform::scale (2 .0f ));
453497 snapshot->drawAt (g, 0 , 0 , 1 .0f );
@@ -461,7 +505,7 @@ class WelcomePanel : public Component
461505
462506 auto lB = bounds.toFloat ().expanded (0 .5f );
463507 // Draw background even for images incase there is a transparent PNG
464- nvgDrawRoundedRect (nvg, lB.getX (), lB.getY (), lB.getWidth (), lB.getHeight (), convertColour (findColour (PlugDataColour::canvasBackgroundColourId )), convertColour (findColour (PlugDataColour::toolbarOutlineColourId)), Corners::largeCornerRadius);
508+ nvgDrawRoundedRect (nvg, lB.getX (), lB.getY (), lB.getWidth (), lB.getHeight (), convertColour (findColour (PlugDataColour::panelForegroundColourId )), convertColour (findColour (PlugDataColour::toolbarOutlineColourId)), Corners::largeCornerRadius);
465509 if (thumbnailImageData.isValid ()) {
466510 // Render the thumbnail image file that is in the root dir of the pd patch
467511 auto sB = bounds.toFloat ().reduced (0 .2f );
@@ -477,7 +521,7 @@ class WelcomePanel : public Component
477521 nvgFontFace (nvg, " icon_font-Regular" );
478522 nvgFontSize (nvg, 68 .0f );
479523 nvgTextAlign (nvg, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE);
480- nvgText (nvg, bounds.getCentreX (), (bounds.getHeight () - 30 ) * 0 .5f , Icons::PlugdataIconStandard.toRawUTF8 (), nullptr );
524+ nvgText (nvg, bounds.getCentreX (), (bounds.getHeight () - 30 ) * 0 .5f , tileType == LibraryPatch ? Icons::PlugdataIconStandard. toRawUTF8 () : Icons::Error .toRawUTF8 (), nullptr );
481525 }
482526
483527 nvgRestore (nvg);
@@ -677,6 +721,9 @@ class WelcomePanel : public Component
677721 auto const buttonY = getHeight () * 0 .5f - 30 ;
678722 newPatchTile->setBounds (rowBounds.withX (startX).withWidth (buttonWidth).withY (buttonY));
679723 openPatchTile->setBounds (rowBounds.withX (startX + buttonWidth + tileSpacing).withWidth (buttonWidth).withY (buttonY));
724+
725+ auto firstTileBounds = rowBounds.removeFromLeft (actualTileWidth * 1 .5f );
726+ storeTile->setBounds (firstTileBounds);
680727 } else {
681728 auto firstTileBounds = rowBounds.removeFromLeft (actualTileWidth * 1 .5f );
682729 newPatchTile->setBounds (firstTileBounds);
@@ -782,6 +829,16 @@ class WelcomePanel : public Component
782829
783830 auto subTree = recentlyOpenedTree.getChild (i);
784831 auto patchFile = File (subTree.getProperty (" Path" ).toString ());
832+
833+ if (!File (patchFile).existsAsFile ())
834+ {
835+ if (!subTree.hasProperty (" Removable" ))
836+ {
837+ recentlyOpenedTree.removeChild (subTree, nullptr );
838+ }
839+ continue ;
840+ }
841+
785842 auto patchThumbnailBase = File (patchFile.getParentDirectory ().getFullPathName () + " \\ " + patchFile.getFileNameWithoutExtension () + " _thumb" );
786843
787844 auto favourited = subTree.hasProperty (" Pinned" ) && static_cast <bool >(subTree.getProperty (" Pinned" ));
@@ -874,7 +931,14 @@ class WelcomePanel : public Component
874931 break ;
875932 }
876933 }
877- auto * tile = libraryTiles.add (new WelcomePanelTile (*this , patchFile, scale, false , thumbImage));
934+ auto metaFile = patchFile.getParentDirectory ().getChildFile (" meta.json" );
935+ String author;
936+ if (metaFile.existsAsFile ())
937+ {
938+ auto json = JSON::fromString (metaFile.loadFileAsString ());
939+ author = json[" Author" ].toString ();
940+ }
941+ auto * tile = libraryTiles.add (new WelcomePanelTile (*this , patchFile, author, scale, false , thumbImage));
878942 tile->onClick = [this , patchFile]() mutable {
879943 if (patchFile.existsAsFile ()) {
880944 editor->pd ->autosave ->checkForMoreRecentAutosave (patchFile, editor, [this , patchFile]() {
@@ -958,6 +1022,8 @@ class WelcomePanel : public Component
9581022 String searchQuery;
9591023 Tab currentTab = Home;
9601024 UnorderedMap<String, String> patchSvgCache;
1025+
1026+ std::unique_ptr<Dialog> confirmationDialog;
9611027
9621028 // To make the library panel update automatically
9631029 class LibraryFSListener : public FileSystemWatcher ::Listener
0 commit comments