diff --git a/Headers/AppKit/NSScrubber.h b/Headers/AppKit/NSScrubber.h index da5223145..15e727474 100644 --- a/Headers/AppKit/NSScrubber.h +++ b/Headers/AppKit/NSScrubber.h @@ -29,12 +29,415 @@ #if OS_API_VERSION(MAC_OS_X_VERSION_10_12, GS_API_LATEST) +#import "AppKit/NSScrubberLayout.h" +#import "AppKit/NSScrubberItemView.h" + #if defined(__cplusplus) extern "C" { #endif +@class NSScrubberLayout, NSColor, NSView; +@protocol NSScrubberDataSource, NSScrubberDelegate; + +/** + * Alignment options for scrubber items. + */ +typedef enum _NSScrubberAlignment { + NSScrubberAlignmentNone, + NSScrubberAlignmentLeading, + NSScrubberAlignmentTrailing, + NSScrubberAlignmentCenter +} NSScrubberAlignment; + +/** + * Mode options for scrubber interaction. + */ +typedef enum _NSScrubberMode { + NSScrubberModeFree, + NSScrubberModeFixed +} NSScrubberMode; + +/** + * Selection background style options. + */ +typedef enum _NSScrubberSelectionStyle { + NSScrubberSelectionStyleNone, + NSScrubberSelectionStyleOutline, + NSScrubberSelectionStyleRoundedBackground +} NSScrubberSelectionStyle; + +/** + * Protocol defining data source methods for NSScrubber. + * The data source provides the scrubber with information about the number of items + * and the view to display for each item. + */ +@protocol NSScrubberDataSource + +@required +/** + * Returns the number of items in the scrubber. + * scrubber is the scrubber requesting this information. + * Returns the number of items to display. + */ +- (NSInteger) numberOfItemsForScrubber: (NSScrubber *)scrubber; + +/** + * Returns the view to display at the specified index. + * scrubber is the scrubber requesting the view. + * index is the index of the item. + * Returns the view to display for the item at the given index. + */ +- (NSScrubberItemView *) scrubber: (NSScrubber *)scrubber + viewForItemAt: (NSInteger)index; + +@end + +/** + * Protocol defining delegate methods for NSScrubber. + * The delegate receives notifications about user interactions and state changes. + */ +@protocol NSScrubberDelegate + +@optional +/** + * Called when the user selects an item in the scrubber. + * scrubber is the scrubber in which the selection occurred. + * selectedIndex is the index of the selected item. + */ +- (void) scrubber: (NSScrubber *)scrubber + didSelectItemAt: (NSInteger)selectedIndex; + +/** + * Called when the user highlights an item in the scrubber. + * scrubber is the scrubber in which the highlighting occurred. + * highlightedIndex is the index of the highlighted item. + */ +- (void) scrubber: (NSScrubber *)scrubber +didHighlightItemAt: (NSInteger)highlightedIndex; + +/** + * Called when the user begins interacting with the scrubber. + * scrubber is the scrubber with which interaction began. + */ +- (void) didBeginInteractingWithScrubber: (NSScrubber *)scrubber; + +/** + * Called when the user finishes interacting with the scrubber. + * scrubber is the scrubber with which interaction finished. + */ +- (void) didFinishInteractingWithScrubber: (NSScrubber *)scrubber; + +/** + * Called when the user cancels interaction with the scrubber. + * scrubber is the scrubber with which interaction was cancelled. + */ +- (void) didCancelInteractingWithScrubber: (NSScrubber *)scrubber; + +@end + +/** + * NSScrubber is a horizontal scrolling list control for displaying a set of items + * in the Touch Bar or other interface elements. It provides smooth scrolling and + * selection capabilities for navigating through collections of items. + */ APPKIT_EXPORT_CLASS @interface NSScrubber : NSView +{ + // Data source and delegate + id _dataSource; + id _delegate; + + // Layout and appearance + NSScrubberLayout *_scrubberLayout; + NSColor *_backgroundColor; + NSView *_backgroundView; + NSScrubberAlignment _itemAlignment; + + // Selection and highlighting + NSInteger _selectedIndex; + NSInteger _highlightedIndex; + NSView *_selectionBackgroundStyle; + NSView *_selectionOverlayStyle; + BOOL _floatsSelectionViews; + + // Behavior + NSScrubberMode _mode; + BOOL _continuous; + BOOL _showsArrowButtons; + BOOL _showsAdditionalContentIndicators; + + // Item management + NSMutableDictionary *_registeredClasses; + NSMutableDictionary *_registeredNibs; + NSMutableArray *_itemViews; + NSMutableArray *_reusableItemViews; + BOOL _isUpdating; + BOOL _needsReload; +} + +// MARK: - Initialization + +/** + * Initializes a scrubber with the specified frame. + * frameRect is the frame rectangle for the scrubber. + * Returns an initialized scrubber object. + */ +- (id) initWithFrame: (NSRect)frameRect; + +// MARK: - Data Source and Delegate + +/** + * Returns the object that acts as the data source of the scrubber. + */ +- (id) dataSource; + +/** + * Sets the object that acts as the data source of the scrubber. + * dataSource is the data source object. + */ +- (void) setDataSource: (id)dataSource; + +/** + * Returns the object that acts as the delegate of the scrubber. + */ +- (id) delegate; + +/** + * Sets the object that acts as the delegate of the scrubber. + * delegate is the delegate object. + */ +- (void) setDelegate: (id)delegate; + +// MARK: - Layout and Appearance + +/** + * Returns the layout object used to arrange items in the scrubber. + */ +- (NSScrubberLayout *) scrubberLayout; + +/** + * Sets the layout object used to arrange items in the scrubber. + * scrubberLayout is the layout object. + */ +- (void) setScrubberLayout: (NSScrubberLayout *)scrubberLayout; + +/** + * Returns the background color of the scrubber. + */ +- (NSColor *) backgroundColor; + +/** + * Sets the background color of the scrubber. + * backgroundColor is the background color. + */ +- (void) setBackgroundColor: (NSColor *)backgroundColor; + +/** + * Returns the background view displayed behind the scrubber items. + */ +- (NSView *) backgroundView; + +/** + * Sets the background view displayed behind the scrubber items. + * backgroundView is the background view. + */ +- (void) setBackgroundView: (NSView *)backgroundView; + +/** + * Returns the alignment of items within the scrubber. + */ +- (NSScrubberAlignment) itemAlignment; + +/** + * Sets the alignment of items within the scrubber. + * itemAlignment is the alignment value. + */ +- (void) setItemAlignment: (NSScrubberAlignment)itemAlignment; + +// MARK: - Selection and Highlighting + +/** + * Returns the index of the currently selected item, or NSNotFound if no item is selected. + */ +- (NSInteger) selectedIndex; + +/** + * Sets the index of the currently selected item. + * selectedIndex is the index to select, or NSNotFound for no selection. + */ +- (void) setSelectedIndex: (NSInteger)selectedIndex; + +/** + * Returns the index of the currently highlighted item, or NSNotFound if no item is highlighted. + */ +- (NSInteger) highlightedIndex; + +/** + * Sets the index of the currently highlighted item. + * highlightedIndex is the index to highlight, or NSNotFound for no highlighting. + */ +- (void) setHighlightedIndex: (NSInteger)highlightedIndex; + +/** + * Returns the selection background style for selected items. + */ +- (NSView *) selectionBackgroundStyle; + +/** + * Sets the selection background style for selected items. + * selectionBackgroundStyle is the background style view. + */ +- (void) setSelectionBackgroundStyle: (NSView *)selectionBackgroundStyle; + +/** + * Returns the selection overlay style for selected items. + */ +- (NSView *) selectionOverlayStyle; + +/** + * Sets the selection overlay style for selected items. + * selectionOverlayStyle is the overlay style view. + */ +- (void) setSelectionOverlayStyle: (NSView *)selectionOverlayStyle; + +/** + * Returns whether selection views float above items. + */ +- (BOOL) floatsSelectionViews; + +/** + * Sets whether selection views float above items. + * floatsSelectionViews is YES to float selection views, NO otherwise. + */ +- (void) setFloatsSelectionViews: (BOOL)floatsSelectionViews; + +// MARK: - Behavior + +/** + * Returns the interaction mode of the scrubber. + */ +- (NSScrubberMode) mode; + +/** + * Sets the interaction mode of the scrubber. + * mode is the interaction mode. + */ +- (void) setMode: (NSScrubberMode)mode; + +/** + * Returns whether the scrubber sends actions continuously. + */ +- (BOOL) continuous; + +/** + * Sets whether the scrubber sends actions continuously. + * continuous is YES to send actions continuously, NO otherwise. + */ +- (void) setContinuous: (BOOL)continuous; + +/** + * Returns whether arrow buttons are shown at the edges. + */ +- (BOOL) showsArrowButtons; + +/** + * Sets whether arrow buttons are shown at the edges. + * showsArrowButtons is YES to show arrow buttons, NO otherwise. + */ +- (void) setShowsArrowButtons: (BOOL)showsArrowButtons; + +/** + * Returns whether additional content indicators are shown. + */ +- (BOOL) showsAdditionalContentIndicators; + +/** + * Sets whether additional content indicators are shown. + * showsAdditionalContentIndicators is YES to show indicators, NO otherwise. + */ +- (void) setShowsAdditionalContentIndicators: (BOOL)showsAdditionalContentIndicators; + +// MARK: - Item Management + +/** + * Returns the total number of items in the scrubber. + */ +- (NSInteger) numberOfItems; + +/** + * Reloads all data in the scrubber. + */ +- (void) reloadData; + +/** + * Reloads the items at the specified indexes. + * indexes specifies the indexes of the items to reload. + */ +- (void) reloadItemsAtIndexes: (NSIndexSet *)indexes; + +/** + * Inserts new items at the specified indexes. + * indexes specifies the indexes at which to insert items. + */ +- (void) insertItemsAtIndexes: (NSIndexSet *)indexes; + +/** + * Removes items at the specified indexes. + * indexes specifies the indexes of the items to remove. + */ +- (void) removeItemsAtIndexes: (NSIndexSet *)indexes; + +/** + * Moves an item from one index to another. + * fromIndex is the current index of the item. + * toIndex is the destination index for the item. + */ +- (void) moveItemAtIndex: (NSInteger)fromIndex + toIndex: (NSInteger)toIndex; + +// MARK: - Item Views and Registration + +/** + * Returns the item view for the item at the specified index. + * index is the index of the item. + * Returns the item view, or nil if the item is not visible. + */ +- (NSScrubberItemView *) itemViewForItemAtIndex: (NSInteger)index; + +/** + * Registers a class to use when creating new item views. + * itemViewClass is the class to register. + * identifier is the reuse identifier for the class. + */ +- (void) registerClass: (Class)itemViewClass + forItemIdentifier: (NSString *)identifier; + +/** + * Registers a nib to use when creating new item views. + * nib is the nib to register. + * identifier is the reuse identifier for the nib. + */ +- (void) registerNib: (NSNib *)nib + forItemIdentifier: (NSString *)identifier; + +/** + * Returns a reusable item view located by its identifier. + * identifier is the reuse identifier. + * owner is the owner object for loading the nib. + * Returns a reusable item view or a newly created one. + */ +- (NSScrubberItemView *) makeItemWithIdentifier: (NSString *)identifier + owner: (id)owner; + +// MARK: - Scrolling + +/** + * Scrolls the scrubber to make the item at the specified index visible. + * index is the index of the item to scroll to. + * alignment is the alignment for positioning the item. + */ +- (void) scrollItemAtIndex: (NSInteger)index + toAlignment: (NSScrubberAlignment)alignment; @end diff --git a/Headers/AppKit/NSScrubberItemView.h b/Headers/AppKit/NSScrubberItemView.h index 4eb804413..7d923d4fc 100644 --- a/Headers/AppKit/NSScrubberItemView.h +++ b/Headers/AppKit/NSScrubberItemView.h @@ -33,13 +33,42 @@ extern "C" { #endif +/** + * NSScrubberArrangedView is the base class for views that can be arranged + * within a scrubber layout. + */ APPKIT_EXPORT_CLASS @interface NSScrubberArrangedView : NSView @end +/** + * NSScrubberItemView represents an individual item within an NSScrubber. + * This is the base class for custom item views in a scrubber control. + */ APPKIT_EXPORT_CLASS @interface NSScrubberItemView : NSScrubberArrangedView +{ + NSString *_reuseIdentifier; +} + +/** + * Returns the reuse identifier associated with this item view. + * Used by the scrubber to efficiently reuse item views. + */ +- (NSString *) reuseIdentifier; + +/** + * Sets the reuse identifier associated with this item view. + * reuseIdentifier is the identifier string. + */ +- (void) setReuseIdentifier: (NSString *)reuseIdentifier; + +/** + * Prepares the item view for reuse by clearing any view-specific content. + * Subclasses should override this method to reset their content to a default state. + */ +- (void) prepareForReuse; @end diff --git a/Headers/AppKit/NSScrubberLayout.h b/Headers/AppKit/NSScrubberLayout.h index e22a97bb1..09cb2454f 100644 --- a/Headers/AppKit/NSScrubberLayout.h +++ b/Headers/AppKit/NSScrubberLayout.h @@ -37,50 +37,153 @@ extern "C" { #endif @class NSScrubber; - + +/** + * NSScrubberLayoutAttributes defines the layout information for an item + * in a scrubber layout. It encapsulates properties like frame, alpha, + * and item index. + */ APPKIT_EXPORT_CLASS -@interface NSScrubberLayoutAttributes : NSObject +@interface NSScrubberLayoutAttributes : NSObject +{ + CGFloat _alpha; + NSRect _frame; + NSInteger _itemIndex; +} +/** + * Creates layout attributes for an item at the specified index. + * index is the index of the item. + * Returns a new layout attributes object. + */ + (NSScrubberLayoutAttributes *) layoutAttributesForItemAtIndex: (NSInteger)index; +/** + * Returns the alpha (opacity) value for the item. + */ - (CGFloat) alpha; +/** + * Sets the alpha (opacity) value for the item. + * alpha is the opacity value. + */ +- (void) setAlpha: (CGFloat)alpha; + +/** + * Returns the frame rectangle for the item. + */ - (NSRect) frame; +/** + * Sets the frame rectangle for the item. + * frame is the frame rectangle. + */ +- (void) setFrame: (NSRect)frame; + +/** + * Returns the index of the item these attributes represent. + */ - (NSInteger) itemIndex; +/** + * Sets the index of the item these attributes represent. + * itemIndex is the item index. + */ +- (void) setItemIndex: (NSInteger)itemIndex; + @end - + +/** + * NSScrubberLayout is an abstract base class for layout objects that + * arrange items within a scrubber. Subclasses implement specific layout + * strategies such as flow layout or proportional layout. + */ APPKIT_EXPORT_CLASS @interface NSScrubberLayout : NSObject +{ + NSScrubber *_scrubber; // weak reference +} -// Configuring +// MARK: - Layout Configuration + +/** + * Returns the class used for layout attributes. + * Subclasses can override this to provide custom attribute classes. + * Returns the layout attributes class. + */ - (Class) layoutAttributesClass; +/** + * Returns the scrubber that owns this layout object. + */ - (NSScrubber *) scrubber; +/** + * Returns the visible rectangle of the scrubber. + */ - (NSRect) visibleRect; +/** + * Invalidates the current layout, causing it to be recalculated. + */ - (void) invalidateLayout; -// Subclassing layout +// MARK: - Subclassing Methods + +/** + * Prepares the layout for use. + * Subclasses should override this method to perform any necessary setup + * before layout attributes are requested. + */ - (void) prepareLayout; +/** + * Returns the total content size required for all items. + * Returns the content size. + */ - (NSSize) scrubberContentSize; +/** + * Returns the layout attributes for the item at the specified index. + * index is the index of the item. + * Returns the layout attributes for the item. + */ - (NSScrubberLayoutAttributes *) layoutAttributesForItemAtIndex: (NSInteger)index; -- (NSScrubberLayoutAttributes *) layoutAttributesForItemsInRect: (NSRect)rect; - +/** + * Returns an array of layout attributes for items in the specified rectangle. + * rect is the rectangle to query. + * Returns an array of layout attributes. + */ +- (NSArray *) layoutAttributesForItemsInRect: (NSRect)rect; + +/** + * Returns whether the layout should be invalidated when highlighting changes. + * Returns YES if the layout should be invalidated, NO otherwise. + */ - (BOOL) shouldInvalidateLayoutForHighlightChange; +/** + * Returns whether the layout should be invalidated when selection changes. + * Returns YES if the layout should be invalidated, NO otherwise. + */ - (BOOL) shouldInvalidateLayoutForSelectionChange; +/** + * Returns whether the layout should be invalidated for visible rect changes. + * fromRect is the previous visible rectangle. + * toRect is the new visible rectangle. + * Returns YES if the layout should be invalidated, NO otherwise. + */ - (BOOL) shouldInvalidateLayoutForChangeFromVisibleRect: (NSRect)fromRect toVisibleRect: (NSRect)toRect; +/** + * Returns whether the layout automatically mirrors in right-to-left layouts. + * Returns YES if the layout mirrors automatically, NO otherwise. + */ - (BOOL) automaticallyMirrorsInRightToLeftLayout; - + @end diff --git a/Source/NSScrubber.m b/Source/NSScrubber.m index 45b4b3423..4873dc4e5 100644 --- a/Source/NSScrubber.m +++ b/Source/NSScrubber.m @@ -23,8 +23,856 @@ */ #import "AppKit/NSScrubber.h" +#import "AppKit/NSColor.h" +#import "AppKit/NSNib.h" +#import "AppKit/NSScrollView.h" +#import "AppKit/NSNib.h" + +#import +#import +#import +#import +#import + +// Private interface for internal state management +@interface NSScrubber() + +- (void) _commonInit; +- (void) _layoutItemViews; +- (void) _updateItemViewsIfNeeded; +- (NSScrubberItemView *) _dequeueReusableItemViewWithIdentifier: (NSString *)identifier; +- (void) _enqueueReusableItemView: (NSScrubberItemView *)itemView; +- (void) _setNeedsLayout; +- (void) _sendDelegateDidSelectItemAtIndex: (NSInteger)index; +- (void) _sendDelegateDidHighlightItemAtIndex: (NSInteger)index; +- (void) _sendDelegateDidBeginInteracting; +- (void) _sendDelegateDidFinishInteracting; +- (void) _sendDelegateDidCancelInteracting; + +@end @implementation NSScrubber +// MARK: - Class Methods + ++ (void) initialize +{ + if (self == [NSScrubber class]) + { + [self setVersion: 1]; + } +} + +// MARK: - Initialization + +- (id) init +{ + return [self initWithFrame: NSZeroRect]; +} + +- (id) initWithFrame: (NSRect)frameRect +{ + self = [super initWithFrame: frameRect]; + if (self) + { + [self _commonInit]; + } + return self; +} + +- (id) initWithCoder: (NSCoder *)coder +{ + self = [super initWithCoder: coder]; + if (self) + { + [self _commonInit]; + + // Decode properties from the coder if needed + if ([coder containsValueForKey: @"NSScrubber.backgroundColor"]) + { + _backgroundColor = [coder decodeObjectForKey: @"NSScrubber.backgroundColor"]; + } + + if ([coder containsValueForKey: @"NSScrubber.selectedIndex"]) + { + _selectedIndex = [coder decodeIntegerForKey: @"NSScrubber.selectedIndex"]; + } + + if ([coder containsValueForKey: @"NSScrubber.highlightedIndex"]) + { + _highlightedIndex = [coder decodeIntegerForKey: @"NSScrubber.highlightedIndex"]; + } + + if ([coder containsValueForKey: @"NSScrubber.itemAlignment"]) + { + _itemAlignment = [coder decodeIntegerForKey: @"NSScrubber.itemAlignment"]; + } + + if ([coder containsValueForKey: @"NSScrubber.mode"]) + { + _mode = [coder decodeIntegerForKey: @"NSScrubber.mode"]; + } + + if ([coder containsValueForKey: @"NSScrubber.continuous"]) + { + _continuous = [coder decodeBoolForKey: @"NSScrubber.continuous"]; + } + + if ([coder containsValueForKey: @"NSScrubber.showsArrowButtons"]) + { + _showsArrowButtons = [coder decodeBoolForKey: @"NSScrubber.showsArrowButtons"]; + } + + if ([coder containsValueForKey: @"NSScrubber.showsAdditionalContentIndicators"]) + { + _showsAdditionalContentIndicators = [coder decodeBoolForKey: @"NSScrubber.showsAdditionalContentIndicators"]; + } + + if ([coder containsValueForKey: @"NSScrubber.floatsSelectionViews"]) + { + _floatsSelectionViews = [coder decodeBoolForKey: @"NSScrubber.floatsSelectionViews"]; + } + } + return self; +} + +- (void) encodeWithCoder: (NSCoder *)coder +{ + [super encodeWithCoder: coder]; + + [coder encodeObject: _backgroundColor forKey: @"NSScrubber.backgroundColor"]; + [coder encodeInteger: _selectedIndex forKey: @"NSScrubber.selectedIndex"]; + [coder encodeInteger: _highlightedIndex forKey: @"NSScrubber.highlightedIndex"]; + [coder encodeInteger: _itemAlignment forKey: @"NSScrubber.itemAlignment"]; + [coder encodeInteger: _mode forKey: @"NSScrubber.mode"]; + [coder encodeBool: _continuous forKey: @"NSScrubber.continuous"]; + [coder encodeBool: _showsArrowButtons forKey: @"NSScrubber.showsArrowButtons"]; + [coder encodeBool: _showsAdditionalContentIndicators forKey: @"NSScrubber.showsAdditionalContentIndicators"]; + [coder encodeBool: _floatsSelectionViews forKey: @"NSScrubber.floatsSelectionViews"]; +} + +- (void) dealloc +{ + [_registeredClasses release]; + [_registeredNibs release]; + [_itemViews release]; + [_reusableItemViews release]; + [_backgroundColor release]; + [_backgroundView release]; + [_scrubberLayout release]; + [_selectionBackgroundStyle release]; + [_selectionOverlayStyle release]; + + [super dealloc]; +} + +// MARK: - Private Methods + +- (void) _commonInit +{ + _registeredClasses = [[NSMutableDictionary alloc] init]; + _registeredNibs = [[NSMutableDictionary alloc] init]; + _itemViews = [[NSMutableArray alloc] init]; + _reusableItemViews = [[NSMutableArray alloc] init]; + + // Set default values + _selectedIndex = NSNotFound; + _highlightedIndex = NSNotFound; + _itemAlignment = NSScrubberAlignmentNone; + _mode = NSScrubberModeFree; + _continuous = YES; + _showsArrowButtons = NO; + _showsAdditionalContentIndicators = NO; + _floatsSelectionViews = YES; + _isUpdating = NO; + _needsReload = YES; + + // Set up default background color + _backgroundColor = [[NSColor clearColor] retain]; +} + +- (void) _layoutItemViews +{ + if (_scrubberLayout) + { + [_scrubberLayout prepareLayout]; + + NSSize contentSize = [_scrubberLayout scrubberContentSize]; + NSInteger i = 0; + + // Layout each visible item view + for (i = 0; i < [_itemViews count]; i++) + { + NSScrubberItemView *itemView = [_itemViews objectAtIndex: i]; + if (itemView && itemView != (id)[NSNull null]) + { + NSScrubberLayoutAttributes *attributes = [_scrubberLayout layoutAttributesForItemAtIndex: i]; + if (attributes) + { + [itemView setFrame: [attributes frame]]; + [itemView setAlphaValue: [attributes alpha]]; + } + } + } + } + + [self setNeedsDisplay: YES]; +} + +- (void) _updateItemViewsIfNeeded +{ + if (_needsReload && !_isUpdating) + { + [self reloadData]; + } +} + +- (NSScrubberItemView *) _dequeueReusableItemViewWithIdentifier: (NSString *)identifier +{ + NSInteger i = 0; + for (i = [_reusableItemViews count] - 1; i >= 0; i--) + { + NSScrubberItemView *itemView = [_reusableItemViews objectAtIndex: i]; + if ([[itemView reuseIdentifier] isEqualToString: identifier]) + { + [itemView retain]; + [_reusableItemViews removeObjectAtIndex: i]; + return [itemView autorelease]; + } + } + return nil; +} + +- (void) _enqueueReusableItemView: (NSScrubberItemView *)itemView +{ + if (itemView) + { + [itemView removeFromSuperview]; + [itemView prepareForReuse]; + [_reusableItemViews addObject: itemView]; + } +} + +- (void) _setNeedsLayout +{ + [self setNeedsLayout: YES]; + [self setNeedsDisplay: YES]; +} + +// MARK: - Delegate Methods + +- (void) _sendDelegateDidSelectItemAtIndex: (NSInteger)index +{ + if (_delegate && [_delegate respondsToSelector: @selector(scrubber:didSelectItemAt:)]) + { + [_delegate scrubber: self didSelectItemAt: index]; + } +} + +- (void) _sendDelegateDidHighlightItemAtIndex: (NSInteger)index +{ + if (_delegate && [_delegate respondsToSelector: @selector(scrubber:didHighlightItemAt:)]) + { + [_delegate scrubber: self didHighlightItemAt: index]; + } +} + +- (void) _sendDelegateDidBeginInteracting +{ + if (_delegate && [_delegate respondsToSelector: @selector(didBeginInteractingWithScrubber:)]) + { + [_delegate didBeginInteractingWithScrubber: self]; + } +} + +- (void) _sendDelegateDidFinishInteracting +{ + if (_delegate && [_delegate respondsToSelector: @selector(didFinishInteractingWithScrubber:)]) + { + [_delegate didFinishInteractingWithScrubber: self]; + } +} + +- (void) _sendDelegateDidCancelInteracting +{ + if (_delegate && [_delegate respondsToSelector: @selector(didCancelInteractingWithScrubber:)]) + { + [_delegate didCancelInteractingWithScrubber: self]; + } +} + +// MARK: - Property Accessors + +- (void) setDataSource: (id)dataSource +{ + if (_dataSource != dataSource) + { + _dataSource = dataSource; + _needsReload = YES; + [self _setNeedsLayout]; + } +} + +- (void) setScrubberLayout: (NSScrubberLayout *)scrubberLayout +{ + if (_scrubberLayout != scrubberLayout) + { + [_scrubberLayout release]; + _scrubberLayout = [scrubberLayout retain]; + + // Set the scrubber reference in the layout + if (_scrubberLayout && [_scrubberLayout respondsToSelector: @selector(_setScrubber:)]) + { + [_scrubberLayout performSelector: @selector(_setScrubber:) withObject: self]; + } + + [self _setNeedsLayout]; + } +} + +- (void) setBackgroundColor: (NSColor *)backgroundColor +{ + if (_backgroundColor != backgroundColor) + { + [_backgroundColor release]; + _backgroundColor = [backgroundColor retain]; + [self setNeedsDisplay: YES]; + } +} + +- (void) setBackgroundView: (NSView *)backgroundView +{ + if (_backgroundView != backgroundView) + { + [_backgroundView removeFromSuperview]; + [_backgroundView release]; + _backgroundView = [backgroundView retain]; + + if (_backgroundView) + { + [self addSubview: _backgroundView positioned: NSWindowBelow relativeTo: nil]; + } + [self setNeedsDisplay: YES]; + } +} + +- (void) setSelectedIndex: (NSInteger)selectedIndex +{ + if (_selectedIndex != selectedIndex) + { + NSInteger oldIndex = _selectedIndex; + _selectedIndex = selectedIndex; + + // Update selection appearance + [self setNeedsDisplay: YES]; + + if (_continuous) + { + [self _sendDelegateDidSelectItemAtIndex: selectedIndex]; + } + } +} + +- (void) setHighlightedIndex: (NSInteger)highlightedIndex +{ + if (_highlightedIndex != highlightedIndex) + { + _highlightedIndex = highlightedIndex; + [self setNeedsDisplay: YES]; + [self _sendDelegateDidHighlightItemAtIndex: highlightedIndex]; + } +} + +- (void) setItemAlignment: (NSScrubberAlignment)itemAlignment +{ + if (_itemAlignment != itemAlignment) + { + _itemAlignment = itemAlignment; + [self _setNeedsLayout]; + } +} + +- (void) setMode: (NSScrubberMode)mode +{ + if (_mode != mode) + { + _mode = mode; + [self _setNeedsLayout]; + } +} + +- (void) setContinuous: (BOOL)continuous +{ + _continuous = continuous; +} + +- (void) setShowsArrowButtons: (BOOL)showsArrowButtons +{ + if (_showsArrowButtons != showsArrowButtons) + { + _showsArrowButtons = showsArrowButtons; + [self setNeedsDisplay: YES]; + } +} + +- (void) setShowsAdditionalContentIndicators: (BOOL)showsAdditionalContentIndicators +{ + if (_showsAdditionalContentIndicators != showsAdditionalContentIndicators) + { + _showsAdditionalContentIndicators = showsAdditionalContentIndicators; + [self setNeedsDisplay: YES]; + } +} + +- (void) setFloatsSelectionViews: (BOOL)floatsSelectionViews +{ + if (_floatsSelectionViews != floatsSelectionViews) + { + _floatsSelectionViews = floatsSelectionViews; + [self setNeedsDisplay: YES]; + } +} + +- (NSInteger) numberOfItems +{ + if (_dataSource && [_dataSource respondsToSelector: @selector(numberOfItemsForScrubber:)]) + { + return [_dataSource numberOfItemsForScrubber: self]; + } + return 0; +} + +// MARK: - Item Management + +- (void) reloadData +{ + _isUpdating = YES; + + // Remove all current item views + NSEnumerator *enumerator = [_itemViews objectEnumerator]; + NSScrubberItemView *itemView; + while ((itemView = [enumerator nextObject])) + { + if (itemView && itemView != (id)[NSNull null]) + { + [self _enqueueReusableItemView: itemView]; + } + } + [_itemViews removeAllObjects]; + + // Get the number of items from the data source + NSInteger itemCount = [self numberOfItems]; + NSInteger i = 0; + + // Create placeholder array for item views + for (i = 0; i < itemCount; i++) + { + [_itemViews addObject: [NSNull null]]; + } + + // Load visible item views + if (_dataSource && [_dataSource respondsToSelector: @selector(scrubber:viewForItemAt:)]) + { + // For now, load all items (in a real implementation, this would be optimized for visible items only) + for (NSInteger i = 0; i < itemCount; i++) + { + NSScrubberItemView *itemView = [_dataSource scrubber: self viewForItemAt: i]; + if (itemView) + { + [_itemViews replaceObjectAtIndex: i withObject: itemView]; + [self addSubview: itemView]; + } + } + } + + _needsReload = NO; + _isUpdating = NO; + + [self _layoutItemViews]; +} + +- (void) reloadItemsAtIndexes: (NSIndexSet *)indexes +{ + if (!_dataSource || _isUpdating) + return; + + NSUInteger idx = [indexes firstIndex]; + while (idx != NSNotFound) + { + if (idx < [_itemViews count]) + { + // Remove existing item view + NSScrubberItemView *oldItemView = [_itemViews objectAtIndex: idx]; + if (oldItemView && oldItemView != (id)[NSNull null]) + { + [self _enqueueReusableItemView: oldItemView]; + } + + // Load new item view + if ([_dataSource respondsToSelector: @selector(scrubber:viewForItemAt:)]) + { + NSScrubberItemView *newItemView = [_dataSource scrubber: self viewForItemAt: idx]; + if (newItemView) + { + [_itemViews replaceObjectAtIndex: idx withObject: newItemView]; + [self addSubview: newItemView]; + } + else + { + [_itemViews replaceObjectAtIndex: idx withObject: [NSNull null]]; + } + } + } + idx = [indexes indexGreaterThanIndex: idx]; + } + + [self _layoutItemViews]; +} + +- (void) insertItemsAtIndexes: (NSIndexSet *)indexes +{ + if (_isUpdating) + return; + + _isUpdating = YES; + + // Insert null placeholders for the new items (in reverse order) + NSUInteger idx = [indexes lastIndex]; + while (idx != NSNotFound) + { + [_itemViews insertObject: [NSNull null] atIndex: idx]; + idx = [indexes indexLessThanIndex: idx]; + } + + // Reload the inserted items + [self reloadItemsAtIndexes: indexes]; + + _isUpdating = NO; +} + +- (void) removeItemsAtIndexes: (NSIndexSet *)indexes +{ + if (_isUpdating) + return; + + _isUpdating = YES; + + // Remove item views and update selection/highlighting (in reverse order) + NSUInteger idx = [indexes lastIndex]; + while (idx != NSNotFound) + { + if (idx < [_itemViews count]) + { + NSScrubberItemView *itemView = [_itemViews objectAtIndex: idx]; + if (itemView && itemView != (id)[NSNull null]) + { + [self _enqueueReusableItemView: itemView]; + } + [_itemViews removeObjectAtIndex: idx]; + } + + // Update selection and highlighting indices + if (_selectedIndex != NSNotFound && _selectedIndex >= (NSInteger)idx) + { + if (_selectedIndex == (NSInteger)idx) + { + _selectedIndex = NSNotFound; + } + else + { + _selectedIndex--; + } + } + + if (_highlightedIndex != NSNotFound && _highlightedIndex >= (NSInteger)idx) + { + if (_highlightedIndex == (NSInteger)idx) + { + _highlightedIndex = NSNotFound; + } + else + { + _highlightedIndex--; + } + } + idx = [indexes indexLessThanIndex: idx]; + } + + _isUpdating = NO; + [self _layoutItemViews]; +} + +- (void) moveItemAtIndex: (NSInteger)fromIndex + toIndex: (NSInteger)toIndex +{ + if (_isUpdating || fromIndex == toIndex || + fromIndex < 0 || fromIndex >= [_itemViews count] || + toIndex < 0 || toIndex >= [_itemViews count]) + return; + + _isUpdating = YES; + + // Move the item view + NSScrubberItemView *itemView = [[_itemViews objectAtIndex: fromIndex] retain]; + [_itemViews removeObjectAtIndex: fromIndex]; + [_itemViews insertObject: itemView atIndex: toIndex]; + [itemView release]; + + // Update selection and highlighting indices + if (_selectedIndex == fromIndex) + { + _selectedIndex = toIndex; + } + else if (_selectedIndex > fromIndex && _selectedIndex <= toIndex) + { + _selectedIndex--; + } + else if (_selectedIndex < fromIndex && _selectedIndex >= toIndex) + { + _selectedIndex++; + } + + if (_highlightedIndex == fromIndex) + { + _highlightedIndex = toIndex; + } + else if (_highlightedIndex > fromIndex && _highlightedIndex <= toIndex) + { + _highlightedIndex--; + } + else if (_highlightedIndex < fromIndex && _highlightedIndex >= toIndex) + { + _highlightedIndex++; + } + + _isUpdating = NO; + [self _layoutItemViews]; +} + +// MARK: - Item Views and Registration + +- (NSScrubberItemView *) itemViewForItemAtIndex: (NSInteger)index +{ + if (index >= 0 && index < [_itemViews count]) + { + NSScrubberItemView *itemView = [_itemViews objectAtIndex: index]; + if (itemView && itemView != (id)[NSNull null]) + { + return itemView; + } + } + return nil; +} + +- (void) registerClass: (Class)itemViewClass + forItemIdentifier: (NSString *)identifier +{ + if (itemViewClass && identifier) + { + [_registeredClasses setObject: itemViewClass forKey: identifier]; + } +} + +- (void) registerNib: (NSNib *)nib + forItemIdentifier: (NSString *)identifier +{ + if (nib && identifier) + { + [_registeredNibs setObject: nib forKey: identifier]; + } +} + +- (NSScrubberItemView *) makeItemWithIdentifier: (NSString *)identifier + owner: (id)owner +{ + if (!identifier) + return nil; + + // Try to dequeue a reusable item view + NSScrubberItemView *itemView = [self _dequeueReusableItemViewWithIdentifier: identifier]; + + if (!itemView) + { + // Try to create from registered nib + NSNib *nib = [_registeredNibs objectForKey: identifier]; + if (nib) + { + NSArray *topLevelObjects = nil; + if ([nib instantiateWithOwner: owner topLevelObjects: &topLevelObjects]) + { + NSEnumerator *objEnumerator = [topLevelObjects objectEnumerator]; + id object; + while ((object = [objEnumerator nextObject])) + { + if ([object isKindOfClass: [NSScrubberItemView class]]) + { + itemView = object; + break; + } + } + } + } + else + { + // Try to create from registered class + Class itemViewClass = [_registeredClasses objectForKey: identifier]; + if (itemViewClass && [itemViewClass isSubclassOfClass: [NSScrubberItemView class]]) + { + itemView = [[[itemViewClass alloc] init] autorelease]; + } + } + + if (itemView) + { + [itemView setReuseIdentifier: identifier]; + } + } + + return itemView; +} + +// MARK: - Scrolling + +- (void) scrollItemAtIndex: (NSInteger)index + toAlignment: (NSScrubberAlignment)alignment +{ + if (index < 0 || index >= [self numberOfItems]) + return; + + NSScrubberItemView *itemView = [self itemViewForItemAtIndex: index]; + if (!itemView) + return; + + NSRect itemFrame = [itemView frame]; + NSRect visibleRect = [self visibleRect]; + + // Calculate the target scroll position based on alignment + CGFloat targetX = 0; + + switch (alignment) + { + case NSScrubberAlignmentLeading: + targetX = NSMinX(itemFrame); + break; + + case NSScrubberAlignmentCenter: + targetX = NSMidX(itemFrame) - NSWidth(visibleRect) / 2.0; + break; + + case NSScrubberAlignmentTrailing: + targetX = NSMaxX(itemFrame) - NSWidth(visibleRect); + break; + + default: + return; + } + + NSPoint scrollPoint = NSMakePoint(targetX, NSMinY(visibleRect)); + [self scrollPoint: scrollPoint]; +} + +// MARK: - Batch Updates + +// MARK: - NSView Overrides + +- (void) drawRect: (NSRect)dirtyRect +{ + [super drawRect: dirtyRect]; + + // Draw background color + if (_backgroundColor) + { + [_backgroundColor setFill]; + NSRectFill(dirtyRect); + } + + // The item views will draw themselves + + // Draw selection and highlighting if needed + if (_selectedIndex != NSNotFound || _highlightedIndex != NSNotFound) + { + [self setNeedsDisplay: YES]; + } +} + +- (void) layout +{ + [super layout]; + [self _updateItemViewsIfNeeded]; + [self _layoutItemViews]; +} + +- (void) setFrame: (NSRect)frame +{ + [super setFrame: frame]; + [self _setNeedsLayout]; +} + +- (void) setBounds: (NSRect)bounds +{ + [super setBounds: bounds]; + [self _setNeedsLayout]; +} + +- (BOOL) acceptsFirstResponder +{ + return YES; +} + +- (BOOL) isOpaque +{ + return _backgroundColor && [_backgroundColor alphaComponent] >= 1.0; +} + +// MARK: - Mouse Event Handling + +- (void) mouseDown: (NSEvent *)event +{ + NSPoint location = [self convertPoint: [event locationInWindow] fromView: nil]; + + // Find which item was clicked + for (NSInteger i = 0; i < [_itemViews count]; i++) + { + NSScrubberItemView *itemView = [_itemViews objectAtIndex: i]; + if (itemView && itemView != (id)[NSNull null] && NSPointInRect(location, [itemView frame])) + { + [self _sendDelegateDidBeginInteracting]; + + // Update selection + [self setSelectedIndex: i]; + + if (!_continuous) + { + [self _sendDelegateDidSelectItemAtIndex: i]; + } + + [self _sendDelegateDidFinishInteracting]; + return; + } + } + + [super mouseDown: event]; +} + +- (void) mouseMoved: (NSEvent *)event +{ + NSPoint location = [self convertPoint: [event locationInWindow] fromView: nil]; + + // Find which item is under the cursor + NSInteger newHighlightedIndex = NSNotFound; + for (NSInteger i = 0; i < [_itemViews count]; i++) + { + NSScrubberItemView *itemView = [_itemViews objectAtIndex: i]; + if (itemView && itemView != (id)[NSNull null] && NSPointInRect(location, [itemView frame])) + { + newHighlightedIndex = i; + break; + } + } + + [self setHighlightedIndex: newHighlightedIndex]; + [super mouseMoved: event]; +} + @end diff --git a/Source/NSScrubberItemView.m b/Source/NSScrubberItemView.m index e7ab88793..197122ea8 100644 --- a/Source/NSScrubberItemView.m +++ b/Source/NSScrubberItemView.m @@ -23,12 +23,118 @@ */ #import "AppKit/NSScrubberItemView.h" +#import "Foundation/NSString.h" @implementation NSScrubberArrangedView +// MARK: - Initialization + +- (id) initWithFrame: (NSRect)frameRect +{ + self = [super initWithFrame: frameRect]; + if (self) + { + // Set up default properties for arranged view + } + return self; +} + +// MARK: - NSView Overrides + +- (BOOL) isOpaque +{ + return NO; +} + @end @implementation NSScrubberItemView +// MARK: - Class Methods + ++ (void) initialize +{ + if (self == [NSScrubberItemView class]) + { + [self setVersion: 1]; + } +} + +// MARK: - Initialization + +- (id) initWithFrame: (NSRect)frameRect +{ + self = [super initWithFrame: frameRect]; + if (self) + { + _reuseIdentifier = nil; + } + return self; +} + +- (id) initWithCoder: (NSCoder *)coder +{ + self = [super initWithCoder: coder]; + if (self) + { + if ([coder containsValueForKey: @"NSScrubberItemView.reuseIdentifier"]) + { + _reuseIdentifier = [[coder decodeObjectForKey: @"NSScrubberItemView.reuseIdentifier"] copy]; + } + } + return self; +} + +- (void) encodeWithCoder: (NSCoder *)coder +{ + [super encodeWithCoder: coder]; + + if (_reuseIdentifier) + { + [coder encodeObject: _reuseIdentifier forKey: @"NSScrubberItemView.reuseIdentifier"]; + } +} + +- (void) dealloc +{ + [_reuseIdentifier release]; + [super dealloc]; +} + +// MARK: - Property Accessors + +- (void) setReuseIdentifier: (NSString *)reuseIdentifier +{ + if (_reuseIdentifier != reuseIdentifier) + { + [_reuseIdentifier release]; + _reuseIdentifier = [reuseIdentifier copy]; + } +} + +// MARK: - Reuse Management + +- (void) prepareForReuse +{ + /** + * Default implementation does nothing. + * Subclasses should override this method to reset their content + * to a default state suitable for reuse. + * + * Common tasks in this method include: + * - Clearing text fields, image views, and other content + * - Resetting view state (selection, highlighting, etc.) + * - Canceling any ongoing operations or timers + * - Removing any temporary constraints or modifications + */ +} + +// MARK: - NSView Overrides + +- (BOOL) isOpaque +{ + return NO; +} + @end diff --git a/Source/NSScrubberLayout.m b/Source/NSScrubberLayout.m index 83d4bff83..91dccc372 100644 --- a/Source/NSScrubberLayout.m +++ b/Source/NSScrubberLayout.m @@ -23,112 +23,259 @@ */ #import "AppKit/NSScrubberLayout.h" +#import "Foundation/NSArray.h" + + @implementation NSScrubberLayoutAttributes +// MARK: - Class Methods + ++ (void) initialize +{ + if (self == [NSScrubberLayoutAttributes class]) + { + [self setVersion: 1]; + } +} + + (NSScrubberLayoutAttributes *) layoutAttributesForItemAtIndex: (NSInteger)index { - return nil; + NSScrubberLayoutAttributes *attributes = [[self alloc] init]; + [attributes setItemIndex: index]; + [attributes setAlpha: 1.0]; + [attributes setFrame: NSZeroRect]; + return [attributes autorelease]; } -- (CGFloat) alpha +// MARK: - Initialization + +- (id) init { - return 0.0; + self = [super init]; + if (self) + { + _alpha = 1.0; + _frame = NSZeroRect; + _itemIndex = NSNotFound; + } + return self; } -- (NSRect) frame +// MARK: - NSCopying Protocol + +- (id) copyWithZone: (NSZone *)zone { - return NSZeroRect; + NSScrubberLayoutAttributes *copy = [[[self class] allocWithZone: zone] init]; + [copy setAlpha: _alpha]; + [copy setFrame: _frame]; + [copy setItemIndex: _itemIndex]; + return copy; } -- (NSInteger) itemIndex +// MARK: - Equality and Hashing + +- (BOOL) isEqual: (id)other { - return 0; + if (![other isKindOfClass: [NSScrubberLayoutAttributes class]]) + return NO; + + NSScrubberLayoutAttributes *otherAttrs = (NSScrubberLayoutAttributes *)other; + return _itemIndex == otherAttrs.itemIndex && + _alpha == otherAttrs.alpha && + NSEqualRects(_frame, otherAttrs.frame); } -- (instancetype) copyWithZone: (NSZone *)z +- (NSUInteger) hash { - return nil; + return _itemIndex ^ (NSUInteger)_alpha ^ NSStringFromRect(_frame).hash; +} + +// MARK: - NSCoding Protocol + +- (id) initWithCoder: (NSCoder *)coder +{ + self = [super init]; + if (self) + { + _itemIndex = [coder decodeIntegerForKey: @"NSScrubberLayoutAttributes.itemIndex"]; + _frame = [coder decodeRectForKey: @"NSScrubberLayoutAttributes.frame"]; + _alpha = [coder decodeDoubleForKey: @"NSScrubberLayoutAttributes.alpha"]; + } + return self; +} + +- (void) encodeWithCoder: (NSCoder *)coder +{ + [coder encodeInteger: _itemIndex forKey: @"NSScrubberLayoutAttributes.itemIndex"]; + [coder encodeRect: _frame forKey: @"NSScrubberLayoutAttributes.frame"]; + [coder encodeDouble: _alpha forKey: @"NSScrubberLayoutAttributes.alpha"]; +} + +// MARK: - Description + +- (NSString *) description +{ + return [NSString stringWithFormat: @"<%@: %p; itemIndex: %ld; frame: %@; alpha: %g>", + [self className], self, (long)_itemIndex, NSStringFromRect(_frame), _alpha]; } @end @implementation NSScrubberLayout -// Configuring + +// MARK: - Class Methods + ++ (void) initialize +{ + if (self == [NSScrubberLayout class]) + { + [self setVersion: 1]; + } +} + +// MARK: - Initialization + +- (id) init +{ + self = [super init]; + if (self) + { + _scrubber = nil; + } + return self; +} + +- (id) initWithCoder: (NSCoder *)coder +{ + self = [super init]; + if (self) + { + _scrubber = nil; + // Decode any layout-specific properties here if needed + } + return self; +} + +- (void) encodeWithCoder: (NSCoder *)coder +{ + // Encode any layout-specific properties here if needed +} + +// MARK: - Layout Configuration + - (Class) layoutAttributesClass { - return nil; + return [NSScrubberLayoutAttributes class]; } - (NSScrubber *) scrubber { - return nil; + return _scrubber; } - (NSRect) visibleRect { - return NSZeroRect; + if (_scrubber) + { + return [_scrubber visibleRect]; + } + return NSZeroRect; } - (void) invalidateLayout { + // Default implementation does nothing + // Subclasses should override to perform invalidation-specific logic } -// Subclassing layout +// MARK: - Subclassing Methods + - (void) prepareLayout { + /** + * Default implementation does nothing. + * Subclasses should override this method to perform any setup + * required before layout attributes are calculated. + */ } - (NSSize) scrubberContentSize { - return NSZeroSize; + /** + * Default implementation returns zero size. + * Subclasses must override this method to return the total + * content size needed to display all items. + */ + return NSZeroSize; } - (NSScrubberLayoutAttributes *) layoutAttributesForItemAtIndex: (NSInteger)index { - return nil; + /** + * Default implementation returns nil. + * Subclasses must override this method to provide layout + * attributes for the item at the specified index. + */ + return nil; } -- (NSScrubberLayoutAttributes *) layoutAttributesForItemsInRect: (NSRect)rect +- (NSArray *) layoutAttributesForItemsInRect: (NSRect)rect { - return nil; + /** + * Default implementation returns an empty array. + * Subclasses should override this method to return layout + * attributes for all items that intersect with the given rectangle. + */ + return [NSArray array]; } - (BOOL) shouldInvalidateLayoutForHighlightChange { - return NO; + /** + * Default implementation returns NO. + * Subclasses can override this to return YES if highlighting + * changes require layout recalculation. + */ + return NO; } - (BOOL) shouldInvalidateLayoutForSelectionChange { - return NO; + /** + * Default implementation returns NO. + * Subclasses can override this to return YES if selection + * changes require layout recalculation. + */ + return NO; } - (BOOL) shouldInvalidateLayoutForChangeFromVisibleRect: (NSRect)fromRect toVisibleRect: (NSRect)toRect { - return NO; + /** + * Default implementation returns NO. + * Subclasses can override this to return YES if visible rect + * changes require layout recalculation. + */ + return NO; } - (BOOL) automaticallyMirrorsInRightToLeftLayout { - return NO; + /** + * Default implementation returns NO. + * Subclasses can override this to return YES if the layout + * should automatically mirror in right-to-left environments. + */ + return NO; } -- (instancetype) init -{ - self = [super init]; - return self; -} +// MARK: - Internal Methods (called by NSScrubber) -- (instancetype) initWithCoder: (NSCoder *)coder -{ - self = [super init]; - return self; -} - -- (void) encodeWithCoder: (NSCoder *)coder +- (void) _setScrubber: (NSScrubber *)scrubber { + _scrubber = scrubber; } @end