[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