@@ -44,7 +44,7 @@ class WelcomePanel : public Component
4444 nvgFillColor (nvg, NVGComponent::convertColour (findColour (PlugDataColour::panelTextColourId)));
4545 nvgText (nvg, 96 , 138 , " Recently Opened" , NULL );
4646
47- nvgFontFace (nvg, " plugdata_icon_font " );
47+ nvgFontFace (nvg, " icon_font-Regular " );
4848 nvgFontSize (nvg, 14 );
4949 nvgFillColor (nvg, NVGComponent::convertColour (findColour (PlugDataColour::panelTextColourId).withAlpha (isHoveringClearButton ? 0 .6f : 1 .0f )));
5050 nvgText (nvg, clearButtonBounds.getCentreX (), clearButtonBounds.getCentreY (), Icons::Clear.toRawUTF8 (), NULL );
@@ -159,7 +159,7 @@ class WelcomePanel : public Component
159159 }
160160
161161 case Open: {
162- nvgFontFace (nvg, " plugdata_icon_font " );
162+ nvgFontFace (nvg, " icon_font-Regular " );
163163 nvgFillColor (nvg, bgCol);
164164 nvgFontSize (nvg, 34 );
165165 nvgTextAlign (nvg, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE);
@@ -199,6 +199,9 @@ class WelcomePanel : public Component
199199
200200 void mouseUp (MouseEvent const & e) override
201201 {
202+ if (!getScreenBounds ().reduced (12 ).contains (e.getScreenPosition ()))
203+ return ;
204+
202205 if (!e.mods .isLeftButtonDown ())
203206 return ;
204207
@@ -211,6 +214,7 @@ class WelcomePanel : public Component
211214 bool isFavourited;
212215 std::function<void ()> onClick = []() { };
213216 std::function<void (bool )> onFavourite = nullptr ;
217+ std::function<void ()> onRemove = []() { };
214218
215219 private:
216220 WelcomePanel& parent;
@@ -223,17 +227,74 @@ class WelcomePanel : public Component
223227 Image thumbnailImageData;
224228 int lastWidth = -1 ;
225229 int lastHeight = -1 ;
226-
230+
231+ String creationTimeDescription = String();
232+ String modifiedTimeDescription = String();
233+ String fileSizeDescription = String();
234+
235+ File patchFile;
236+
227237 public:
238+ WelcomePanelTile (WelcomePanel& welcomePanel, ValueTree subTree, String svgImage, Colour iconColour, float scale, bool favourited, Image const & thumbImage = Image())
239+ : parent(welcomePanel)
240+ , isFavourited(favourited)
241+ , snapshotScale(scale)
242+ , thumbnailImageData(thumbImage)
243+ {
244+ patchFile = File (subTree.getProperty (" Path" ).toString ());
245+ tileName = patchFile.getFileNameWithoutExtension ();
246+
247+ auto is24Hour = OSUtils::is24HourTimeFormat ();
248+
249+ auto formatTimeDescription = [is24Hour](const Time& openTime) {
250+ auto diff = Time::getCurrentTime () - openTime;
251+
252+ String date;
253+ auto days = diff.inDays ();
254+ if (days < 1 )
255+ date = " Today" ;
256+ else if (days > 1 && days < 2 )
257+ date = " Yesterday" ;
258+ else
259+ date = openTime.toString (true , false );
260+
261+ String time = openTime.toString (false , true , false , is24Hour);
262+
263+ return date + " , " + time;
264+ };
265+
266+ tileSubtitle = formatTimeDescription (Time (static_cast <int64>(subTree.getProperty (" Time" ))));
267+
268+ auto const fileSize = patchFile.getSize ();
269+
270+ if (fileSize < 1024 ) {
271+ fileSizeDescription = String (fileSize) + " Bytes" ; // Less than 1 KiB
272+ } else if (fileSize < 1024 * 1024 ) {
273+ fileSizeDescription = String (fileSize / 1024.0 , 2 ) + " KiB" ; // Between 1 KiB and 1 MiB
274+ } else {
275+ fileSizeDescription = String (fileSize / (1024.0 * 1024.0 ), 2 ) + " MiB" ; // 1 MiB or more
276+ }
277+
278+ creationTimeDescription = formatTimeDescription (patchFile.getCreationTime ());
279+ modifiedTimeDescription = formatTimeDescription (patchFile.getLastModificationTime ());
280+
281+ updateGeneratedThumbnailIfNeeded (thumbImage, svgImage, iconColour);
282+ }
283+
228284 WelcomePanelTile (WelcomePanel& welcomePanel, String name, String subtitle, String svgImage, Colour iconColour, float scale, bool favourited, Image const & thumbImage = Image())
229- : isFavourited(favourited )
230- , parent(welcomePanel )
285+ : parent(welcomePanel )
286+ , isFavourited(favourited )
231287 , snapshotScale(scale)
232288 , tileName(name)
233289 , tileSubtitle(subtitle)
234290 , thumbnailImageData(thumbImage)
235291 {
236- if (!thumbImage.isValid ()) {
292+ updateGeneratedThumbnailIfNeeded (thumbImage, svgImage, iconColour);
293+ }
294+
295+ void updateGeneratedThumbnailIfNeeded (Image const & thumbnailImage, String& svgImage, Colour iconColour)
296+ {
297+ if (!thumbnailImage.isValid ()) {
237298 snapshot = Drawable::createFromImageData (svgImage.toRawUTF8 (), svgImage.getNumBytesAsUTF8 ());
238299 if (snapshot) {
239300 snapshot->replaceColour (Colours::black, iconColour);
@@ -242,7 +303,47 @@ class WelcomePanel : public Component
242303
243304 resized ();
244305 }
245-
306+
307+ void setHovered ()
308+ {
309+ isHovered = true ;
310+ repaint ();
311+ }
312+
313+ void mouseDown (MouseEvent const & e) override {
314+ if (!e.mods .isRightButtonDown ())
315+ return ;
316+
317+ PopupMenu tileMenu;
318+
319+ tileMenu.addItem (PlatformStrings::getBrowserTip (), [this ]() {
320+ if (patchFile.existsAsFile ())
321+ patchFile.revealToUser ();
322+ });
323+ tileMenu.addSeparator ();
324+ tileMenu.addItem (isFavourited ? " Remove from favourites" : " Add to favourites" , [this ]() {
325+ isFavourited = !isFavourited;
326+ onFavourite (isFavourited);
327+ });
328+ tileMenu.addSeparator ();
329+ PopupMenu patchInfoSubMenu;
330+ patchInfoSubMenu.addItem (String (" Size: " + fileSizeDescription), false , false , nullptr );
331+ patchInfoSubMenu.addSeparator ();
332+ patchInfoSubMenu.addItem (String (" Created: " + creationTimeDescription), false , false , nullptr );
333+ patchInfoSubMenu.addItem (String (" Modified: " + modifiedTimeDescription), false , false , nullptr );
334+ patchInfoSubMenu.addItem (String (" Accessed: " + tileSubtitle), false , false , nullptr );
335+ tileMenu.addSubMenu (String (tileName + " .pd file info" ), patchInfoSubMenu, true );
336+ tileMenu.addSeparator ();
337+ // TODO: we may want to be clearer about this - that it doesn't delete the file on disk
338+ // Put this at he bottom, so it's not accidentally clicked on
339+ tileMenu.addItem (" Remove from recently opened" , onRemove);
340+
341+ PopupMenu::Options options;
342+ options.withTargetComponent (this );
343+
344+ tileMenu.showMenuAsync (options);
345+ }
346+
246347 void setSearchQuery (String const & searchQuery)
247348 {
248349 setVisible (tileName.containsIgnoreCase (searchQuery));
@@ -398,6 +499,11 @@ class WelcomePanel : public Component
398499 if (!e.mods .isLeftButtonDown ())
399500 return ;
400501
502+ // If the cursor is no longer over the tile, don't trigger the tile
503+ // (Standard behaviour for mouse up on object)
504+ if (!getLocalBounds ().reduced (12 ).contains (e.getPosition ()))
505+ return ;
506+
401507 if (onFavourite && getHeartIconBounds ().contains (e.x , e.y )) {
402508 isFavourited = !isFavourited;
403509 onFavourite (isFavourited);
@@ -522,7 +628,7 @@ class WelcomePanel : public Component
522628
523629 auto tilesBounds = Rectangle<int >(24 , showNewOpenTiles ? 146 : 24 , totalWidth + 24 , totalHeight + 24 );
524630
525- contentComponent.setBounds (tiles.size () ? tilesBounds : bounds );
631+ contentComponent.setBounds (tiles.size () ? tilesBounds : getLocalBounds () );
526632
527633 viewport.setBounce (!tiles.isEmpty ());
528634
@@ -583,24 +689,6 @@ class WelcomePanel : public Component
583689
584690 triggerAsyncUpdate ();
585691 }
586-
587- String getSystemLocalTime (uint64 timestamp) {
588- StackArray<char , 100 > buffer;
589- std::time_t now = static_cast <std::time_t >(timestamp / 1000 );
590- std::tm* localTime = std::localtime (&now);
591-
592- // Format the time using the current locale
593- std::strftime (buffer.data (), buffer.size (), " %X" , localTime);
594-
595- auto result = String::fromUTF8 (buffer.data ());
596-
597- // Remove seconds from system time format
598- auto secondsStart = result.lastIndexOfChar (' :' );
599- auto secondsEnd = result.indexOfChar (secondsStart, ' ' );
600- if (secondsEnd < 0 ) secondsEnd = result.length ();
601-
602- return result.substring (0 , secondsStart) + result.substring (secondsEnd);
603- }
604692
605693 void handleAsyncUpdate () override
606694 {
@@ -648,21 +736,8 @@ class WelcomePanel : public Component
648736 }
649737 }
650738
651- auto openTimestamp = static_cast <int64>(subTree.getProperty (" Time" ));
652- auto openTime = Time (openTimestamp);
653- auto diff = Time::getCurrentTime () - openTime;
654- String date;
655- if (diff.inDays () == 0 )
656- date = " Today" ;
657- else if (diff.inDays () == 1 )
658- date = " Yesterday" ;
659- else
660- date = openTime.toString (true , false );
661-
662- String time = getSystemLocalTime (openTimestamp);
663- String timeDescription = date + " , " + time;
739+ auto * tile = recentlyOpenedTiles.add (new WelcomePanelTile (*this , subTree, silhoutteSvg, snapshotColour, 1 .0f , favourited, thumbImage));
664740
665- auto * tile = recentlyOpenedTiles.add (new WelcomePanelTile (*this , patchFile.getFileNameWithoutExtension (), timeDescription, silhoutteSvg, snapshotColour, 1 .0f , favourited, thumbImage));
666741 tile->onClick = [this , patchFile]() mutable {
667742 if (patchFile.existsAsFile ()) {
668743 editor->pd ->autosave ->checkForMoreRecentAutosave (patchFile, editor, [this , patchFile]() {
@@ -683,6 +758,16 @@ class WelcomePanel : public Component
683758 subTree.setProperty (" Pinned" , shouldBeFavourite, nullptr );
684759 resized ();
685760 };
761+ tile->onRemove = [this , path = subTree.getProperty (" Path" )]() {
762+ auto settingsTree = SettingsFile::getInstance ()->getValueTree ();
763+ auto recentlyOpenedTree = settingsTree.getChildWithName (" RecentlyOpened" );
764+ auto subTree = recentlyOpenedTree.getChildWithProperty (" Path" , path);
765+ recentlyOpenedTree.removeChild (subTree, nullptr );
766+ // Make sure to clear the recent items in the current welcome panel
767+ if (editor->welcomePanel )
768+ editor->welcomePanel ->triggerAsyncUpdate ();
769+ };
770+
686771 contentComponent.addAndMakeVisible (tile);
687772 }
688773 }
0 commit comments