[vlc-commits] [Git][videolan/vlc][master] macosx: library: Add "Add to Playlist" submenu to context menu

Jean-Baptiste Kempf (@jbk) gitlab at videolan.org
Mon Jun 8 20:44:07 UTC 2026



Jean-Baptiste Kempf pushed to branch master at VideoLAN / VLC


Commits:
7248a57c by Serhii Bykov at 2026-06-08T22:33:19+02:00
macosx: library: Add "Add to Playlist" submenu to context menu

- - - - -


7 changed files:

- modules/gui/macosx/Makefile.am
- + modules/gui/macosx/library/VLCLibraryAddToPlaylistMenuController.h
- + modules/gui/macosx/library/VLCLibraryAddToPlaylistMenuController.m
- modules/gui/macosx/library/VLCLibraryController.m
- modules/gui/macosx/library/VLCLibraryDataTypes.h
- modules/gui/macosx/library/VLCLibraryDataTypes.m
- modules/gui/macosx/library/VLCLibraryMenuController.m


Changes:

=====================================
modules/gui/macosx/Makefile.am
=====================================
@@ -148,6 +148,8 @@ libmacosx_plugin_la_SOURCES = \
 	gui/macosx/library/VLCLibraryAbstractMediaLibrarySegmentViewController.m \
 	gui/macosx/library/VLCLibraryAbstractSegmentViewController.h \
 	gui/macosx/library/VLCLibraryAbstractSegmentViewController.m \
+	gui/macosx/library/VLCLibraryAddToPlaylistMenuController.h \
+	gui/macosx/library/VLCLibraryAddToPlaylistMenuController.m \
 	gui/macosx/library/VLCLibraryCarouselViewItemView.h \
 	gui/macosx/library/VLCLibraryCarouselViewItemView.m \
 	gui/macosx/library/VLCLibraryCollectionView.h \


=====================================
modules/gui/macosx/library/VLCLibraryAddToPlaylistMenuController.h
=====================================
@@ -0,0 +1,37 @@
+/*****************************************************************************
+ * VLCLibraryAddToPlaylistMenuController.h: MacOS X interface module
+ *****************************************************************************
+ * Copyright (C) 2026 VLC authors and VideoLAN
+ *
+ * Authors: Serhii Bykov <esphynox at gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+ *****************************************************************************/
+
+#import <Cocoa/Cocoa.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+ at class VLCLibraryRepresentedItem;
+
+ at interface VLCLibraryAddToPlaylistMenuController : NSObject
+
+ at property (readonly) NSMenu *addToPlaylistMenu;
+
+ at property (copy, nullable) NSArray<VLCLibraryRepresentedItem *> *representedItems;
+
+ at end
+
+NS_ASSUME_NONNULL_END


=====================================
modules/gui/macosx/library/VLCLibraryAddToPlaylistMenuController.m
=====================================
@@ -0,0 +1,141 @@
+/*****************************************************************************
+ * VLCLibraryAddToPlaylistMenuController.m: MacOS X interface module
+ *****************************************************************************
+ * Copyright (C) 2026 VLC authors and VideoLAN
+ *
+ * Authors: Serhii Bykov <esphynox at gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+ *****************************************************************************/
+
+#import "VLCLibraryAddToPlaylistMenuController.h"
+
+#import "extensions/NSString+Helpers.h"
+
+#import "library/VLCLibraryController.h"
+#import "library/VLCLibraryDataTypes.h"
+#import "library/VLCLibraryModel.h"
+#import "library/VLCLibraryRepresentedItem.h"
+
+#import "main/VLCMain.h"
+
+#import <vlc_media_library.h>
+
+ at interface VLCLibraryAddToPlaylistMenuController () <NSMenuDelegate>
+ at end
+
+ at implementation VLCLibraryAddToPlaylistMenuController
+
+- (instancetype)init
+{
+    self = [super init];
+    if (self) {
+        _addToPlaylistMenu = [[NSMenu alloc] initWithTitle:@""];
+        _addToPlaylistMenu.delegate = self;
+    }
+    return self;
+}
+
+#pragma mark - Helpers
+
+- (nullable NSArray<VLCMediaLibraryPlaylist *> *)writablePlaylists
+{
+    VLCLibraryModel * const libraryModel = VLCMain.sharedInstance.libraryController.libraryModel;
+    NSArray<VLCMediaLibraryPlaylist *> * const allPlaylists =
+        [libraryModel listOfPlaylistsOfType:VLC_ML_PLAYLIST_TYPE_ALL];
+    if (allPlaylists.count == 0) {
+        return nil;
+    }
+
+    NSMutableArray<VLCMediaLibraryPlaylist *> * const writable =
+        [NSMutableArray arrayWithCapacity:allPlaylists.count];
+    for (VLCMediaLibraryPlaylist * const playlist in allPlaylists) {
+        if (!playlist.readOnly) {
+            [writable addObject:playlist];
+        }
+    }
+
+    [writable sortUsingComparator:^NSComparisonResult(VLCMediaLibraryPlaylist * _Nonnull lhs,
+                                                      VLCMediaLibraryPlaylist * _Nonnull rhs) {
+        return [lhs.displayString caseInsensitiveCompare:rhs.displayString];
+    }];
+
+    return writable;
+}
+
+- (NSArray<VLCMediaLibraryMediaItem *> *)selectedMediaItems
+{
+    NSMutableArray<VLCMediaLibraryMediaItem *> * const mediaItems =
+        [NSMutableArray arrayWithCapacity:self.representedItems.count];
+    for (VLCLibraryRepresentedItem * const representedItem in self.representedItems) {
+        [mediaItems addObjectsFromArray:representedItem.item.mediaItems];
+    }
+    return mediaItems;
+}
+
+#pragma mark - NSMenuDelegate
+
+// Rebuilt on demand so newly created playlists appear without cache invalidation.
+- (void)menuNeedsUpdate:(NSMenu *)menu
+{
+    [menu removeAllItems];
+
+    NSMenuItem * const newPlaylistItem =
+        [[NSMenuItem alloc] initWithTitle:_NS("New Playlist...")
+                                   action:@selector(createNewPlaylist:)
+                            keyEquivalent:@""];
+    newPlaylistItem.target = self;
+    [menu addItem:newPlaylistItem];
+
+    NSArray<VLCMediaLibraryPlaylist *> * const playlists = [self writablePlaylists];
+    if (playlists.count == 0) {
+        return;
+    }
+
+    [menu addItem:NSMenuItem.separatorItem];
+
+    for (VLCMediaLibraryPlaylist * const playlist in playlists) {
+        NSMenuItem * const item = [[NSMenuItem alloc] initWithTitle:playlist.displayString
+                                                             action:@selector(addToSelectedPlaylist:)
+                                                      keyEquivalent:@""];
+        item.target = self;
+        item.representedObject = playlist;
+        [menu addItem:item];
+    }
+}
+
+#pragma mark - Actions
+
+- (void)createNewPlaylist:(NSMenuItem *)sender
+{
+    NSArray<VLCMediaLibraryMediaItem *> * const mediaItems = [self selectedMediaItems];
+    if (mediaItems.count == 0) {
+        return;
+    }
+
+    [VLCMain.sharedInstance.libraryController showCreatePlaylistDialogForMediaItems:mediaItems];
+}
+
+- (void)addToSelectedPlaylist:(NSMenuItem *)sender
+{
+    VLCMediaLibraryPlaylist * const playlist = sender.representedObject;
+    if (![playlist isKindOfClass:VLCMediaLibraryPlaylist.class]) {
+        return;
+    }
+
+    [playlist appendMediaItems:[self selectedMediaItems]];
+}
+
+ at end


=====================================
modules/gui/macosx/library/VLCLibraryController.m
=====================================
@@ -361,24 +361,20 @@ typedef int (*folder_action_f)(vlc_medialibrary_t*, const char*);
     if (!_p_libraryInstance || !playlistName || playlistName.length == 0) {
         return NO;
     }
-    
+
     vlc_ml_playlist_t * const playlist = vlc_ml_playlist_create(_p_libraryInstance, playlistName.UTF8String);
     if (!playlist) {
         msg_Err(getIntf(), "Failed to create playlist with name: %s", playlistName.UTF8String);
         return NO;
     }
-    
+
     const int64_t playlistId = playlist->i_id;
     vlc_ml_playlist_release(playlist);
-    
-    for (VLCMediaLibraryMediaItem * const mediaItem in mediaItems) {
-        const int64_t mediaId = mediaItem.libraryID;
-        const int result = vlc_ml_playlist_append(_p_libraryInstance, playlistId, &mediaId, 1);
-        if (result != VLC_SUCCESS) {
-            msg_Warn(getIntf(), "Failed to add media library item %lld to playlist", mediaItem.libraryID);
-        }
-    }
-    
+
+    VLCMediaLibraryPlaylist * const mlPlaylist =
+        [VLCMediaLibraryPlaylist playlistForLibraryID:playlistId];
+    [mlPlaylist appendMediaItems:mediaItems];
+
     return YES;
 }
 


=====================================
modules/gui/macosx/library/VLCLibraryDataTypes.h
=====================================
@@ -289,6 +289,7 @@ typedef NS_ENUM(NSUInteger, VLCMediaLibraryParentGroupType) {
 - (instancetype)initWithPlaylist:(const struct vlc_ml_playlist_t * const)p_playlist;
 
 - (void)removeMediaItemsAtPositions:(NSArray<NSNumber *> *)positions;
+- (BOOL)appendMediaItems:(NSArray<VLCMediaLibraryMediaItem *> *)mediaItems;
 
 @end
 


=====================================
modules/gui/macosx/library/VLCLibraryDataTypes.m
=====================================
@@ -1100,8 +1100,8 @@ static NSString *genreArrayDisplayString(NSArray<VLCMediaLibraryGenre *> * const
                                                   (uint32_t)range.location,
                                                   (uint32_t)range.length);
         if (result != VLC_SUCCESS) {
-            NSLog(@"Failed to remove %lu items at position %lu from playlist %lld",
-                  (unsigned long)range.length, (unsigned long)range.location, self.libraryID);
+            msg_Err(getIntf(), "Failed to remove %lu items at position %lu from playlist %lld",
+                    (unsigned long)range.length, (unsigned long)range.location, self.libraryID);
         }
     }
 }
@@ -1133,6 +1133,41 @@ static NSString *genreArrayDisplayString(NSArray<VLCMediaLibraryGenre *> * const
     return res;
 }
 
+- (BOOL)appendMediaItems:(NSArray<VLCMediaLibraryMediaItem *> *)mediaItems
+{
+    if (_readOnly || mediaItems.count == 0) {
+        return NO;
+    }
+
+    vlc_medialibrary_t * const p_mediaLibrary = getMediaLibrary();
+    if (p_mediaLibrary == NULL) {
+        return NO;
+    }
+
+    const size_t count = mediaItems.count;
+    int64_t * const mediaIDs = malloc(count * sizeof(int64_t));
+    if (mediaIDs == NULL) {
+        return NO;
+    }
+    for (size_t i = 0; i < count; ++i) {
+        mediaIDs[i] = mediaItems[i].libraryID;
+    }
+
+    const int result =
+        vlc_ml_playlist_append(p_mediaLibrary, self.libraryID, mediaIDs, count);
+    free(mediaIDs);
+
+    if (result != VLC_SUCCESS) {
+        msg_Err(getIntf(), "Failed to append %zu item(s) to playlist %s (ID %lld)",
+                count, self.displayString.UTF8String, self.libraryID);
+        return NO;
+    }
+
+    // Drop the cached media list so the next -mediaItems access re-fetches.
+    _mediaItems = nil;
+    return YES;
+}
+
 @end
 
 @interface VLCMediaLibraryMediaItem ()


=====================================
modules/gui/macosx/library/VLCLibraryMenuController.m
=====================================
@@ -27,6 +27,7 @@
 #import "extensions/NSString+Helpers.h"
 
 #import "library/VLCInputItem.h"
+#import "library/VLCLibraryAddToPlaylistMenuController.h"
 #import "library/VLCLibraryController.h"
 #import "library/VLCLibraryModel.h"
 #import "library/VLCLibraryRepresentedItem.h"
@@ -54,6 +55,9 @@
 
     NSMenuItem *_deleteItem;
     NSMenuItem *_removeFromPlaylistItem;
+
+    VLCLibraryAddToPlaylistMenuController *_addToPlaylistMenuController;
+    NSMenuItem *_addToPlaylistItem;
 }
 
 @property (readwrite) NSMenuItem *favoriteItem;
@@ -107,8 +111,11 @@
     _favoriteItem = [[NSMenuItem alloc] initWithTitle:_NS("Toggle Favorite") action:@selector(toggleFavorite:) keyEquivalent:@""];
     self.favoriteItem.target = self;
 
-    NSMenuItem *createPlaylistItem = [[NSMenuItem alloc] initWithTitle:_NS("Create Playlist from Selection") action:@selector(createPlaylistFromSelection:) keyEquivalent:@""];
-    createPlaylistItem.target = self;
+    _addToPlaylistMenuController = [[VLCLibraryAddToPlaylistMenuController alloc] init];
+    _addToPlaylistItem = [[NSMenuItem alloc] initWithTitle:_NS("Add to Playlist")
+                                                    action:nil
+                                             keyEquivalent:@""];
+    [_addToPlaylistItem setSubmenu:_addToPlaylistMenuController.addToPlaylistMenu];
 
     _removeFromPlaylistItem = [[NSMenuItem alloc] initWithTitle:_NS("Remove from Playlist")
                                                          action:@selector(removeFromPlaylist:)
@@ -117,7 +124,7 @@
 
     [playItem vlc_setActionImageWithSystemSymbolName:@"play.fill"];
     [appendItem vlc_setActionImageWithSystemSymbolName:@"text.line.last.and.arrowtriangle.forward"];
-    [createPlaylistItem vlc_setActionImageWithSystemSymbolName:@"music.note.list"];
+    [_addToPlaylistItem vlc_setActionImageWithSystemSymbolName:@"text.badge.plus"];
     [_removeFromPlaylistItem vlc_setActionImageWithSystemSymbolName:@"minus.circle"];
     [self.favoriteItem vlc_setActionImageWithSystemSymbolName:@"heart"];
     [bookmarkItem vlc_setActionImageWithSystemSymbolName:@"bookmark"];
@@ -132,7 +139,7 @@
     [_libraryMenu addMenuItemsFromArray:@[
         playItem,
         appendItem,
-        createPlaylistItem,
+        _addToPlaylistItem,
         self.favoriteItem,
         bookmarkItem,
         addToLibraryItem,
@@ -141,14 +148,14 @@
         _deleteItem,
         markUnseenItem,
         informationItem,
-        [NSMenuItem separatorItem], 
+        [NSMenuItem separatorItem],
         addItem
     ]];
 
     _mediaItemRequiringMenuItems = [NSHashTable weakObjectsHashTable];
     [_mediaItemRequiringMenuItems addObject:playItem];
     [_mediaItemRequiringMenuItems addObject:appendItem];
-    [_mediaItemRequiringMenuItems addObject:createPlaylistItem];
+    [_mediaItemRequiringMenuItems addObject:_addToPlaylistItem];
     [_mediaItemRequiringMenuItems addObject:self.favoriteItem];
     [_mediaItemRequiringMenuItems addObject:revealItem];
     [_mediaItemRequiringMenuItems addObject:_deleteItem];
@@ -323,22 +330,6 @@
     }
 }
 
-- (void)createPlaylistFromSelection:(id)sender
-{
-    if (self.representedItems == nil || self.representedItems.count == 0) {
-        return;
-    }
-    
-    NSMutableArray<VLCMediaLibraryMediaItem *> * const mediaItems = [NSMutableArray arrayWithCapacity:self.representedItems.count];
-    for (VLCLibraryRepresentedItem * const representedItem in self.representedItems) {
-        [mediaItems addObjectsFromArray:representedItem.item.mediaItems];
-    }
-    
-    if (mediaItems.count > 0) {
-        [VLCMain.sharedInstance.libraryController showCreatePlaylistDialogForMediaItems:mediaItems];
-    }
-}
-
 - (void)removeFromPlaylist:(id)sender
 {
     if (self.representedItems.count == 0) {
@@ -499,6 +490,7 @@
 {
     _representedItems = items;
     _representedInputItems = nil;
+    _addToPlaylistMenuController.representedItems = items;
     [self updateMenuItems];
 }
 
@@ -506,6 +498,7 @@
 {
     _representedInputItems = representedInputItems;
     _representedItems = nil;
+    _addToPlaylistMenuController.representedItems = nil;
     [self updateMenuItems];
 }
 



View it on GitLab: https://code.videolan.org/videolan/vlc/-/commit/7248a57ca0cc7011b1c79bdeabe9acecf059c104

-- 
View it on GitLab: https://code.videolan.org/videolan/vlc/-/commit/7248a57ca0cc7011b1c79bdeabe9acecf059c104
You're receiving this email because of your account on code.videolan.org. Manage all notifications: https://code.videolan.org/-/profile/notifications | Help: https://code.videolan.org/help




More information about the vlc-commits mailing list