[vlc-commits] [Git][videolan/vlc][master] macosx: library: add "Remove from Playlist" context menu action

Steve Lhomme (@robUx4) gitlab at videolan.org
Wed Jun 3 14:44:55 UTC 2026



Steve Lhomme pushed to branch master at VideoLAN / VLC


Commits:
d08ea6b6 by Serhii Bykov at 2026-06-03T13:56:44+00:00
macosx: library: add "Remove from Playlist" context menu action

- - - - -


9 changed files:

- modules/gui/macosx/library/VLCLibraryDataTypes.h
- modules/gui/macosx/library/VLCLibraryDataTypes.m
- modules/gui/macosx/library/VLCLibraryItemInternalMediaItemsDataSource.m
- modules/gui/macosx/library/VLCLibraryMenuController.m
- modules/gui/macosx/library/VLCLibraryRepresentedItem.h
- modules/gui/macosx/library/VLCLibraryRepresentedItem.m
- modules/gui/macosx/library/VLCLibraryTableView.m
- modules/gui/macosx/library/VLCLibraryTableViewDataSource.h
- modules/gui/macosx/library/playlist-library/VLCLibraryPlaylistDataSource.m


Changes:

=====================================
modules/gui/macosx/library/VLCLibraryDataTypes.h
=====================================
@@ -288,6 +288,8 @@ typedef NS_ENUM(NSUInteger, VLCMediaLibraryParentGroupType) {
 + (instancetype)playlistForLibraryID:(int64_t)libraryID;
 - (instancetype)initWithPlaylist:(const struct vlc_ml_playlist_t * const)p_playlist;
 
+- (void)removeMediaItemsAtPositions:(NSArray<NSNumber *> *)positions;
+
 @end
 
 @interface VLCMediaLibraryMediaItem : NSObject<VLCMediaLibraryItemProtocol, NSSecureCoding>


=====================================
modules/gui/macosx/library/VLCLibraryDataTypes.m
=====================================
@@ -1062,6 +1062,50 @@ static NSString *genreArrayDisplayString(NSArray<VLCMediaLibraryGenre *> * const
     }
 }
 
+- (void)removeMediaItemsAtPositions:(NSArray<NSNumber *> *)positions
+{
+    if (positions.count == 0) {
+        return;
+    }
+
+    vlc_medialibrary_t * const p_ml = getMediaLibrary();
+    if (p_ml == NULL) {
+        return;
+    }
+
+    NSArray<NSNumber *> * const sorted =
+        [positions sortedArrayUsingSelector:@selector(compare:)];
+
+    // Fold consecutive positions into (start, length) ranges
+    NSMutableArray<NSValue *> * const ranges = NSMutableArray.array;
+    NSUInteger rangeStart = sorted.firstObject.unsignedIntegerValue;
+    NSUInteger rangeLen = 1;
+    for (NSUInteger i = 1; i < sorted.count; i++) {
+        const NSUInteger pos = sorted[i].unsignedIntegerValue;
+        if (pos == rangeStart + rangeLen) {
+            rangeLen++;
+            continue;
+        }
+        [ranges addObject:[NSValue valueWithRange:NSMakeRange(rangeStart, rangeLen)]];
+        rangeStart = pos;
+        rangeLen = 1;
+    }
+    [ranges addObject:[NSValue valueWithRange:NSMakeRange(rangeStart, rangeLen)]];
+
+    // Descending order so removing one range does not shift earlier indices
+    for (NSValue * const value in ranges.reverseObjectEnumerator) {
+        const NSRange range = value.rangeValue;
+        const int result = vlc_ml_playlist_remove(p_ml,
+                                                  self.libraryID,
+                                                  (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);
+        }
+    }
+}
+
 - (void)revealInFinder
 {
     NSURL * const URL = [NSURL URLWithString:_MRL];


=====================================
modules/gui/macosx/library/VLCLibraryItemInternalMediaItemsDataSource.m
=====================================
@@ -99,7 +99,18 @@ const CGFloat VLCLibraryInternalMediaItemRowHeight = 40.;
 
 - (VLCMediaLibraryParentGroupType)currentParentType
 {
+    if ([self.internalItem isKindOfClass:VLCMediaLibraryPlaylist.class]) {
+        return VLCMediaLibraryParentGroupTypePlaylist;
+    }
     return VLCMediaLibraryParentGroupTypeAlbum;
 }
 
+- (id<VLCMediaLibraryItemProtocol>)parentItemForTableView:(NSTableView *)tableView
+{
+    if ([self.internalItem isKindOfClass:VLCMediaLibraryPlaylist.class]) {
+        return self.internalItem;
+    }
+    return nil;
+}
+
 @end


=====================================
modules/gui/macosx/library/VLCLibraryMenuController.m
=====================================
@@ -51,8 +51,9 @@
     NSHashTable<NSMenuItem*> *_inputItemRequiringMenuItems;
     NSHashTable<NSMenuItem*> *_localInputItemRequiringMenuItems;
     NSHashTable<NSMenuItem*> *_folderInputItemRequiringMenuItems;
-    
+
     NSMenuItem *_deleteItem;
+    NSMenuItem *_removeFromPlaylistItem;
 }
 
 @property (readwrite) NSMenuItem *favoriteItem;
@@ -109,9 +110,15 @@
     NSMenuItem *createPlaylistItem = [[NSMenuItem alloc] initWithTitle:_NS("Create Playlist from Selection") action:@selector(createPlaylistFromSelection:) keyEquivalent:@""];
     createPlaylistItem.target = self;
 
+    _removeFromPlaylistItem = [[NSMenuItem alloc] initWithTitle:_NS("Remove from Playlist")
+                                                         action:@selector(removeFromPlaylist:)
+                                                  keyEquivalent:@""];
+    _removeFromPlaylistItem.target = self;
+
     [playItem vlc_setActionImageWithSystemSymbolName:@"play.fill"];
     [appendItem vlc_setActionImageWithSystemSymbolName:@"text.line.last.and.arrowtriangle.forward"];
     [createPlaylistItem vlc_setActionImageWithSystemSymbolName:@"music.note.list"];
+    [_removeFromPlaylistItem vlc_setActionImageWithSystemSymbolName:@"minus.circle"];
     [self.favoriteItem vlc_setActionImageWithSystemSymbolName:@"heart"];
     [bookmarkItem vlc_setActionImageWithSystemSymbolName:@"bookmark"];
     [addToLibraryItem vlc_setActionImageWithSystemSymbolName:@"plus.rectangle.on.folder"];
@@ -130,6 +137,7 @@
         bookmarkItem,
         addToLibraryItem,
         revealItem,
+        _removeFromPlaylistItem,
         _deleteItem,
         markUnseenItem,
         informationItem,
@@ -175,12 +183,26 @@
     VLCLibraryModel * const libraryModel = VLCMain.sharedInstance.libraryController.libraryModel;
     NSArray<VLCMediaLibraryMediaItem *> * const recents = libraryModel.listOfRecentMedia;
 
+    _removeFromPlaylistItem.hidden = YES;
+
     if (self.representedItems != nil && self.representedItems.count > 0) {
         [self menuItems:_inputItemRequiringMenuItems setHidden:YES];
         [self menuItems:_localInputItemRequiringMenuItems setHidden:YES];
         [self menuItems:_folderInputItemRequiringMenuItems setHidden:YES];
         [self menuItems:_mediaItemRequiringMenuItems setHidden:NO];
 
+        BOOL allInPlaylist = YES;
+        for (VLCLibraryRepresentedItem * const item in self.representedItems) {
+            if (item.parentType != VLCMediaLibraryParentGroupTypePlaylist ||
+                ![item.item isKindOfClass:VLCMediaLibraryMediaItem.class] ||
+                item.parentItem == nil ||
+                item.positionInParent == NSNotFound) {
+                allInPlaylist = NO;
+                break;
+            }
+        }
+        _removeFromPlaylistItem.hidden = !allInPlaylist;
+
         BOOL anyNonRecent = NO;
         for (VLCLibraryRepresentedItem * const item in self.representedItems) {
             if ([recents indexOfObjectPassingTest:^BOOL(VLCMediaLibraryMediaItem * _Nonnull obj, NSUInteger __unused idx, BOOL * __unused stop) {
@@ -228,6 +250,7 @@
 
     } else if (_representedInputItems != nil && self.representedInputItems.count > 0) {
         [self menuItems:_mediaItemRequiringMenuItems setHidden:YES];
+        _removeFromPlaylistItem.hidden = YES;
         [self menuItems:_inputItemRequiringMenuItems setHidden:NO];
 
         BOOL anyStream = NO;
@@ -316,6 +339,29 @@
     }
 }
 
+- (void)removeFromPlaylist:(id)sender
+{
+    if (self.representedItems.count == 0) {
+        return;
+    }
+
+    VLCMediaLibraryPlaylist * const playlist =
+        (VLCMediaLibraryPlaylist *)self.representedItems.firstObject.parentItem;
+    if (![playlist isKindOfClass:VLCMediaLibraryPlaylist.class]) {
+        return;
+    }
+
+    NSMutableArray<NSNumber *> * const positions =
+        [NSMutableArray arrayWithCapacity:self.representedItems.count];
+    for (VLCLibraryRepresentedItem * const repItem in self.representedItems) {
+        if (repItem.positionInParent != NSNotFound) {
+            [positions addObject:@(repItem.positionInParent)];
+        }
+    }
+
+    [playlist removeMediaItemsAtPositions:positions];
+}
+
 - (void)addMedia:(id)sender
 {
     NSOpenPanel *openPanel = [NSOpenPanel openPanel];


=====================================
modules/gui/macosx/library/VLCLibraryRepresentedItem.h
=====================================
@@ -36,10 +36,18 @@ NS_ASSUME_NONNULL_BEGIN
 @property (readonly) NSArray<VLCMediaLibraryMediaItem *> *parentMediaArray;
 // If unknown, will always default to individual play mode
 @property (readonly) VLCMediaLibraryParentGroupType parentType;
+// Optional explicit parent (e.g. the containing playlist)
+ at property (readonly, nullable) id<VLCMediaLibraryItemProtocol> parentItem;
 @property (readonly) NSInteger itemIndexInParent;
+// Required for playlists where libraryID lookup collapses duplicate items
+ at property (readonly) NSInteger positionInParent;
 
 - (instancetype)initWithItem:(const id<VLCMediaLibraryItemProtocol>)item
                   parentType:(const VLCMediaLibraryParentGroupType)parentType;
+- (instancetype)initWithItem:(const id<VLCMediaLibraryItemProtocol>)item
+                  parentType:(const VLCMediaLibraryParentGroupType)parentType
+                  parentItem:(nullable const id<VLCMediaLibraryItemProtocol>)parentItem
+            positionInParent:(NSInteger)positionInParent;
 
 - (void)play;
 - (void)queue;


=====================================
modules/gui/macosx/library/VLCLibraryRepresentedItem.m
=====================================
@@ -49,11 +49,21 @@
 
 - (instancetype)initWithItem:(const id<VLCMediaLibraryItemProtocol>)item
                   parentType:(const VLCMediaLibraryParentGroupType)parentType
+{
+    return [self initWithItem:item parentType:parentType parentItem:nil positionInParent:NSNotFound];
+}
+
+- (instancetype)initWithItem:(const id<VLCMediaLibraryItemProtocol>)item
+                  parentType:(const VLCMediaLibraryParentGroupType)parentType
+                  parentItem:(nullable const id<VLCMediaLibraryItemProtocol>)parentItem
+            positionInParent:(NSInteger)positionInParent
 {
     self = [self init];
     if (self) {
         _item = item;
         _parentType = parentType;
+        _parentItem = parentItem;
+        _positionInParent = positionInParent;
         _mediaType = item.firstMediaItem.mediaType;
     }
     return self;
@@ -71,10 +81,15 @@
 - (void)setup
 {
     _itemIndexInParent = NSNotFound;
+    _positionInParent = NSNotFound;
 }
 
 - (NSInteger)itemIndexInParent
 {
+    if (_parentItem != nil && _positionInParent != NSNotFound) {
+        return _positionInParent;
+    }
+
     @synchronized(self) {
         if (_itemIndexInParent == NSNotFound) {
             _itemIndexInParent = [self findItemIndexInParent];
@@ -88,7 +103,9 @@
 {
     NSArray<VLCMediaLibraryMediaItem *> * items = nil;
 
-    if (self.parentType == VLCMediaLibraryParentGroupTypeUnknown) {
+    if (self.parentItem != nil) {
+        items = self.parentItem.mediaItems;
+    } else if (self.parentType == VLCMediaLibraryParentGroupTypeUnknown) {
         VLCLibraryModel * const libraryModel = VLCMain.sharedInstance.libraryController.libraryModel;
         const BOOL isVideo = self.mediaType == VLC_ML_MEDIA_TYPE_VIDEO;
         items = isVideo ? libraryModel.listOfVideoMedia : libraryModel.listOfAudioMedia;
@@ -192,6 +209,10 @@
 
 - (NSArray<VLCMediaLibraryMediaItem *> *)parentMediaArrayForItem:(const id<VLCMediaLibraryItemProtocol>)item
 {
+    if (self.parentItem != nil) {
+        return self.parentItem.mediaItems;
+    }
+
     if (self.parentType == VLCMediaLibraryParentGroupTypeAllFavorites) {
         VLCLibraryModel * const libraryModel = VLCMain.sharedInstance.libraryController.libraryModel;
         return [libraryModel listOfMediaItemsForParentType:self.parentType];


=====================================
modules/gui/macosx/library/VLCLibraryTableView.m
=====================================
@@ -91,26 +91,32 @@
             NSMutableArray.array;
         const id<VLCLibraryTableViewDataSource> vlcLibraryDataSource = 
             (id<VLCLibraryTableViewDataSource>)dataSource;
+        const VLCMediaLibraryParentGroupType parentType =
+            vlcLibraryDataSource.currentParentType;
+        const id<VLCMediaLibraryItemProtocol> parentItem =
+            [vlcLibraryDataSource respondsToSelector:@selector(parentItemForTableView:)]
+                ? [vlcLibraryDataSource parentItemForTableView:self]
+                : nil;
 
         if ([indices containsIndex:clickedRow]) {
             [indices enumerateIndexesUsingBlock:^(const NSUInteger index, BOOL * const __unused stop) {
                 const id<VLCMediaLibraryItemProtocol> mediaItem =
                     [vlcLibraryDataSource libraryItemAtRow:index forTableView:self];
-                const VLCMediaLibraryParentGroupType parentType =
-                    vlcLibraryDataSource.currentParentType;
                 VLCLibraryRepresentedItem * const representedItem =
                     [[VLCLibraryRepresentedItem alloc] initWithItem:mediaItem
-                                                         parentType:parentType];
+                                                         parentType:parentType
+                                                         parentItem:parentItem
+                                                   positionInParent:(NSInteger)index];
                 [representedItems addObject:representedItem];
             }];
         } else {
             const id<VLCMediaLibraryItemProtocol> mediaItem = 
                 [vlcLibraryDataSource libraryItemAtRow:clickedRow forTableView:self];
-            const VLCMediaLibraryParentGroupType parentType = 
-                vlcLibraryDataSource.currentParentType;
             VLCLibraryRepresentedItem * const representedItem = 
                 [[VLCLibraryRepresentedItem alloc] initWithItem:mediaItem
-                                                     parentType:parentType];
+                                                     parentType:parentType
+                                                     parentItem:parentItem
+                                               positionInParent:clickedRow];
             [representedItems addObject:representedItem];
         }
 


=====================================
modules/gui/macosx/library/VLCLibraryTableViewDataSource.h
=====================================
@@ -35,6 +35,9 @@ NS_ASSUME_NONNULL_BEGIN
 - (id<VLCMediaLibraryItemProtocol>)libraryItemAtRow:(NSInteger)row
                                        forTableView:(nullable NSTableView *)tableView;
 
+ at optional
+- (nullable id<VLCMediaLibraryItemProtocol>)parentItemForTableView:(NSTableView *)tableView;
+
 @end
 
 NS_ASSUME_NONNULL_END


=====================================
modules/gui/macosx/library/playlist-library/VLCLibraryPlaylistDataSource.m
=====================================
@@ -142,20 +142,33 @@ typedef NS_ENUM(NSInteger, VLCLibraryDataSourceCacheAction) {
 - (void)reloadViewsAtIndex:(NSUInteger)index
           dueToCacheAction:(VLCLibraryDataSourceCacheAction)action
 {
+    NSTableView * const masterTableView = self.masterTableView;
+    NSTableView * const detailTableView = self.detailTableView;
     NSIndexSet * const indexSet = [NSIndexSet indexSetWithIndex:index];
+    const NSInteger selectedMasterRow = masterTableView.selectedRow;
+    const BOOL affectsSelectedDetail =
+        detailTableView != nil && (NSInteger)index == selectedMasterRow;
+
     switch (action) {
-        case VLCLibraryDataSourceCacheUpdateAction:
-            [self.masterTableView reloadDataForRowIndexes:indexSet
-                                            columnIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, self.masterTableView.numberOfColumns)]];
+        case VLCLibraryDataSourceCacheUpdateAction: {
+            NSIndexSet * const columnSet =
+                [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, masterTableView.numberOfColumns)];
+            [masterTableView reloadDataForRowIndexes:indexSet columnIndexes:columnSet];
             break;
+        }
         case VLCLibraryDataSourceCacheDeleteAction:
-            [self.masterTableView removeRowsAtIndexes:indexSet
-                                       withAnimation:NSTableViewAnimationEffectNone];
+            [masterTableView removeRowsAtIndexes:indexSet
+                                   withAnimation:NSTableViewAnimationEffectNone];
             break;
         default:
             NSAssert(false, @"Invalid playlist cache action");
     }
 
+    if (affectsSelectedDetail) {
+        [detailTableView reloadData];
+        [self updateHeaderInTableView:detailTableView forMasterSelection:masterTableView];
+    }
+
     NSIndexPath * const indexPath = [NSIndexPath indexPathForItem:index inSection:0];
     NSSet<NSIndexPath *> * const indexPathSet = [NSSet setWithObject:indexPath];
 
@@ -383,6 +396,20 @@ viewForSupplementaryElementOfKind:(NSCollectionViewSupplementaryElementKind)kind
     return VLCMediaLibraryParentGroupTypePlaylist;
 }
 
+- (id<VLCMediaLibraryItemProtocol>)parentItemForTableView:(NSTableView *)tableView
+{
+    if (tableView != self.detailTableView) {
+        return nil;
+    }
+
+    const NSInteger selectedRow = self.masterTableView.selectedRow;
+    if (selectedRow < 0 || selectedRow >= self.playlists.count) {
+        return nil;
+    }
+
+    return self.playlists[selectedRow];
+}
+
 - (NSString *)supplementaryDetailViewKind
 {
     return VLCLibraryCollectionViewMediaItemListSupplementaryDetailViewKind;



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

-- 
View it on GitLab: https://code.videolan.org/videolan/vlc/-/commit/d08ea6b6203010f5af53adce84cb0b4e8c8f20bb
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