Steve Lhomme pushed to branch master at VideoLAN / VLC
Commits: 1bd955db by Claudio Cambra at 2026-06-04T18:58:47+00:00 macosx: Remove removeAndCleanUpStatesInRange Signed-off-by: Claudio Cambra <[email protected]> - - - - - a5444827 by Claudio Cambra at 2026-06-04T18:58:47+00:00 macosx: Cache child nodes of input node Signed-off-by: Claudio Cambra <[email protected]> - - - - - 7aeae469 by Claudio Cambra at 2026-06-04T18:58:47+00:00 macosx: Call didFinishGeneratingChildNodesForNodeHandler during child node generation early return Signed-off-by: Claudio Cambra <[email protected]> - - - - - 1b46f999 by Claudio Cambra at 2026-06-04T18:58:47+00:00 macosx: Do not manually clear out child nodes in media source Signed-off-by: Claudio Cambra <[email protected]> - - - - - e329ad93 by Claudio Cambra at 2026-06-04T18:58:47+00:00 macosx: Check children count before attempting to generate child nodes Signed-off-by: Claudio Cambra <[email protected]> - - - - - 4cde2b20 by Claudio Cambra at 2026-06-04T18:58:47+00:00 macosx: Cancel prior observed dispatch source before creating new one Signed-off-by: Claudio Cambra <[email protected]> - - - - - 9af1e6fc by Claudio Cambra at 2026-06-04T18:58:47+00:00 macosx: Use obj-c input node (and clear cached children) type as parameter for generateChildNodesForDirectoryNode: Signed-off-by: Claudio Cambra <[email protected]> - - - - - 7fc886fb by Claudio Cambra at 2026-06-04T18:58:47+00:00 macosx: Remove unnecessary node generation within setNodeToDisplay Signed-off-by: Claudio Cambra <[email protected]> - - - - - a004bb45 by Claudio Cambra at 2026-06-04T18:58:47+00:00 macosx: Append current library state when clicking breadcrumb Signed-off-by: Claudio Cambra <[email protected]> - - - - - a4245e73 by Claudio Cambra at 2026-06-04T18:58:47+00:00 macosx: Replace progressive path control mechanism with snapshots Signed-off-by: Claudio Cambra <[email protected]> - - - - - e6582167 by Claudio Cambra at 2026-06-04T18:58:47+00:00 macosx: Provide some visual feedback for nodes with failed child generation Signed-off-by: Claudio Cambra <[email protected]> - - - - - 10 changed files: - modules/gui/macosx/library/VLCInputItem.h - modules/gui/macosx/library/VLCInputItem.m - modules/gui/macosx/library/VLCInputNodePathControl.h - modules/gui/macosx/library/VLCInputNodePathControl.m - modules/gui/macosx/library/media-source/VLCLibraryMediaSourceViewNavigationStack.m - modules/gui/macosx/library/media-source/VLCLibraryMediaSourceViewNavigationState.h - modules/gui/macosx/library/media-source/VLCMediaSource.h - modules/gui/macosx/library/media-source/VLCMediaSource.m - modules/gui/macosx/library/media-source/VLCMediaSourceBaseDataSource.m - modules/gui/macosx/library/media-source/VLCMediaSourceDataSource.m Changes: ===================================== modules/gui/macosx/library/VLCInputItem.h ===================================== @@ -102,6 +102,8 @@ NSDictionary<NSString *, id> *commonInputItemData(NSArray<VLCInputItem*> * const @property (readonly) int numberOfChildren; @property (readonly, nullable) NSArray <VLCInputNode *> *children; +- (void)clearChildrenCache; + @end NS_ASSUME_NONNULL_END ===================================== modules/gui/macosx/library/VLCInputItem.m ===================================== @@ -654,6 +654,12 @@ NSString * const value_##prop = } +@interface VLCInputNode () +{ + NSArray<VLCInputNode *> *_cachedChildren; +} +@end + @implementation VLCInputNode - (instancetype)initWithInputNode:(struct input_item_node_t *)p_inputNode @@ -679,24 +685,40 @@ NSString * const value_##prop = return [NSString stringWithFormat:@"%@: node: %p input name: %@, number of children: %i", NSStringFromClass([self class]),_vlcInputItemNode, inputItemName, self.numberOfChildren]; } +- (void)clearChildrenCache +{ + _cachedChildren = nil; +} + - (int)numberOfChildren { + if (_cachedChildren) { + return (int)_cachedChildren.count; + } return _vlcInputItemNode ? _vlcInputItemNode->i_children : 0; } - (nullable NSArray<VLCInputNode *> *)children { + if (_cachedChildren) { + return _cachedChildren; + } + if (_vlcInputItemNode == NULL) { return nil; } NSMutableArray *mutableArray = [[NSMutableArray alloc] initWithCapacity:_vlcInputItemNode->i_children]; for (int i = 0; i < _vlcInputItemNode->i_children; i++) { + if (_vlcInputItemNode->pp_children == NULL) { + break; + } VLCInputNode *inputNode = [[VLCInputNode alloc] initWithInputNode:_vlcInputItemNode->pp_children[i]]; if (inputNode) { [mutableArray addObject:inputNode]; } } - return [mutableArray copy]; + _cachedChildren = [mutableArray copy]; + return _cachedChildren; } @end ===================================== modules/gui/macosx/library/VLCInputNodePathControl.h ===================================== @@ -29,11 +29,17 @@ NS_ASSUME_NONNULL_BEGIN @interface VLCInputNodePathControl : NSPathControl @property (readonly) NSMutableDictionary *inputNodePathControlItems; +// The path items in display order, resolved back to their +// VLCInputNodePathControlItem instances. Used to snapshot the full breadcrumb +// for a navigation state so it can be restored later. +@property (readonly) NSArray<VLCInputNodePathControlItem *> *orderedInputNodePathControlItems; - (void)appendInputNodePathControlItem:(VLCInputNodePathControlItem *)inputNodePathControlItem; -- (void)removeLastInputNodePathControlItem; - (void)clearInputNodePathControlItems; - (void)clearPathControlItemsAheadOf:(NSPathControlItem *)item; +// Replaces the entire breadcrumb with the given ordered items, rebuilding the +// backing lookup table. Used when restoring a navigation state. +- (void)setPathWithInputNodePathControlItems:(NSArray<VLCInputNodePathControlItem *> *)items; @end ===================================== modules/gui/macosx/library/VLCInputNodePathControl.m ===================================== @@ -44,21 +44,6 @@ self.pathItems = pathItems; } -- (void)removeLastInputNodePathControlItem -{ - if (self.pathItems.count == 0) { - _inputNodePathControlItems = NSMutableDictionary.dictionary; - return; - } - - NSMutableArray * const pathItems = self.pathItems.mutableCopy; - NSPathControlItem * const lastItem = pathItems.lastObject; - - [pathItems removeLastObject]; - self.pathItems = pathItems; - [self.inputNodePathControlItems removeObjectForKey:lastItem.image.accessibilityDescription]; -} - - (void)clearInputNodePathControlItems { _inputNodePathControlItems = NSMutableDictionary.dictionary; @@ -92,4 +77,26 @@ [self.inputNodePathControlItems removeObjectsForKeys:itemIdsToRemove]; } +- (NSArray<VLCInputNodePathControlItem *> *)orderedInputNodePathControlItems +{ + NSMutableArray<VLCInputNodePathControlItem *> * const ordered = + [NSMutableArray arrayWithCapacity:self.pathItems.count]; + for (NSPathControlItem * const item in self.pathItems) { + VLCInputNodePathControlItem * const inputNodeItem = + [self.inputNodePathControlItems objectForKey:item.image.accessibilityDescription]; + if (inputNodeItem != nil) { + [ordered addObject:inputNodeItem]; + } + } + return ordered.copy; +} + +- (void)setPathWithInputNodePathControlItems:(NSArray<VLCInputNodePathControlItem *> *)items +{ + [self clearInputNodePathControlItems]; + for (VLCInputNodePathControlItem * const item in items) { + [self appendInputNodePathControlItem:item]; + } +} + @end ===================================== modules/gui/macosx/library/media-source/VLCLibraryMediaSourceViewNavigationStack.m ===================================== @@ -164,10 +164,6 @@ _currentPosition = [[VLCLibraryMediaSourceViewNavigationCurrentStackPosition alloc] initWithStackIndex:newPositionIndex andState:_navigationStates[newPositionIndex]]; - VLCInputNode *node = _currentPosition.navigationState.currentNodeDisplayed; - VLCInputNodePathControlItem *nodePathItem = [[VLCInputNodePathControlItem alloc] initWithInputNode:node]; - [self.libraryWindow.mediaSourcePathControl appendInputNodePathControlItem:nodePathItem]; - [self setMediaSourceViewToState:_currentPosition.navigationState]; } @@ -181,8 +177,6 @@ _currentPosition = [[VLCLibraryMediaSourceViewNavigationCurrentStackPosition alloc] initWithStackIndex:newPositionIndex andState:_navigationStates[newPositionIndex]]; - [self.libraryWindow.mediaSourcePathControl removeLastInputNodePathControlItem]; - [self setMediaSourceViewToState:_currentPosition.navigationState]; } @@ -196,12 +190,13 @@ NSUInteger firstIndexToRemove = _currentPosition.navigationStackIndex + 1; // -1 to account for the array count NSRange rangeToRemove = NSMakeRange(firstIndexToRemove, (_navigationStates.count - 1) - _currentPosition.navigationStackIndex); - [self removeAndCleanUpStatesInRange:rangeToRemove]; [_navigationStates removeObjectsInRange:rangeToRemove]; } VLCLibraryMediaSourceViewNavigationState * const navigationState = [[VLCLibraryMediaSourceViewNavigationState alloc] initFromMediaSourceDataSource:self.baseDataSource.childDataSource]; + navigationState.pathControlItems = + self.libraryWindow.mediaSourcePathControl.orderedInputNodePathControlItems; _currentPosition = [[VLCLibraryMediaSourceViewNavigationCurrentStackPosition alloc] initWithStackIndex:_navigationStates.count andState:navigationState]; [_navigationStates addObject:navigationState]; @@ -209,22 +204,6 @@ [self updateDelegateNavigationButtons]; } -- (void)removeAndCleanUpStatesInRange:(NSRange)range -{ - NSAssert(range.location + range.length - 1 < _navigationStates.count, @"Invalid range for state removal and cleanup, out of bounds."); - - for (NSUInteger i = range.location; i < range.length; ++i) { - VLCLibraryMediaSourceViewNavigationState *state = [_navigationStates objectAtIndex:i]; - VLCInputNode *stateNode = state.currentNodeDisplayed; - - if (stateNode) { - [state.currentMediaSource.displayedMediaSource clearChildNodesForNode:stateNode.vlcInputItemNode]; - } - - [_navigationStates removeObjectAtIndex:i]; - } -} - - (void)updateDelegateNavigationButtons { if (self.libraryWindow == nil) { @@ -239,6 +218,7 @@ { [self.baseDataSource setChildDataSource:state.currentMediaSource]; [self.baseDataSource.childDataSource setNodeToDisplay:state.currentNodeDisplayed]; + [self.libraryWindow.mediaSourcePathControl setPathWithInputNodePathControlItems:state.pathControlItems]; [self updateDelegateNavigationButtons]; } ===================================== modules/gui/macosx/library/media-source/VLCLibraryMediaSourceViewNavigationState.h ===================================== @@ -26,11 +26,15 @@ NS_ASSUME_NONNULL_BEGIN @class VLCMediaSourceDataSource; @class VLCInputNode; +@class VLCInputNodePathControlItem; @interface VLCLibraryMediaSourceViewNavigationState : NSObject @property (readonly) VLCMediaSourceDataSource *currentMediaSource; @property (readwrite) VLCInputNode *currentNodeDisplayed; +// Snapshot of the full breadcrumb shown while this state was current, so the +// path control can be rebuilt exactly when navigating back or forward to it. +@property (readwrite, nullable) NSArray<VLCInputNodePathControlItem *> *pathControlItems; - (instancetype)initFromMediaSourceDataSource:(VLCMediaSourceDataSource *)mediaSourceDataSource; ===================================== modules/gui/macosx/library/media-source/VLCMediaSource.h ===================================== @@ -47,7 +47,7 @@ extern NSString *VLCMediaSourcePreparsingEnded; - (nullable NSError *)preparseInputNodeWithinTree:(VLCInputNode *)inputNode; - (void)clearChildNodesForNode:(input_item_node_t*)inputNode; -- (nullable NSError *)generateChildNodesForDirectoryNode:(input_item_node_t *)directoryNode +- (nullable NSError *)generateChildNodesForDirectoryNode:(VLCInputNode *)directoryInputNode withUrl:(NSURL *)directoryUrl; @property (nonatomic, readonly) NSString *mediaSourceDescription; ===================================== modules/gui/macosx/library/media-source/VLCMediaSource.m ===================================== @@ -289,16 +289,7 @@ static const char *const remoteBrowseDescription = "Remote Browse"; if (_p_mediaSource->description == localDevicesDescription || _p_mediaSource->description == myFoldersDescription) { _p_mediaSource->description = NULL; - input_item_node_t **childrenNodes = _p_mediaSource->tree->root.pp_children; - if (childrenNodes) { - for (int i = 0; i <_p_mediaSource->tree->root.i_children; ++i) { - input_item_node_t *childNode = childrenNodes[i]; - input_item_node_RemoveNode(&(_p_mediaSource->tree->root), childNode); - input_item_node_Delete(childNode); - } - } - - free(_p_mediaSource->tree); + vlc_media_tree_Release(_p_mediaSource->tree); free(_p_mediaSource); _p_mediaSource = NULL; } else if (_p_mediaSource->description == remoteBrowseDescription) { @@ -329,10 +320,8 @@ static const char *const remoteBrowseDescription = "Remote Browse"; if (inputNode.inputItem.inputType == ITEM_TYPE_DIRECTORY && [inputNode.inputItem.MRL hasPrefix:@"file://"]) { - input_item_node_t *vlcInputNode = inputNode.vlcInputItemNode; NSURL *dirUrl = [NSURL URLWithString:inputNode.inputItem.MRL]; - - return [self generateChildNodesForDirectoryNode:vlcInputNode withUrl:dirUrl]; + return [self generateChildNodesForDirectoryNode:inputNode withUrl:dirUrl]; } vlc_media_tree_Preparse(_p_mediaSource->tree, _p_preparser, @@ -431,10 +420,13 @@ static const char *const remoteBrowseDescription = "Remote Browse"; }); } -- (NSError *)generateChildNodesForDirectoryNode:(input_item_node_t *)directoryNode +- (NSError *)generateChildNodesForDirectoryNode:(VLCInputNode *)directoryInputNode withUrl:(NSURL *)directoryUrl { - NSParameterAssert(directoryNode != NULL && directoryUrl != nil); + NSParameterAssert(directoryInputNode != NULL && directoryUrl != nil); + [directoryInputNode clearChildrenCache]; + input_item_node_t * const directoryNode = directoryInputNode.vlcInputItemNode; + @synchronized (self) { if (self.willStartGeneratingChildNodesForNodeHandler) { self.willStartGeneratingChildNodesForNodeHandler(directoryNode); @@ -459,6 +451,9 @@ static const char *const remoteBrowseDescription = "Remote Browse"; if (error) { NSLog(@"Failed to get directories: %@.", error); + if (self.didFinishGeneratingChildNodesForNodeHandler) { + self.didFinishGeneratingChildNodesForNodeHandler(directoryNode); + } return error; } ===================================== modules/gui/macosx/library/media-source/VLCMediaSourceBaseDataSource.m ===================================== @@ -623,8 +623,20 @@ referenceSizeForHeaderInSection:(NSInteger)section VLCInputNodePathControlItem * const matchingItem = [self.pathControl.inputNodePathControlItems objectForKey:itemNodeMrl]; if (matchingItem != nil) { + VLCInputNode * const currentNode = self.childDataSource.nodeToDisplay; + if (currentNode != nil && + [matchingItem.inputNode.inputItem.MRL isEqualToString:currentNode.inputItem.MRL]) { + return; + } + + // Jumping to an ancestor via the path control is a navigation in its own + // right. Record it as a new state (with the trimmed breadcrumb) so that + // pressing back returns to the descendant we were previously viewing, + // rather than to the parent of the clicked item. The breadcrumb for each + // state is restored wholesale on back/forward, so the path stays correct. self.childDataSource.nodeToDisplay = matchingItem.inputNode; [self.pathControl clearPathControlItemsAheadOf:selectedItem]; + [self.navigationStack appendCurrentLibraryState]; } else { NSLog(@"Could not find matching item for clicked path item: %@", selectedItem); } ===================================== modules/gui/macosx/library/media-source/VLCMediaSourceDataSource.m ===================================== @@ -134,8 +134,6 @@ NSString * const VLCMediaSourceDataSourceNodeChanged = @"VLCMediaSourceDataSourc NSAssert(nodeToDisplay, @"Nil node to display, will not set"); _nodeToDisplay = nodeToDisplay; - input_item_node_t * const inputNode = nodeToDisplay.vlcInputItemNode; - NSParameterAssert(self.parentBaseDataSource); if (self.parentBaseDataSource.mediaSourceMode == VLCMediaSourceModeLAN) { NSURL * const nodeUrl = [NSURL URLWithString:nodeToDisplay.inputItem.MRL]; @@ -145,16 +143,9 @@ NSString * const VLCMediaSourceDataSourceNodeChanged = @"VLCMediaSourceDataSourc return; } - NSError * const error = - [self.displayedMediaSource generateChildNodesForDirectoryNode:inputNode - withUrl:nodeUrl]; - if (error) { - dispatch_async(dispatch_get_main_queue(), ^{ - NSAlert * const alert = [NSAlert alertWithError:error]; - alert.alertStyle = NSAlertStyleCritical; - [alert runModal]; - }); - return; + if (self.observedPathDispatchSource) { + dispatch_source_cancel(self.observedPathDispatchSource); + self.observedPathDispatchSource = nil; } const __weak typeof(self) weakSelf = self; @@ -171,7 +162,7 @@ NSString * const VLCMediaSourceDataSourceNodeChanged = @"VLCMediaSourceDataSourc }); } else { dispatch_async(dispatch_get_main_queue(), ^{ - [weakSelf.displayedMediaSource generateChildNodesForDirectoryNode:inputNode + [weakSelf.displayedMediaSource generateChildNodesForDirectoryNode:nodeToDisplay withUrl:nodeUrl]; [weakSelf reloadData]; }); @@ -321,12 +312,15 @@ NSString * const VLCMediaSourceDataSourceNodeChanged = @"VLCMediaSourceDataSourc cellView.textField.stringValue = NSTR("Loading…"); dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{ NSURL * const inputNodeUrl = [NSURL URLWithString:inputNode.inputItem.MRL]; - input_item_node_t * const p_inputNode = inputNode.vlcInputItemNode; NSError * const error = - [self.displayedMediaSource generateChildNodesForDirectoryNode:p_inputNode + [self.displayedMediaSource generateChildNodesForDirectoryNode:inputNode withUrl:inputNodeUrl]; - if (error) + if (error) { + dispatch_async(dispatch_get_main_queue(), ^{ + cellView.textField.stringValue = NSTR("Unavailable"); + }); return; + } dispatch_async(dispatch_get_main_queue(), ^{ cellView.textField.stringValue = View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/c8d61664c5671e99f644eecf7ad482dc3eb57af4...e65821678553077d4b11fe01b1c4e48f47432799 -- View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/c8d61664c5671e99f644eecf7ad482dc3eb57af4...e65821678553077d4b11fe01b1c4e48f47432799 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
_______________________________________________ vlc-commits mailing list [email protected] https://mailman.videolan.org/listinfo/vlc-commits
