[vlc-commits] [Git][videolan/vlc][master] 11 commits: macosx: Add convenience method to get file type description
Felix Paul Kühne (@fkuehne)
gitlab at videolan.org
Tue Oct 13 07:06:52 UTC 2026
Felix Paul Kühne pushed to branch master at VideoLAN / VLC
Commits:
3c9e1cbf by Claudio Cambra at 2026-10-13T08:37:26+02:00
macosx: Add convenience method to get file type description
Signed-off-by: Claudio Cambra <developer at claudiocambra.com>
- - - - -
240cf026 by Claudio Cambra at 2026-10-13T08:37:26+02:00
macosx: Add input type description string property to VLCInputItem
Signed-off-by: Claudio Cambra <developer at claudiocambra.com>
- - - - -
12fd9571 by Claudio Cambra at 2026-10-13T08:37:26+02:00
macosx: Add VLCMediaSource category description string property
Signed-off-by: Claudio Cambra <developer at claudiocambra.com>
- - - - -
f7d54151 by Claudio Cambra at 2026-10-13T08:37:26+02:00
macosx: Add test for new file type description nsstring method
Signed-off-by: Claudio Cambra <developer at claudiocambra.com>
- - - - -
77338ed5 by Claudio Cambra at 2026-10-13T08:37:26+02:00
macosx: Define column and sort identifiers in base data source
Signed-off-by: Claudio Cambra <developer at claudiocambra.com>
- - - - -
99efea71 by Claudio Cambra at 2026-10-13T08:37:26+02:00
macosx: Simplify media source base data source's viewForTableColumn
Signed-off-by: Claudio Cambra <developer at claudiocambra.com>
- - - - -
94b26700 by Claudio Cambra at 2026-10-13T08:37:26+02:00
macosx: Simplify media source data source's viewForTableColumn
Signed-off-by: Claudio Cambra <developer at claudiocambra.com>
- - - - -
3c984cb1 by Claudio Cambra at 2026-10-13T08:37:26+02:00
macosx: Add media source table view sorting helper class
Signed-off-by: Claudio Cambra <developer at claudiocambra.com>
- - - - -
e50d4f74 by Claudio Cambra at 2026-10-13T08:37:26+02:00
macosx: Implement sorting for list view in the media source base data source
Signed-off-by: Claudio Cambra <developer at claudiocambra.com>
- - - - -
07f3f31c by Claudio Cambra at 2026-10-13T08:37:26+02:00
macosx: Implement sorting for list view in the media source data source
Signed-off-by: Claudio Cambra <developer at claudiocambra.com>
- - - - -
716e856c by Claudio Cambra at 2026-10-13T08:37:26+02:00
macosx: Add test suite for VLCMediaSourceTableViewSorting
Signed-off-by: Claudio Cambra <developer at claudiocambra.com>
- - - - -
17 changed files:
- extras/package/macosx/VLC.xcodeproj/project.pbxproj
- modules/gui/macosx/Makefile.am
- modules/gui/macosx/extensions/NSString+Helpers.h
- modules/gui/macosx/extensions/NSString+Helpers.m
- modules/gui/macosx/library/VLCInputItem.h
- modules/gui/macosx/library/VLCInputItem.m
- modules/gui/macosx/library/media-source/VLCMediaSource.h
- modules/gui/macosx/library/media-source/VLCMediaSource.m
- modules/gui/macosx/library/media-source/VLCMediaSourceBaseDataSource.h
- modules/gui/macosx/library/media-source/VLCMediaSourceBaseDataSource.m
- modules/gui/macosx/library/media-source/VLCMediaSourceDataSource.h
- modules/gui/macosx/library/media-source/VLCMediaSourceDataSource.m
- + modules/gui/macosx/library/media-source/VLCMediaSourceTableViewSorting.h
- + modules/gui/macosx/library/media-source/VLCMediaSourceTableViewSorting.m
- modules/gui/macosx/tests/Makefile.am
- modules/gui/macosx/tests/NSStringHelpersTest.m
- + modules/gui/macosx/tests/VLCMediaSourceTableViewSortingTest.m
Changes:
=====================================
extras/package/macosx/VLC.xcodeproj/project.pbxproj
=====================================
@@ -289,6 +289,7 @@
CBA75925CFF3CDBA090978F9 /* VLCConnectToServerDialog.m in Sources */ = {isa = PBXBuildFile; fileRef = BD7C30D642B4C89C9F539A27 /* VLCConnectToServerDialog.m */; };
D5E843223C3B2003665D0392 /* VLCLibrarySearchViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BAD0D16D79A606065692135 /* VLCLibrarySearchViewController.m */; };
EAA945B2B98EEA0417A2BE39 /* NSMenuItem+VLCAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = CC1F1AD9B6357DC114DCF464 /* NSMenuItem+VLCAdditions.m */; };
+ F905D8ED29CD4DF0B8A46B24 /* VLCMediaSourceTableViewSorting.m in Sources */ = {isa = PBXBuildFile; fileRef = 499CEBBD50D8423086BD3682 /* VLCMediaSourceTableViewSorting.m */; };
FDF2CACBF02A94F2ED2E0CE3 /* NSAnimationContext+VLCAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = F42980861A7BC3BDB5546FB5 /* NSAnimationContext+VLCAdditions.m */; };
/* End PBXBuildFile section */
@@ -336,6 +337,7 @@
3D46D70C2E1C023F00741E96 /* VLCLibraryRatingIndicator.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = VLCLibraryRatingIndicator.m; sourceTree = "<group>"; };
45C012BF53A7082827B907FA /* VLCConnectToServerDialog.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VLCConnectToServerDialog.h; sourceTree = "<group>"; };
4618AD10A0A0997054DE8AD9 /* VLCLibrarySearchProvider.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = VLCLibrarySearchProvider.m; sourceTree = "<group>"; };
+ 499CEBBD50D8423086BD3682 /* VLCMediaSourceTableViewSorting.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = VLCMediaSourceTableViewSorting.m; sourceTree = "<group>"; };
4B8B3AAE11FC272D811B71EC /* VLCLibraryGroupHeaderDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VLCLibraryGroupHeaderDelegate.h; sourceTree = "<group>"; };
4DEC6E8A21C184846D985A69 /* VLCLibraryNameCache.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = VLCLibraryNameCache.m; sourceTree = "<group>"; };
50EC718DBC26E627D0539815 /* NSAnimationContext+VLCAdditions.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSAnimationContext+VLCAdditions.h"; sourceTree = "<group>"; };
@@ -577,6 +579,7 @@
5CCED71314C0D4A90057F8D1 /* VLCExtensionsManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VLCExtensionsManager.m; sourceTree = "<group>"; };
5CCED71414C0D4A90057F8D1 /* VLCUIWidgets.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VLCUIWidgets.h; sourceTree = "<group>"; };
5CCED71514C0D4A90057F8D1 /* VLCUIWidgets.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VLCUIWidgets.m; sourceTree = "<group>"; };
+ 5E9D41F61DDE4F2D8A16D6EE /* VLCMediaSourceTableViewSorting.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VLCMediaSourceTableViewSorting.h; sourceTree = "<group>"; };
5EA1F8D351D1520B21E49E57 /* VLCUIUnits.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VLCUIUnits.h; sourceTree = "<group>"; };
633121CA1B51122700E636DA /* VLCResumeDialogController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VLCResumeDialogController.h; sourceTree = "<group>"; };
633121CB1B51122700E636DA /* VLCResumeDialogController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VLCResumeDialogController.m; sourceTree = "<group>"; };
@@ -2153,6 +2156,8 @@
7D2554C322C8E8B400F2D9C2 /* VLCMediaSourceDeviceCollectionViewItem.m */,
7DFBDCB9226CED6300B700A5 /* VLCMediaSourceProvider.h */,
7DFBDCBA226CED6300B700A5 /* VLCMediaSourceProvider.m */,
+ 5E9D41F61DDE4F2D8A16D6EE /* VLCMediaSourceTableViewSorting.h */,
+ 499CEBBD50D8423086BD3682 /* VLCMediaSourceTableViewSorting.m */,
);
path = "media-source";
sourceTree = "<group>";
@@ -2553,6 +2558,7 @@
1C3113AD1E508C6900D4DD76 /* VLCExtensionsManager.m in Sources */,
53A8F9D42A7F815900BC11BF /* VLCLibraryPlaylistDataSource.m in Sources */,
7DFBDCBB226CED6300B700A5 /* VLCMediaSourceProvider.m in Sources */,
+ F905D8ED29CD4DF0B8A46B24 /* VLCMediaSourceTableViewSorting.m in Sources */,
7D66D4362200BC340040D04A /* VLCClickerManager.m in Sources */,
535053EE2B610C3400D7EAF2 /* VLCLoadingOverlayView.m in Sources */,
53B447F9293BB47B00857588 /* VLCLibraryVideoDataSource.m in Sources */,
=====================================
modules/gui/macosx/Makefile.am
=====================================
@@ -346,6 +346,8 @@ libmacosx_plugin_la_SOURCES = \
gui/macosx/library/media-source/VLCLibraryMediaSourceViewNavigationState.m \
gui/macosx/library/media-source/VLCMediaSourceProvider.h \
gui/macosx/library/media-source/VLCMediaSourceProvider.m \
+ gui/macosx/library/media-source/VLCMediaSourceTableViewSorting.h \
+ gui/macosx/library/media-source/VLCMediaSourceTableViewSorting.m \
gui/macosx/library/playlist-library/VLCLibraryPlaylistDataSource.h \
gui/macosx/library/playlist-library/VLCLibraryPlaylistDataSource.m \
gui/macosx/library/playlist-library/VLCLibraryPlaylistViewController.h \
=====================================
modules/gui/macosx/extensions/NSString+Helpers.h
=====================================
@@ -183,6 +183,11 @@ bool fixIntfSettings(void);
*/
- (nullable NSString *)stringWithIncrementedTrailingNumber;
+/**
+ Returns a description of the receiver's file extension, or "File" if it has none.
+ */
+- (NSString *)fileTypeDescription;
+
/**
* Returns an array of file extensions from a semicolon-separated string of extensions,
* stripping any leading "*." prefix.
=====================================
modules/gui/macosx/extensions/NSString+Helpers.m
=====================================
@@ -34,6 +34,7 @@
#import <sys/param.h>
#import <sys/mount.h>
+#import <CoreServices/CoreServices.h>
#import <IOKit/storage/IOMedia.h>
#import <IOKit/storage/IOCDMedia.h>
#import <IOKit/storage/IODVDMedia.h>
@@ -278,6 +279,27 @@ NSString * flagEmojiStringForCountryCode(NSString *countryCode)
return result.copy;
}
+- (NSString *)fileTypeDescription
+{
+ NSString * const extension = self.pathExtension.lowercaseString;
+ if (extension.length == 0) {
+ return NSTR("File");
+ }
+
+ const CFStringRef uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension,
+ (__bridge CFStringRef)extension,
+ NULL);
+ if (uti != NULL) {
+ const CFStringRef description = UTTypeCopyDescription(uti);
+ CFRelease(uti);
+ if (description != NULL) {
+ return CFBridgingRelease(description);
+ }
+ }
+
+ return [NSString stringWithFormat:@"%@ File", extension.capitalizedString];
+}
+
+ (NSArray<NSString *> *)extensionsArrayFromVLCStyleString:(const char *)extensionsString
{
if (extensionsString == NULL) {
=====================================
modules/gui/macosx/library/VLCInputItem.h
=====================================
@@ -72,6 +72,7 @@ extern NSString * const VLCInputItemCommonDataDifferingFlagString;
@property (readonly) NSString *totalNumberOfDiscs;
@property (readonly) vlc_tick_t duration;
@property (readonly) enum input_item_type_e inputType;
+ at property (readonly) NSString *inputTypeDescription;
@property (readonly) struct input_item_node_t *subTree;
@property (readwrite) NSURL *artworkURL;
@property (readonly) BOOL preparsed;
=====================================
modules/gui/macosx/library/VLCInputItem.m
=====================================
@@ -358,6 +358,31 @@ static const struct input_item_parser_cbs_t parserCallbacks =
return _vlcInputItem->i_type;
}
+- (NSString *)inputTypeDescription
+{
+ switch (self.inputType) {
+ case ITEM_TYPE_UNKNOWN:
+ return NSTR("Unknown");
+ case ITEM_TYPE_FILE:
+ return self.MRL.fileTypeDescription;
+ case ITEM_TYPE_DIRECTORY:
+ return NSTR("Directory");
+ case ITEM_TYPE_DISC:
+ return NSTR("Disc");
+ case ITEM_TYPE_CARD:
+ return NSTR("Card");
+ case ITEM_TYPE_STREAM:
+ return NSTR("Stream");
+ case ITEM_TYPE_PLAYLIST:
+ return NSTR("Playlist");
+ case ITEM_TYPE_NODE:
+ return NSTR("Node");
+ case ITEM_TYPE_NUMBER:
+ return NSTR("Undefined");
+ }
+ return NSTR("Unknown");
+}
+
- (NSURL *)artworkURL
{
char *p_artworkURL = input_item_GetArtworkURL(_vlcInputItem);
=====================================
modules/gui/macosx/library/media-source/VLCMediaSource.h
=====================================
@@ -63,6 +63,7 @@ extern NSString * const VLCMediaSourcePreparseStatusKey;
@property (nonatomic, readonly) NSString *mediaSourceDescription;
@property (nonatomic, readonly) VLCInputNode *rootNode;
@property (nonatomic, readonly) enum services_discovery_category_e category;
+ at property (nonatomic, readonly) NSString *categoryDescription;
@property (readwrite) void (^willStartGeneratingChildNodesForNodeHandler)(input_item_node_t *);
@property (readwrite) void (^didFinishGeneratingChildNodesForNodeHandler)(input_item_node_t *);
=====================================
modules/gui/macosx/library/media-source/VLCMediaSource.m
=====================================
@@ -616,6 +616,22 @@ static const char *const remoteBrowseDescription = "Remote Browse";
return inputNode;
}
+- (NSString *)categoryDescription
+{
+ switch (self.category) {
+ case SD_CAT_DEVICES:
+ return _NS("Devices");
+ case SD_CAT_LAN:
+ return _NS("LAN");
+ case SD_CAT_INTERNET:
+ return _NS("Internet");
+ case SD_CAT_MYCOMPUTER:
+ return _NS("My Computer");
+ default:
+ return @"";
+ }
+}
+
#pragma mark - Local Media Items Methods
- (void)volumeIsMounted:(NSNotification *)aNotification
{
=====================================
modules/gui/macosx/library/media-source/VLCMediaSourceBaseDataSource.h
=====================================
@@ -38,6 +38,16 @@ typedef NS_ENUM(NSInteger, VLCMediaSourceMode) {
extern NSString * const VLCMediaSourceBaseDataSourceNodeChanged;
+extern NSString * const VLCMediaSourceTableNameColumnIdentifier;
+extern NSString * const VLCMediaSourceTableCountColumnIdentifier;
+extern NSString * const VLCMediaSourceTableKindColumnIdentifier;
+extern NSString * const VLCMediaSourceTableTagsColumnIdentifier;
+
+extern NSString * const VLCMediaSourceTableNameSortDescriptorKey;
+extern NSString * const VLCMediaSourceTableCountSortDescriptorKey;
+extern NSString * const VLCMediaSourceTableKindSortDescriptorKey;
+extern NSString * const VLCMediaSourceTableTagsSortDescriptorKey;
+
@interface VLCMediaSourceBaseDataSource : NSObject <NSCollectionViewDataSource,
NSCollectionViewDelegate,
NSCollectionViewDelegateFlowLayout,
=====================================
modules/gui/macosx/library/media-source/VLCMediaSourceBaseDataSource.m
=====================================
@@ -28,6 +28,7 @@
#import "VLCMediaSourceDataSource.h"
#import "VLCMediaSourceDeviceCollectionViewItem.h"
#import "VLCMediaSourceProvider.h"
+#import "VLCMediaSourceTableViewSorting.h"
#import "extensions/NSImage+VLCAdditions.h"
#import "extensions/NSPasteboardItem+VLCAdditions.h"
@@ -54,8 +55,17 @@
#import "views/VLCUIUnits.h"
NSString * const VLCMediaSourceBaseDataSourceNodeChanged = @"VLCMediaSourceBaseDataSourceNodeChanged";
+
+NSString * const VLCMediaSourceTableNameColumnIdentifier = @"VLCMediaSourceTableNameColumn";
+NSString * const VLCMediaSourceTableCountColumnIdentifier = @"VLCMediaSourceTableCountColumn";
+NSString * const VLCMediaSourceTableKindColumnIdentifier = @"VLCMediaSourceTableKindColumn";
NSString * const VLCMediaSourceTableTagsColumnIdentifier = @"VLCMediaSourceTableTagsColumn";
+NSString * const VLCMediaSourceTableNameSortDescriptorKey = @"name";
+NSString * const VLCMediaSourceTableCountSortDescriptorKey = @"childCount";
+NSString * const VLCMediaSourceTableKindSortDescriptorKey = @"kind";
+NSString * const VLCMediaSourceTableTagsSortDescriptorKey = @"tags";
+
@interface VLCLANDeviceRecord : NSObject
@property (readonly) VLCMediaSource *mediaSource;
@property (readonly) VLCInputNode *inputNode;
@@ -83,8 +93,13 @@ NSString * const VLCMediaSourceTableTagsColumnIdentifier = @"VLCMediaSourceTable
{
NSArray<VLCMediaSource *> *_mediaSources;
NSArray<NSString *> *_mediaSourceNotificationNames;
- NSMapTable<VLCMediaSource *, NSArray<VLCLANDeviceRecord *> *> *_sourceRecords;
- NSArray<VLCLANDeviceRecord *> *_lanDeviceSnapshot;
+ // LAN device records grouped by source, updated independently.
+ NSMapTable<VLCMediaSource *, NSArray<VLCLANDeviceRecord *> *> *_deviceRecordsBySource;
+ // Flattened discovery records in Browse table sort order.
+ NSArray<VLCLANDeviceRecord *> *_tableViewDevices;
+ // Internet providers in Streams table sort order; grid order remains in _mediaSources.
+ NSArray<VLCMediaSource *> *_tableViewSources;
+ BOOL _ignoringTableViewSelectionChanges;
}
@end
@@ -95,17 +110,18 @@ NSString * const VLCMediaSourceTableTagsColumnIdentifier = @"VLCMediaSourceTable
self = [super init];
if (self) {
_mediaSources = @[];
- _lanDeviceSnapshot = @[];
+ _tableViewDevices = @[];
+ _tableViewSources = @[];
_mediaSourceNotificationNames = @[
VLCMediaSourceChildrenReset,
VLCMediaSourceChildrenAdded,
VLCMediaSourceChildrenRemoved,
VLCMediaSourcePreparsingEnded
];
- _sourceRecords = [NSMapTable mapTableWithKeyOptions:NSPointerFunctionsWeakMemory |
- NSPointerFunctionsObjectPointerPersonality
- valueOptions:NSPointerFunctionsStrongMemory |
- NSPointerFunctionsObjectPersonality];
+ _deviceRecordsBySource = [NSMapTable mapTableWithKeyOptions:NSPointerFunctionsWeakMemory |
+ NSPointerFunctionsObjectPointerPersonality
+ valueOptions:NSPointerFunctionsStrongMemory |
+ NSPointerFunctionsObjectPersonality];
_mediaSourceMode = VLCMediaSourceModeLAN;
[self loadMediaSources];
[self returnHome];
@@ -164,10 +180,30 @@ NSString * const VLCMediaSourceTableTagsColumnIdentifier = @"VLCMediaSourceTable
NSNib * const tableCellViewNib = [[NSNib alloc] initWithNibNamed:NSStringFromClass(VLCLibraryTableCellView.class) bundle:nil];
[self.tableView registerNib:tableCellViewNib forIdentifier:VLCLibraryTableCellViewIdentifier];
+ [self setupTableViewSorting];
[self updateTableColumnVisibility];
[self reloadViews];
}
+- (void)setupTableViewSorting
+{
+ NSDictionary<NSString *, NSString *> * const keysByColumn = @{
+ VLCMediaSourceTableNameColumnIdentifier: VLCMediaSourceTableNameSortDescriptorKey,
+ VLCMediaSourceTableCountColumnIdentifier: VLCMediaSourceTableCountSortDescriptorKey,
+ VLCMediaSourceTableKindColumnIdentifier: VLCMediaSourceTableKindSortDescriptorKey,
+ VLCMediaSourceTableTagsColumnIdentifier: VLCMediaSourceTableTagsSortDescriptorKey
+ };
+ for (NSTableColumn * const column in self.tableView.tableColumns) {
+ NSString * const key = keysByColumn[column.identifier];
+ NSAssert(key != nil, @"Unexpected media source table column: %@", column.identifier);
+ const SEL selector = [key isEqualToString:VLCMediaSourceTableCountSortDescriptorKey]
+ ? @selector(compare:) : @selector(localizedStandardCompare:);
+ column.sortDescriptorPrototype = [NSSortDescriptor sortDescriptorWithKey:key
+ ascending:YES
+ selector:selector];
+ }
+}
+
- (void)updateTableColumnVisibility
{
NSTableColumn * const tagsColumn =
@@ -177,6 +213,7 @@ NSString * const VLCMediaSourceTableTagsColumnIdentifier = @"VLCMediaSourceTable
- (void)reloadViews
{
+ [self updateTableViewItems];
const VLCLibraryViewModeSegment viewModeSegment = self.viewMode;
if (viewModeSegment == VLCLibraryGridViewModeSegment) {
self.collectionViewScrollView.hidden = NO;
@@ -185,7 +222,11 @@ NSString * const VLCMediaSourceTableTagsColumnIdentifier = @"VLCMediaSourceTable
} else if (viewModeSegment == VLCLibraryListViewModeSegment) {
self.collectionViewScrollView.hidden = YES;
self.tableViewScrollView.hidden = NO;
- [self.tableView reloadData];
+ if (self.childDataSource != nil) {
+ [self.childDataSource reloadData];
+ } else {
+ [self.tableView reloadData];
+ }
} else {
NSAssert(false, @"View mode must be grid or list mode");
}
@@ -214,7 +255,7 @@ NSString * const VLCMediaSourceTableTagsColumnIdentifier = @"VLCMediaSourceTable
}
[self setMediaSources:mediaSources];
- _lanDeviceSnapshot = self.mediaSourceMode == VLCMediaSourceModeLAN ? [self buildMediaSourceSnapshot] : @[];
+ [self rebuildLANDeviceRecords];
[self reloadData];
}
@@ -268,7 +309,7 @@ NSString * const VLCMediaSourceTableTagsColumnIdentifier = @"VLCMediaSourceTable
}
if (_mediaSourceMode == VLCMediaSourceModeLAN) {
- return _lanDeviceSnapshot.count > 0;
+ return _tableViewDevices.count > 0;
}
return _mediaSources.count > 0;
@@ -440,22 +481,22 @@ referenceSizeForHeaderInSection:(NSInteger)section
- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView
{
if (_mediaSourceMode == VLCMediaSourceModeLAN) {
- return _lanDeviceSnapshot.count;
+ return _tableViewDevices.count;
}
- return _mediaSources.count;
+ return _tableViewSources.count;
}
- (NSView *)tableView:(NSTableView *)tableView
viewForTableColumn:(NSTableColumn *)tableColumn
row:(NSInteger)row
{
- if ([tableColumn.identifier isEqualToString:@"VLCMediaSourceTableNameColumn"]) {
+ if ([tableColumn.identifier isEqualToString:VLCMediaSourceTableNameColumnIdentifier]) {
VLCLibraryTableCellView * const cellView =
[tableView makeViewWithIdentifier:VLCLibraryTableCellViewIdentifier owner:self];
if (_mediaSourceMode == VLCMediaSourceModeLAN) {
- VLCInputItem * const currentNodeInput = _lanDeviceSnapshot[row].inputNode.inputItem;
+ VLCInputItem * const currentNodeInput = _tableViewDevices[row].inputNode.inputItem;
NSURL * const artworkURL = currentNodeInput.artworkURL;
NSImage * const placeholder = NSImage.VLCDefaultAppIconImage;
if (currentNodeInput.radioCountryCodeForFlagArtwork) {
@@ -474,15 +515,15 @@ referenceSizeForHeaderInSection:(NSInteger)section
}
NSString * const name = _mediaSourceMode == VLCMediaSourceModeLAN
- ? _lanDeviceSnapshot[row].inputNode.inputItem.name
- : _mediaSources[row].mediaSourceDescription;
+ ? _tableViewDevices[row].inputNode.inputItem.name
+ : _tableViewSources[row].mediaSourceDescription;
cellView.primaryTitleTextField.hidden = YES;
cellView.secondaryTitleTextField.hidden = YES;
cellView.singlePrimaryTitleTextField.hidden = NO;
cellView.singlePrimaryTitleTextField.stringValue = name;
return cellView;
- } else if ([tableColumn.identifier isEqualToString:@"VLCMediaSourceTableKindColumn"]) {
+ } else if ([tableColumn.identifier isEqualToString:VLCMediaSourceTableKindColumnIdentifier]) {
static NSString * const basicCellViewIdentifier = @"BasicTableCellViewIdentifier";
NSTableCellView *cellView =
[tableView makeViewWithIdentifier:basicCellViewIdentifier owner:self];
@@ -493,26 +534,10 @@ referenceSizeForHeaderInSection:(NSInteger)section
NSAssert(cellView, @"Cell view should not be nil");
if (_mediaSourceMode == VLCMediaSourceModeLAN) {
- VLCInputItem * const currentNodeInput = _lanDeviceSnapshot[row].inputNode.inputItem;
- if (currentNodeInput.inputType == ITEM_TYPE_DIRECTORY) {
- cellView.textField.stringValue = _NS("Directory");
- }
+ cellView.textField.stringValue = _tableViewDevices[row].inputNode.inputItem.inputTypeDescription;
} else {
- VLCMediaSource * const mediaSource = _mediaSources[row];
- switch(mediaSource.category) {
- case SD_CAT_DEVICES:
- cellView.textField.stringValue = _NS("Devices");
- break;
- case SD_CAT_LAN:
- cellView.textField.stringValue = _NS("LAN");
- break;
- case SD_CAT_INTERNET:
- cellView.textField.stringValue = _NS("Internet");
- break;
- case SD_CAT_MYCOMPUTER:
- cellView.textField.stringValue = _NS("My Computer");
- break;
- }
+ VLCMediaSource * const mediaSource = _tableViewSources[row];
+ cellView.textField.stringValue = mediaSource.categoryDescription;
}
return cellView;
}
@@ -521,6 +546,9 @@ referenceSizeForHeaderInSection:(NSInteger)section
- (void)tableViewSelectionDidChange:(NSNotification *)notification
{
+ if (_ignoringTableViewSelectionChanges) {
+ return;
+ }
const NSInteger selectedRow = self.tableView.selectedRow;
if (selectedRow < 0) {
return;
@@ -529,11 +557,11 @@ referenceSizeForHeaderInSection:(NSInteger)section
VLCMediaSource *mediaSource = nil;
VLCInputNode *childNode = nil;
if (_mediaSourceMode == VLCMediaSourceModeLAN) {
- VLCLANDeviceRecord * const record = _lanDeviceSnapshot[selectedRow];
+ VLCLANDeviceRecord * const record = _tableViewDevices[selectedRow];
mediaSource = record.mediaSource;
childNode = record.inputNode;
} else {
- mediaSource = _mediaSources[selectedRow];
+ mediaSource = _tableViewSources[selectedRow];
childNode = mediaSource.rootNode;
}
NSAssert(mediaSource != nil, @"Media source should not be nil");
@@ -546,8 +574,8 @@ referenceSizeForHeaderInSection:(NSInteger)section
- (id<NSPasteboardWriting>)tableView:(NSTableView *)tableView pasteboardWriterForRow:(NSInteger)row
{
VLCInputItem * const inputItem = _mediaSourceMode == VLCMediaSourceModeLAN
- ? _lanDeviceSnapshot[row].inputNode.inputItem
- : _mediaSources[row].rootNode.inputItem;
+ ? _tableViewDevices[row].inputNode.inputItem
+ : _tableViewSources[row].rootNode.inputItem;
return [self pasteboardWriterForInputItem:inputItem];
}
@@ -583,44 +611,28 @@ referenceSizeForHeaderInSection:(NSInteger)section
return [NSPasteboardItem pasteboardItemWithInputItem:inputItem];
}
-#pragma mark - LAN device snapshot
+#pragma mark - LAN device records
-- (NSArray<VLCLANDeviceRecord *> *)buildMediaSourceSnapshot
+- (void)rebuildLANDeviceRecords
{
- [_sourceRecords removeAllObjects];
- NSMutableArray<VLCLANDeviceRecord *> * const flat = [NSMutableArray array];
+ [_deviceRecordsBySource removeAllObjects];
+ if (self.mediaSourceMode != VLCMediaSourceModeLAN) {
+ return;
+ }
for (VLCMediaSource * const mediaSource in _mediaSources) {
- NSMutableArray<VLCLANDeviceRecord *> * const sourceRecs = [NSMutableArray array];
- for (VLCInputNode * const child in mediaSource.rootNode.children) {
- VLCLANDeviceRecord * const record = [[VLCLANDeviceRecord alloc] initWithMediaSource:mediaSource
- inputNode:child];
- [sourceRecs addObject:record];
- }
- NSArray<VLCLANDeviceRecord *> * const frozen = [sourceRecs copy];
- [_sourceRecords setObject:frozen forKey:mediaSource];
- [flat addObjectsFromArray:frozen];
+ [self updateLANDeviceRecordsForMediaSource:mediaSource];
}
- return [flat copy];
}
-- (NSArray<VLCLANDeviceRecord *> *)snapshotByUpdatingSource:(VLCMediaSource *)mediaSource
+- (void)updateLANDeviceRecordsForMediaSource:(VLCMediaSource *)mediaSource
{
- NSMutableArray<VLCLANDeviceRecord *> * const sourceRecs = [NSMutableArray array];
- for (VLCInputNode * const child in mediaSource.rootNode.children) {
+ NSMutableArray<VLCLANDeviceRecord *> * const records = NSMutableArray.array;
+ for (VLCInputNode * const node in mediaSource.rootNode.children) {
VLCLANDeviceRecord * const record = [[VLCLANDeviceRecord alloc] initWithMediaSource:mediaSource
- inputNode:child];
- [sourceRecs addObject:record];
- }
- [_sourceRecords setObject:[sourceRecs copy] forKey:mediaSource];
-
- NSMutableArray<VLCLANDeviceRecord *> * const flat = [NSMutableArray array];
- for (VLCMediaSource * const source in _mediaSources) {
- NSArray<VLCLANDeviceRecord *> * const recs = [_sourceRecords objectForKey:source];
- if (recs) {
- [flat addObjectsFromArray:recs];
- }
+ inputNode:node];
+ [records addObject:record];
}
- return [flat copy];
+ [_deviceRecordsBySource setObject:records.copy forKey:mediaSource];
}
#pragma mark - glue code
@@ -667,6 +679,10 @@ referenceSizeForHeaderInSection:(NSInteger)section
return;
}
+ // The shared table's old row indexes belong to the previous location.
+ _ignoringTableViewSelectionChanges = YES;
+ [self.tableView deselectAll:self];
+ _ignoringTableViewSelectionChanges = NO;
_childDataSource = childDataSource;
if (_mediaSourceMode == VLCMediaSourceModeLAN) {
@@ -799,9 +815,9 @@ referenceSizeForHeaderInSection:(NSInteger)section
if (self.mediaSourceMode == VLCMediaSourceModeLAN) {
VLCMediaSource * const source = aNotification.object;
if ([aNotification.name isEqualToString:VLCMediaSourceChildrenReset]) {
- _lanDeviceSnapshot = [self buildMediaSourceSnapshot];
+ [self rebuildLANDeviceRecords];
} else {
- _lanDeviceSnapshot = [self snapshotByUpdatingSource:source];
+ [self updateLANDeviceRecordsForMediaSource:source];
}
}
@@ -810,12 +826,56 @@ referenceSizeForHeaderInSection:(NSInteger)section
});
}
+- (void)updateTableViewItems
+{
+ VLCMediaSourceTableViewSorting * const sorting =
+ [[VLCMediaSourceTableViewSorting alloc] initWithSortDescriptors:self.tableView.sortDescriptors ?: @[]];
+ NSMutableArray<VLCLANDeviceRecord *> * const devices = NSMutableArray.array;
+ for (VLCMediaSource * const source in _mediaSources) {
+ NSArray<VLCLANDeviceRecord *> * const records = [_deviceRecordsBySource objectForKey:source];
+ if (records != nil) {
+ [devices addObjectsFromArray:records];
+ }
+ }
+ _tableViewDevices = [sorting sortedItems:devices
+ valueForItem:^id(VLCLANDeviceRecord *record, NSString *key) {
+ if ([key isEqualToString:VLCMediaSourceTableNameSortDescriptorKey]) {
+ return record.inputNode.inputItem.name ?: @"";
+ }
+ if ([key isEqualToString:VLCMediaSourceTableKindSortDescriptorKey]) {
+ return record.inputNode.inputItem.inputTypeDescription;
+ }
+ return [key isEqualToString:VLCMediaSourceTableCountSortDescriptorKey] ? @(-1) : @"";
+ }];
+ _tableViewSources = [sorting sortedItems:_mediaSources
+ valueForItem:^id(VLCMediaSource *source, NSString *key) {
+ if ([key isEqualToString:VLCMediaSourceTableNameSortDescriptorKey]) {
+ return source.mediaSourceDescription ?: @"";
+ }
+ if ([key isEqualToString:VLCMediaSourceTableKindSortDescriptorKey]) {
+ return source.categoryDescription;
+ }
+ return [key isEqualToString:VLCMediaSourceTableCountSortDescriptorKey] ? @(-1) : @"";
+ }];
+}
+
+- (void)tableView:(NSTableView *)tableView sortDescriptorsDidChange:(NSArray<NSSortDescriptor *> *)oldDescriptors
+{
+ [self reloadData];
+}
+
- (void)reloadData
{
+ [self updateTableViewItems];
if (self.viewMode == VLCLibraryGridViewModeSegment) {
[self.collectionView reloadData];
+ } else if (self.childDataSource != nil) {
+ [self.childDataSource reloadData];
} else {
+ _ignoringTableViewSelectionChanges = YES;
+ [self.tableView deselectAll:self];
[self.tableView reloadData];
+ _ignoringTableViewSelectionChanges = NO;
}
[NSNotificationCenter.defaultCenter postNotificationName:VLCMediaSourceBaseDataSourceNodeChanged
=====================================
modules/gui/macosx/library/media-source/VLCMediaSourceDataSource.h
=====================================
@@ -52,6 +52,7 @@ extern NSString * const VLCMediaSourceDataSourceLoadingEnded;
- (instancetype)initWithParentBaseDataSource:(VLCMediaSourceBaseDataSource *)parentBaseDataSource;
- (void)setupViews;
+- (void)reloadData;
- (VLCInputItem *)mediaSourceInputItemAtRow:(NSInteger)tableViewRow;
- (NSArray<VLCInputItem *> *)mediaSourceInputItemsAtIndexPaths:(NSSet<NSIndexPath *> *const)indexPaths;
=====================================
modules/gui/macosx/library/media-source/VLCMediaSourceDataSource.m
=====================================
@@ -25,6 +25,7 @@
#import "VLCLibraryMediaSourceViewNavigationStack.h"
#import "VLCMediaSource.h"
#import "VLCMediaSourceBaseDataSource.h"
+#import "VLCMediaSourceTableViewSorting.h"
#import "extensions/NSPasteboardItem+VLCAdditions.h"
#import "extensions/NSString+Helpers.h"
@@ -55,6 +56,9 @@ NSString * const VLCMediaSourceDataSourceLoadingEnded = @"VLCMediaSourceDataSour
@interface VLCMediaSourceDataSource()
{
VLCInputItem *_childRootInput;
+ // Table row order; grid items continue to use nodeToDisplay.children.
+ NSArray<VLCInputNode *> *_tableViewNodes;
+ BOOL _ignoringTableViewSelectionChanges;
}
@property (readwrite) dispatch_source_t observedPathDispatchSource;
@@ -157,6 +161,13 @@ static NSValue * _Nullable inputItemIdentifier(VLCInputItem * _Nullable const in
if (!preparseStarted) {
[self.nodeToDisplay clearChildrenCache];
+ }
+
+ if (self.parentBaseDataSource.childDataSource != self) {
+ return;
+ }
+
+ if (!preparseStarted) {
[self reloadData];
}
@@ -167,9 +178,27 @@ static NSValue * _Nullable inputItemIdentifier(VLCInputItem * _Nullable const in
object:self];
}
+- (nullable NSNumber *)knownChildCountForInputNode:(VLCInputNode *)inputNode
+{
+ if (inputNode.inputItem.inputType != ITEM_TYPE_DIRECTORY) {
+ return nil;
+ }
+
+ NSValue * const identifier = inputItemIdentifier(inputNode.inputItem);
+ if ([self.unavailableInputItemIdentifiers containsObject:identifier]) {
+ return nil;
+ }
+
+ const int numberOfChildren = inputNode.numberOfChildren;
+ if (numberOfChildren > 0 || [self.preparedInputItemIdentifiers containsObject:identifier]) {
+ return @(numberOfChildren);
+ }
+ return self.childCountsByInputItemIdentifier[identifier];
+}
+
- (void)reloadCountForInputItemIdentifier:(NSValue *)identifier
{
- NSArray<VLCInputNode *> * const children = self.nodeToDisplay.children;
+ NSArray<VLCInputNode *> * const children = _tableViewNodes;
const NSUInteger row = [children indexOfObjectPassingTest:^BOOL(VLCInputNode * const childNode,
NSUInteger __unused idx,
BOOL * const __unused stop) {
@@ -180,8 +209,20 @@ static NSValue * _Nullable inputItemIdentifier(VLCInputItem * _Nullable const in
}
[children[row] clearChildrenCache];
+ if (self.tableView.dataSource != self) {
+ return;
+ }
- const NSInteger countColumn = [self.tableView columnWithIdentifier:@"VLCMediaSourceTableCountColumn"];
+ VLCMediaSourceTableViewSorting * const sorting =
+ [[VLCMediaSourceTableViewSorting alloc] initWithSortDescriptors:self.tableView.sortDescriptors];
+ // A changed count can move rows when Count is the primary sort or a tie breaker.
+ // Reload to apply that ordering and restore selection; otherwise refresh only the count cell.
+ if ([sorting sortsByKey:VLCMediaSourceTableCountSortDescriptorKey]) {
+ [self reloadData];
+ return;
+ }
+
+ const NSInteger countColumn = [self.tableView columnWithIdentifier:VLCMediaSourceTableCountColumnIdentifier];
if (self.tableView.hidden || countColumn == -1) {
return;
}
@@ -204,6 +245,10 @@ static NSValue * _Nullable inputItemIdentifier(VLCInputItem * _Nullable const in
return;
dispatch_async(dispatch_get_main_queue(), ^{
[self clearDisplayedNodeChildrenCaches];
+ // Navigation history retains inactive data sources that also receive these notifications.
+ if (self.parentBaseDataSource.childDataSource != self) {
+ return;
+ }
[self reloadData];
});
}
@@ -220,7 +265,7 @@ static NSValue * _Nullable inputItemIdentifier(VLCInputItem * _Nullable const in
self.nodeObservation = [self.displayedMediaSource observeInputNode:nodeToDisplay
onChange:^(VLCMediaSourceNodeChange change) {
const typeof(self) strongSelf = weakSelf;
- if (strongSelf == nil) {
+ if (strongSelf == nil || strongSelf.parentBaseDataSource.childDataSource != strongSelf) {
return;
}
switch (change) {
@@ -233,7 +278,12 @@ static NSValue * _Nullable inputItemIdentifier(VLCInputItem * _Nullable const in
}
}];
- [self reloadData];
+ // Initial node configuration precedes attaching the shared views.
+ if (self.tableView == nil) {
+ [self updateTableViewNodes];
+ } else {
+ [self reloadData];
+ }
NSValue * const identifier = inputItemIdentifier(nodeToDisplay.inputItem);
NSString * const loadingNotificationName =
[self.preparingInputItemIdentifiers containsObject:identifier]
@@ -250,6 +300,7 @@ static NSValue * _Nullable inputItemIdentifier(VLCInputItem * _Nullable const in
- (void)setupViews
{
+ [self updateTableViewNodes];
[self.tableView setDoubleAction:@selector(tableViewAction:)];
[self.tableView setTarget:self];
[self.tableView registerForDraggedTypes:@[NSFilenamesPboardType]];
@@ -348,22 +399,49 @@ static NSValue * _Nullable inputItemIdentifier(VLCInputItem * _Nullable const in
#pragma mark - table view data source and delegation
+- (void)updateTableViewNodes
+{
+ VLCMediaSourceTableViewSorting * const sorting =
+ [[VLCMediaSourceTableViewSorting alloc] initWithSortDescriptors:self.tableView.sortDescriptors ?: @[]];
+ _tableViewNodes = [sorting sortedItems:self.nodeToDisplay.children ?: @[]
+ valueForItem:^id(VLCInputNode *node, NSString *key) {
+ VLCInputItem * const inputItem = node.inputItem;
+ if ([key isEqualToString:VLCMediaSourceTableNameSortDescriptorKey]) {
+ return inputItem.name ?: @"";
+ }
+ if ([key isEqualToString:VLCMediaSourceTableCountSortDescriptorKey]) {
+ return [self knownChildCountForInputNode:node] ?: @(-1);
+ }
+ if ([key isEqualToString:VLCMediaSourceTableKindSortDescriptorKey]) {
+ return inputItem.inputTypeDescription;
+ }
+ NSAssert([key isEqualToString:VLCMediaSourceTableTagsSortDescriptorKey],
+ @"Unexpected media source sort key: %@", key);
+ return [inputItem.finderTags componentsJoinedByString:@", "] ?: @"";
+ }];
+}
+
- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView
{
if (_nodeToDisplay) {
- return _nodeToDisplay.numberOfChildren;
+ return _tableViewNodes.count;
}
return 0;
}
+- (void)tableView:(NSTableView *)tableView sortDescriptorsDidChange:(NSArray<NSSortDescriptor *> *)oldDescriptors
+{
+ [self reloadData];
+}
+
- (NSView *)tableView:(NSTableView *)tableView
viewForTableColumn:(NSTableColumn *)tableColumn
row:(NSInteger)row
{
VLCInputNode * const inputNode = [self mediaSourceInputNodeAtRow:row];
- if ([tableColumn.identifier isEqualToString:@"VLCMediaSourceTableNameColumn"]) {
+ if ([tableColumn.identifier isEqualToString:VLCMediaSourceTableNameColumnIdentifier]) {
VLCLibraryTableCellView * const cellView =
[tableView makeViewWithIdentifier:VLCLibraryTableCellViewIdentifier owner:self];
cellView.representedInputItem = inputNode.inputItem;
@@ -373,7 +451,7 @@ static NSValue * _Nullable inputItemIdentifier(VLCInputItem * _Nullable const in
}
// Only present count view for folders
- if ([tableColumn.identifier isEqualToString:@"VLCMediaSourceTableCountColumn"] &&
+ if ([tableColumn.identifier isEqualToString:VLCMediaSourceTableCountColumnIdentifier] &&
inputNode.inputItem.inputType != ITEM_TYPE_DIRECTORY) {
return nil;
}
@@ -387,18 +465,14 @@ static NSValue * _Nullable inputItemIdentifier(VLCInputItem * _Nullable const in
}
NSAssert(cellView, @"Cell view should not be nil");
- if ([tableColumn.identifier isEqualToString:@"VLCMediaSourceTableCountColumn"]) {
+ if ([tableColumn.identifier isEqualToString:VLCMediaSourceTableCountColumnIdentifier]) {
NSValue * const identifier = inputItemIdentifier(inputNode.inputItem);
- const int numberOfChildren = inputNode.numberOfChildren;
- NSNumber * const cachedChildCount = self.childCountsByInputItemIdentifier[identifier];
+ NSNumber * const knownChildCount = [self knownChildCountForInputNode:inputNode];
if ([self.unavailableInputItemIdentifiers containsObject:identifier]) {
cellView.textField.stringValue = NSTR("Unavailable");
- } else if (numberOfChildren > 0 || [self.preparedInputItemIdentifiers containsObject:identifier]) {
+ } else if (knownChildCount != nil) {
cellView.textField.stringValue =
- [NSString stringWithFormat:@"%i items", numberOfChildren];
- } else if (cachedChildCount != nil) {
- cellView.textField.stringValue =
- [NSString stringWithFormat:@"%li items", cachedChildCount.integerValue];
+ [NSString stringWithFormat:@"%li items", knownChildCount.integerValue];
} else {
cellView.textField.stringValue = NSTR("Loading…");
if (![self.preparingInputItemIdentifiers containsObject:identifier]) {
@@ -426,57 +500,9 @@ static NSValue * _Nullable inputItemIdentifier(VLCInputItem * _Nullable const in
});
}
}
- } else if ([tableColumn.identifier isEqualToString:@"VLCMediaSourceTableKindColumn"]) {
- NSString *typeName = NSTR("Unknown");
- switch (inputNode.inputItem.inputType) {
- case ITEM_TYPE_UNKNOWN:
- typeName = NSTR("Unknown");
- break;
- case ITEM_TYPE_FILE:
- {
- NSString * const filePath = inputNode.inputItem.MRL;
- NSString * const extension = filePath.pathExtension.lowercaseString;
- if (extension.length > 0) {
- typeName = [NSString stringWithFormat:@"%@ File", extension.capitalizedString];
-
- const CFStringRef extCF = (__bridge CFStringRef)extension;
- const CFStringRef uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, extCF, NULL);
- if (uti) {
- CFStringRef descriptionCF = UTTypeCopyDescription(uti);
- if (descriptionCF) {
- typeName = CFBridgingRelease(descriptionCF);
- }
- CFRelease(uti);
- }
- } else {
- typeName = NSTR("File");
- }
- break;
- }
- case ITEM_TYPE_DIRECTORY:
- typeName = NSTR("Directory");
- break;
- case ITEM_TYPE_DISC:
- typeName = NSTR("Disc");
- break;
- case ITEM_TYPE_CARD:
- typeName = NSTR("Card");
- break;
- case ITEM_TYPE_STREAM:
- typeName = NSTR("Stream");
- break;
- case ITEM_TYPE_PLAYLIST:
- typeName = NSTR("Playlist");
- break;
- case ITEM_TYPE_NODE:
- typeName = NSTR("Node");
- break;
- case ITEM_TYPE_NUMBER:
- typeName = NSTR("Undefined");
- break;
- }
- cellView.textField.stringValue = typeName;
- } else if ([tableColumn.identifier isEqualToString:@"VLCMediaSourceTableTagsColumn"]) {
+ } else if ([tableColumn.identifier isEqualToString:VLCMediaSourceTableKindColumnIdentifier]) {
+ cellView.textField.stringValue = inputNode.inputItem.inputTypeDescription;
+ } else if ([tableColumn.identifier isEqualToString:VLCMediaSourceTableTagsColumnIdentifier]) {
static NSString * const basicCellViewIdentifier = @"BasicTableCellViewIdentifier";
NSTableCellView *cellView = [tableView makeViewWithIdentifier:basicCellViewIdentifier
owner:self];
@@ -498,6 +524,9 @@ static NSValue * _Nullable inputItemIdentifier(VLCInputItem * _Nullable const in
- (void)tableViewSelectionDidChange:(NSNotification *)notification
{
+ if (_ignoringTableViewSelectionChanges) {
+ return;
+ }
NSInteger selectedIndex = self.tableView.selectedRow;
if (selectedIndex < 0) {
return;
@@ -557,14 +586,10 @@ static NSValue * _Nullable inputItemIdentifier(VLCInputItem * _Nullable const in
return nil;
}
- VLCInputNode *rootNode = _nodeToDisplay;
- NSArray *nodeChildren = rootNode.children;
-
- if (nodeChildren == nil || nodeChildren.count == 0) {
+ if (tableViewRow < 0 || (NSUInteger)tableViewRow >= _tableViewNodes.count) {
return nil;
}
-
- return nodeChildren[tableViewRow];
+ return _tableViewNodes[tableViewRow];
}
- (VLCInputItem*)mediaSourceInputItemAtRow:(NSInteger)tableViewRow
@@ -609,13 +634,37 @@ static NSValue * _Nullable inputItemIdentifier(VLCInputItem * _Nullable const in
- (void)reloadData
{
+ NSParameterAssert(self.tableView.dataSource == self);
+
if (!_collectionView.hidden) {
[_collectionView reloadData];
}
- if(!_tableView.hidden) {
- [_tableView reloadData];
- }
+ // Row indexes change when sorting; preserve selection by input-item identity.
+ NSMutableSet<NSValue *> * const selectedInputItemIdentifiers = NSMutableSet.set;
+ [self.tableView.selectedRowIndexes enumerateIndexesUsingBlock:^(NSUInteger row, BOOL * const __unused stop) {
+ if (row >= self->_tableViewNodes.count) {
+ return;
+ }
+
+ NSValue * const identifier = inputItemIdentifier(self->_tableViewNodes[row].inputItem);
+ if (identifier == nil) {
+ return;
+ }
+ [selectedInputItemIdentifiers addObject:identifier];
+ }];
+ [self updateTableViewNodes];
+ _ignoringTableViewSelectionChanges = YES;
+ [self.tableView reloadData];
+ NSMutableIndexSet * const selectedRows = NSMutableIndexSet.indexSet;
+ [_tableViewNodes enumerateObjectsUsingBlock:^(VLCInputNode * const node, NSUInteger row, BOOL * const __unused stop) {
+ if ([selectedInputItemIdentifiers containsObject:inputItemIdentifier(node.inputItem)]) {
+ [selectedRows addIndex:row];
+ }
+ }];
+ // Restoring selection must not trigger folder navigation.
+ [self.tableView selectRowIndexes:selectedRows byExtendingSelection:NO];
+ _ignoringTableViewSelectionChanges = NO;
[NSNotificationCenter.defaultCenter postNotificationName:VLCMediaSourceDataSourceNodeChanged
object:self];
=====================================
modules/gui/macosx/library/media-source/VLCMediaSourceTableViewSorting.h
=====================================
@@ -0,0 +1,42 @@
+/****************************************************************************
+ * VLCMediaSourceTableViewSorting.h: Browse table sorting
+ ****************************************************************************
+ * Copyright (C) 2026 VLC authors and VideoLAN
+ *
+ * Authors: Claudio Cambra <developer at claudiocambra.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 <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+ at interface VLCMediaSourceTableViewSorting : NSObject
+
+// Descriptors are applied in array order: the first is the primary sort,
+// and each subsequent descriptor breaks ties left by the preceding ones.
+- (instancetype)initWithSortDescriptors:(NSArray<NSSortDescriptor *> *)sortDescriptors;
+
+// Returns YES if any configured descriptor uses this key, including tie breakers,
+// and NO otherwise. This query leaves descriptor order and priority unchanged.
+- (BOOL)sortsByKey:(NSString *)sortDescriptorKey;
+
+- (NSArray *)sortedItems:(NSArray *)items
+ valueForItem:(id (^)(id item, NSString *key))valueForItem;
+
+ at end
+
+NS_ASSUME_NONNULL_END
=====================================
modules/gui/macosx/library/media-source/VLCMediaSourceTableViewSorting.m
=====================================
@@ -0,0 +1,89 @@
+/****************************************************************************
+ * VLCMediaSourceTableViewSorting.m: Browse table sorting
+ ****************************************************************************
+ * Copyright (C) 2026 VLC authors and VideoLAN
+ *
+ * Authors: Claudio Cambra <developer at claudiocambra.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 "VLCMediaSourceTableViewSorting.h"
+
+ at interface VLCMediaSourceTableViewSorting ()
+ at property (readonly, copy) NSArray<NSSortDescriptor *> *sortDescriptors;
+ at end
+
+ at implementation VLCMediaSourceTableViewSorting
+
+- (instancetype)initWithSortDescriptors:(NSArray<NSSortDescriptor *> *)sortDescriptors
+{
+ self = [super init];
+ if (self) {
+ _sortDescriptors = [sortDescriptors copy];
+ }
+ return self;
+}
+
+- (BOOL)sortsByKey:(NSString *)sortDescriptorKey
+{
+ for (NSSortDescriptor * const descriptor in self.sortDescriptors) {
+ if ([descriptor.key isEqualToString:sortDescriptorKey]) {
+ return YES;
+ }
+ }
+ return NO;
+}
+
+- (NSArray *)sortedItems:(NSArray *)items
+ valueForItem:(id (^)(id item, NSString *key))valueForItem
+{
+ if (self.sortDescriptors.count == 0) {
+ return items;
+ }
+ // Cache by object identity for this call only; subsequent sorts read fresh values.
+ NSMapTable<id, NSDictionary<NSString *, id> *> * const valuesByItem =
+ [NSMapTable mapTableWithKeyOptions:NSPointerFunctionsStrongMemory | NSPointerFunctionsObjectPointerPersonality
+ valueOptions:NSPointerFunctionsStrongMemory];
+
+ // Read each distinct object's active keys once to avoid metadata reads during comparisons.
+ for (id item in items) {
+ if ([valuesByItem objectForKey:item] != nil) {
+ continue;
+ }
+ NSMutableDictionary<NSString *, id> * const values = NSMutableDictionary.dictionary;
+ for (NSSortDescriptor * const descriptor in self.sortDescriptors) {
+ if (values[descriptor.key] == nil) {
+ values[descriptor.key] = valueForItem(item, descriptor.key);
+ }
+ }
+ [valuesByItem setObject:values forKey:item];
+ }
+
+ // Sort the original objects, preserving their input order when all descriptors compare equal.
+ return [items sortedArrayWithOptions:NSSortStable usingComparator:^NSComparisonResult(id left, id right) {
+ // Compare cached values in descriptor order, using later descriptors to break ties.
+ for (NSSortDescriptor * const descriptor in self.sortDescriptors) {
+ const NSComparisonResult result = [descriptor compareObject:[valuesByItem objectForKey:left]
+ toObject:[valuesByItem objectForKey:right]];
+ if (result != NSOrderedSame) {
+ return result;
+ }
+ }
+ return NSOrderedSame;
+ }];
+}
+
+ at end
=====================================
modules/gui/macosx/tests/Makefile.am
=====================================
@@ -16,6 +16,7 @@ VLCMacOSXTests_SOURCES = \
gui/macosx/tests/VLCInputItemTest.m \
gui/macosx/tests/VLCInputItemTestSupport.m \
gui/macosx/tests/VLCInputNodeTest.m \
+ gui/macosx/tests/VLCMediaSourceTableViewSortingTest.m \
gui/macosx/tests/VLCLibraryDataTypesIntegrationTest.m \
gui/macosx/tests/VLCLibraryDataTypesIntegrationTestSupport.m \
gui/macosx/tests/VLCLogMessageTest.m \
@@ -27,6 +28,7 @@ VLCMacOSXTests_SOURCES = \
gui/macosx/library/VLCLibraryDataTypes.m \
gui/macosx/library/VLCInputItem.m \
gui/macosx/library/VLCInputNode.m \
+ gui/macosx/library/media-source/VLCMediaSourceTableViewSorting.m \
gui/macosx/windows/logging/VLCLogMessage.m \
gui/macosx/views/VLCTimeFormatter.m
@@ -58,6 +60,7 @@ VLCMacOSXTests_LDFLAGS = \
-Wl,-framework,XCTest \
-Wl,-framework,Foundation \
-Wl,-framework,Cocoa \
+ -Wl,-framework,CoreServices \
-Wl,-framework,IOKit
VLCMacOSXTests_LDADD = \
=====================================
modules/gui/macosx/tests/NSStringHelpersTest.m
=====================================
@@ -12,6 +12,7 @@
*****************************************************************************/
#import <XCTest/XCTest.h>
+#import <CoreServices/CoreServices.h>
#import <vlc_tick.h>
#import <string.h>
@@ -100,6 +101,28 @@
negative:YES], @"01:00");
}
+- (void)testFileTypeDescriptionWithoutExtension
+{
+ XCTAssertEqualObjects([@"" fileTypeDescription], NSTR("File"));
+ XCTAssertEqualObjects([@"/media/README" fileTypeDescription], NSTR("File"));
+ XCTAssertEqualObjects([@"file:///media/README" fileTypeDescription], NSTR("File"));
+ XCTAssertEqualObjects([@"/media/.hidden" fileTypeDescription], NSTR("File"));
+}
+
+- (void)testFileTypeDescriptionForUnknownExtension
+{
+ XCTAssertEqualObjects([@"/media/file.VLCUNKNOWNFILETYPE18436572" fileTypeDescription],
+ @"Vlcunknownfiletype18436572 File");
+}
+
+- (void)testFileTypeDescriptionUsesSystemDescription
+{
+ NSString * const description = CFBridgingRelease(UTTypeCopyDescription(kUTTypePlainText));
+ XCTAssertNotNil(description);
+ XCTAssertEqualObjects([@"/media/file.txt" fileTypeDescription], description);
+ XCTAssertEqualObjects([@"file:///media/file.TXT" fileTypeDescription], description);
+}
+
- (void)testExtensionsArrayFromVLCStyleString
{
XCTAssertEqualObjects([NSString extensionsArrayFromVLCStyleString:"*.mp4;*.mkv"],
=====================================
modules/gui/macosx/tests/VLCMediaSourceTableViewSortingTest.m
=====================================
@@ -0,0 +1,227 @@
+/****************************************************************************
+ * VLCMediaSourceTableViewSortingTest.m: Browse table sorting tests
+ ****************************************************************************
+ * Copyright (C) 2026 VLC authors and VideoLAN
+ *
+ * Authors: Claudio Cambra <developer at claudiocambra.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 <XCTest/XCTest.h>
+
+#import "library/media-source/VLCMediaSourceTableViewSorting.h"
+
+static NSString * const VLCSortingNameKey = @"name";
+static NSString * const VLCSortingCountKey = @"count";
+static NSString * const VLCSortingKindKey = @"kind";
+static NSString * const VLCSortingTagsKey = @"tags";
+
+ at interface VLCSortingItem : NSObject
+
+ at property (copy) NSDictionary<NSString *, id> *values;
+ at property (readonly) NSMutableDictionary<NSString *, NSNumber *> *valueReadCounts;
+
+- (instancetype)initWithValues:(NSDictionary<NSString *, id> *)values;
+- (id)sortValueForKey:(NSString *)key;
+
+ at end
+
+ at implementation VLCSortingItem
+
+- (instancetype)initWithValues:(NSDictionary<NSString *, id> *)values
+{
+ self = [super init];
+ if (self) {
+ _values = [values copy];
+ _valueReadCounts = NSMutableDictionary.dictionary;
+ }
+ return self;
+}
+
+- (id)sortValueForKey:(NSString *)key
+{
+ _valueReadCounts[key] = @(_valueReadCounts[key].unsignedIntegerValue + 1);
+ return self.values[key];
+}
+
+ at end
+
+ at interface VLCMediaSourceTableViewSortingTest : XCTestCase
+ at end
+
+ at implementation VLCMediaSourceTableViewSortingTest
+
+- (void)testNameSortingUsesNaturalOrderAndReverses
+{
+ NSArray<NSDictionary *> * const items = @[
+ @{VLCSortingNameKey: @"Video 10"},
+ @{VLCSortingNameKey: @"video 2"},
+ @{VLCSortingNameKey: @"Video 1"}
+ ];
+ NSSortDescriptor * const descriptor = [NSSortDescriptor sortDescriptorWithKey:VLCSortingNameKey
+ ascending:YES
+ selector:@selector(localizedStandardCompare:)];
+ VLCMediaSourceTableViewSorting * const ascending =
+ [[VLCMediaSourceTableViewSorting alloc] initWithSortDescriptors:@[descriptor]];
+ VLCMediaSourceTableViewSorting * const descending =
+ [[VLCMediaSourceTableViewSorting alloc] initWithSortDescriptors:@[descriptor.reversedSortDescriptor]];
+ id (^valueForItem)(id, NSString *) = ^id(NSDictionary *item, NSString *key) {
+ return item[key];
+ };
+ XCTAssertEqualObjects([ascending sortedItems:items valueForItem:valueForItem],
+ (@[items[2], items[1], items[0]]));
+ XCTAssertEqualObjects([descending sortedItems:items valueForItem:valueForItem],
+ (@[items[0], items[1], items[2]]));
+}
+
+- (void)testCountsSortNumericallyIncludingUnknownCounts
+{
+ NSArray<NSDictionary *> * const items = @[
+ @{VLCSortingCountKey: @10},
+ @{VLCSortingCountKey: @2},
+ @{VLCSortingCountKey: @(-1)},
+ @{VLCSortingCountKey: @0}
+ ];
+ NSSortDescriptor * const descriptor = [NSSortDescriptor sortDescriptorWithKey:VLCSortingCountKey
+ ascending:YES
+ selector:@selector(compare:)];
+ VLCMediaSourceTableViewSorting * const ascending =
+ [[VLCMediaSourceTableViewSorting alloc] initWithSortDescriptors:@[descriptor]];
+ VLCMediaSourceTableViewSorting * const descending =
+ [[VLCMediaSourceTableViewSorting alloc] initWithSortDescriptors:@[descriptor.reversedSortDescriptor]];
+ id (^valueForItem)(id, NSString *) = ^id(NSDictionary *item, NSString *key) {
+ return item[key];
+ };
+ XCTAssertEqualObjects([ascending sortedItems:items valueForItem:valueForItem],
+ (@[items[2], items[3], items[1], items[0]]));
+ XCTAssertEqualObjects([descending sortedItems:items valueForItem:valueForItem],
+ (@[items[0], items[1], items[3], items[2]]));
+}
+
+- (void)testSortQueriesIncludeSecondaryDescriptors
+{
+ NSSortDescriptor * const name = [NSSortDescriptor sortDescriptorWithKey:VLCSortingNameKey ascending:YES];
+ NSSortDescriptor * const count = [NSSortDescriptor sortDescriptorWithKey:VLCSortingCountKey ascending:YES];
+ VLCMediaSourceTableViewSorting * const namesAndCounts =
+ [[VLCMediaSourceTableViewSorting alloc] initWithSortDescriptors:@[name, count]];
+ XCTAssertTrue([namesAndCounts sortsByKey:VLCSortingNameKey]);
+ XCTAssertTrue([namesAndCounts sortsByKey:VLCSortingCountKey]);
+ XCTAssertFalse([namesAndCounts sortsByKey:VLCSortingKindKey]);
+ XCTAssertFalse([namesAndCounts sortsByKey:VLCSortingTagsKey]);
+
+ NSSortDescriptor * const kind = [NSSortDescriptor sortDescriptorWithKey:VLCSortingKindKey ascending:YES];
+ NSSortDescriptor * const tags = [NSSortDescriptor sortDescriptorWithKey:VLCSortingTagsKey ascending:NO];
+ VLCMediaSourceTableViewSorting * const kindsAndTags =
+ [[VLCMediaSourceTableViewSorting alloc] initWithSortDescriptors:@[kind, tags]];
+ XCTAssertTrue([kindsAndTags sortsByKey:VLCSortingKindKey]);
+ XCTAssertTrue([kindsAndTags sortsByKey:VLCSortingTagsKey]);
+ XCTAssertFalse([kindsAndTags sortsByKey:VLCSortingNameKey]);
+ XCTAssertFalse([kindsAndTags sortsByKey:VLCSortingCountKey]);
+}
+
+- (void)testSecondaryDescriptorsAndStableTiesPreserveObjectIdentity
+{
+ NSArray<VLCSortingItem *> * const items = @[
+ [[VLCSortingItem alloc] initWithValues:@{VLCSortingKindKey: @"File", VLCSortingNameKey: @"B"}],
+ [[VLCSortingItem alloc] initWithValues:@{VLCSortingKindKey: @"File", VLCSortingNameKey: @"A"}],
+ [[VLCSortingItem alloc] initWithValues:@{VLCSortingKindKey: @"File", VLCSortingNameKey: @"A"}]
+ ];
+ NSSortDescriptor * const kind = [NSSortDescriptor sortDescriptorWithKey:VLCSortingKindKey ascending:YES];
+ NSSortDescriptor * const name = [NSSortDescriptor sortDescriptorWithKey:VLCSortingNameKey ascending:YES];
+ VLCMediaSourceTableViewSorting * const sorting =
+ [[VLCMediaSourceTableViewSorting alloc] initWithSortDescriptors:@[kind, name]];
+ NSArray * const sorted = [sorting sortedItems:items
+ valueForItem:^id(VLCSortingItem *item, NSString *key) {
+ return [item sortValueForKey:key];
+ }];
+ XCTAssertTrue(sorted[0] == items[1]);
+ XCTAssertTrue(sorted[1] == items[2]);
+ XCTAssertTrue(sorted[2] == items[0]);
+}
+
+- (void)testUnsortedItemsDoNotReadValues
+{
+ NSArray<NSDictionary *> * const items = @[
+ @{VLCSortingNameKey: @"B"},
+ @{VLCSortingNameKey: @"A"}
+ ];
+ VLCMediaSourceTableViewSorting * const sorting =
+ [[VLCMediaSourceTableViewSorting alloc] initWithSortDescriptors:@[]];
+ XCTAssertFalse([sorting sortsByKey:VLCSortingNameKey]);
+ XCTAssertFalse([sorting sortsByKey:VLCSortingCountKey]);
+ XCTAssertFalse([sorting sortsByKey:VLCSortingKindKey]);
+ XCTAssertFalse([sorting sortsByKey:VLCSortingTagsKey]);
+ NSArray * const sorted = [sorting sortedItems:items
+ valueForItem:^id(id __unused item, NSString * __unused key) {
+ XCTFail(@"Discovery order should not read sort values");
+ return nil;
+ }];
+ XCTAssertTrue(sorted == items);
+ XCTAssertEqualObjects([sorting sortedItems:@[] valueForItem:^id(id __unused item, NSString * __unused key) {
+ XCTFail(@"An empty list should not read sort values");
+ return nil;
+ }], @[]);
+}
+
+- (void)testOnlyActiveKeysAreReadOnceForRepeatedObjectsAndDescriptors
+{
+ VLCSortingItem * const first = [[VLCSortingItem alloc] initWithValues:@{
+ VLCSortingNameKey: @"Folder", VLCSortingCountKey: @2,
+ VLCSortingKindKey: @"Directory", VLCSortingTagsKey: @"Blue"
+ }];
+ VLCSortingItem * const second = [[VLCSortingItem alloc] initWithValues:@{
+ VLCSortingNameKey: @"Folder", VLCSortingCountKey: @1,
+ VLCSortingKindKey: @"Directory", VLCSortingTagsKey: @"Red"
+ }];
+ NSArray<VLCSortingItem *> * const items = @[first, second, first];
+ NSSortDescriptor * const name = [NSSortDescriptor sortDescriptorWithKey:VLCSortingNameKey ascending:YES];
+ NSSortDescriptor * const count = [NSSortDescriptor sortDescriptorWithKey:VLCSortingCountKey ascending:YES];
+ VLCMediaSourceTableViewSorting * const sorting =
+ [[VLCMediaSourceTableViewSorting alloc] initWithSortDescriptors:@[name, count, name]];
+ NSArray * const sorted = [sorting sortedItems:items
+ valueForItem:^id(VLCSortingItem *item, NSString *key) {
+ return [item sortValueForKey:key];
+ }];
+ XCTAssertTrue(sorted[0] == second);
+ XCTAssertTrue(sorted[1] == first);
+ XCTAssertTrue(sorted[2] == first);
+ for (VLCSortingItem * const item in @[first, second]) {
+ XCTAssertEqualObjects(item.valueReadCounts[VLCSortingNameKey], @1);
+ XCTAssertEqualObjects(item.valueReadCounts[VLCSortingCountKey], @1);
+ XCTAssertNil(item.valueReadCounts[VLCSortingKindKey]);
+ XCTAssertNil(item.valueReadCounts[VLCSortingTagsKey]);
+ }
+}
+
+- (void)testSortingAgainReadsUpdatedValues
+{
+ VLCSortingItem * const first = [[VLCSortingItem alloc] initWithValues:@{VLCSortingCountKey: @(-1)}];
+ VLCSortingItem * const second = [[VLCSortingItem alloc] initWithValues:@{VLCSortingCountKey: @2}];
+ NSArray<VLCSortingItem *> * const items = @[first, second];
+ NSSortDescriptor * const descriptor = [NSSortDescriptor sortDescriptorWithKey:VLCSortingCountKey ascending:YES];
+ VLCMediaSourceTableViewSorting * const sorting =
+ [[VLCMediaSourceTableViewSorting alloc] initWithSortDescriptors:@[descriptor]];
+ id (^valueForItem)(id, NSString *) = ^id(VLCSortingItem *item, NSString *key) {
+ return [item sortValueForKey:key];
+ };
+ XCTAssertTrue([sorting sortedItems:items valueForItem:valueForItem][0] == first);
+ first.values = @{VLCSortingCountKey: @10};
+ XCTAssertTrue([sorting sortedItems:items valueForItem:valueForItem][0] == second);
+ XCTAssertEqualObjects(first.valueReadCounts[VLCSortingCountKey], @2);
+ XCTAssertEqualObjects(second.valueReadCounts[VLCSortingCountKey], @2);
+}
+
+ at end
View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/3c3b719fc6be39c796e9312b4157aa701d7f7bbc...716e856c9f56000a6cf73c49dbc63a0c67ebb620
--
View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/3c3b719fc6be39c796e9312b4157aa701d7f7bbc...716e856c9f56000a6cf73c49dbc63a0c67ebb620
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