[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