aaf2a8d4d462c0b7662be940c97f3e655a989e2c
[vlc.git] / extras / MacOSX / VLC_app / Sources / VLCBrowsableVideoView.m
1 /*****************************************************************************
2  * VLCBrowsableVideoView.h: VideoView subclasses that allow fullscreen
3  * browsing
4  *****************************************************************************
5  * Copyright (C) 2007 Pierre d'Herbemont
6  * Copyright (C) 2007 the VideoLAN team
7  * $Id$
8  *
9  * Authors: Pierre d'Herbemont <pdherbemont # videolan.org>
10  *
11  * This program is free software; you can redistribute it and/or modify
12  * it under the terms of the GNU General Public License as published by
13  * the Free Software Foundation; either version 2 of the License, or
14  * (at your option) any later version.
15  *
16  * This program is distributed in the hope that it will be useful,
17  * but WITHOUT ANY WARRANTY; without even the implied warranty of
18  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19  * GNU General Public License for more details.
20  *
21  * You should have received a copy of the GNU General Public License
22  * along with this program; if not, write to the Free Software
23  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
24  *****************************************************************************/
25
26 #import "VLCBrowsableVideoView.h"
27 #import "VLCAppAdditions.h"
28
29 /* TODO: We may want to clean up the private functions a bit... */
30
31 @interface VLCBrowsableVideoView ()
32 /* Property */
33 @property (readwrite, retain) id selectedObject;
34 @end
35
36 @interface VLCBrowsableVideoView (Private)
37
38 /* Methods */
39 + (CAScrollLayer *)menuLayer;
40 + (CALayer *)backLayer;
41
42 - (void)loadItemsAtIndexPath:(NSIndexPath *)path inLayer:(CALayer *)layer;
43 - (void)changeSelectedIndex:(NSInteger)i;
44 - (void)changeSelectedPath:(NSIndexPath *)newPath withSelectedIndex:(NSUInteger)newIndex;
45
46 - (void)displayEmptyView;
47 @end
48
49
50 /******************************************************************************
51  * VLCBrowsableVideoView
52  */
53 @implementation VLCBrowsableVideoView
54
55 /* Property */
56 @synthesize nodeKeyPath;
57 @synthesize contentKeyPath;
58 @synthesize selectedObject;
59 @synthesize target;
60 @synthesize action;
61
62 - (NSArray *)itemsTree {
63     return itemsTree;
64 }
65 - (void)setItemsTree:(NSArray *)newItemsTree
66 {
67     [itemsTree release];
68     itemsTree = [newItemsTree retain];
69     [self changeSelectedPath:[[[NSIndexPath alloc] init] autorelease] withSelectedIndex:0];
70 }
71
72 /* Initializer */
73 - (void)awakeFromNib
74 {
75     // FIXME: do that in -initWithFrame:
76     [self setWantsLayer:YES];
77     menuDisplayed = NO;
78     displayedItems = NSMakeRange( -1, 0 );
79     selectedIndex = -1;
80     selectionLayer = backLayer = nil;
81     menuLayer = nil;
82     selectedPath = [[NSIndexPath alloc] init];
83     /* Observe our bindings */
84     //[self displayMenu];
85     //[self changeSelectedIndex:0];
86 }
87
88
89 /* Hiding/Displaying the menu */
90
91 - (void)hideMenu
92 {
93     if( !menuDisplayed )
94         return; /* Nothing to do */
95
96     [menuLayer removeFromSuperlayer];
97     [selectionLayer removeFromSuperlayer];
98     [backLayer removeFromSuperlayer];
99     //[menuLayer autorelease]; /* Need gc for that */
100     //[selectionLayer autorelease];
101     //[backLayer autorelease];
102     selectionLayer = backLayer = nil;
103     menuLayer = nil;
104     menuDisplayed = NO;
105 }
106
107 - (void)displayMenu
108 {
109     if( menuDisplayed || !self.itemsTree )
110         return; /* Nothing to do */
111
112     if( !menuLayer )
113     {
114         CALayer * rootLayer = [self layer];
115         rootLayer.backgroundColor = CGColorGetConstantColor(kCGColorBlack);
116         rootLayer.layoutManager = [CAConstraintLayoutManager layoutManager];
117         menuLayer = [VLCBrowsableVideoView menuLayer];
118         [self loadItemsAtIndexPath: selectedPath inLayer: menuLayer];
119     }
120     if( !backLayer )
121     {
122         backLayer = [[VLCBrowsableVideoView backLayer] retain];
123     }
124     [[self layer] addSublayer:backLayer];
125     [[self layer] addSublayer:menuLayer];
126
127     [[self layer] setNeedsLayout];
128     [[self layer] setNeedsDisplay];
129
130     menuDisplayed = YES;
131     [self changeSelectedPath:selectedPath withSelectedIndex:selectedIndex];
132 }
133
134 - (void)toggleMenu
135 {
136     if( menuDisplayed )
137         [self hideMenu];
138     else
139         [self displayMenu];
140 }
141
142
143 /* Event handling */
144
145 - (BOOL)acceptsFirstResponder
146 {
147     return YES;
148 }
149
150 -(void)moveUp:(id)sender
151 {
152     [self changeSelectedIndex:selectedIndex-1];
153 }
154
155 -(void)moveDown:(id)sender
156 {
157     [self changeSelectedIndex:selectedIndex+1];
158 }
159
160 - (void)keyDown:(NSEvent *)theEvent
161 {
162     if(([[theEvent charactersIgnoringModifiers] characterAtIndex:0] == 13) && menuDisplayed)
163     {
164         [self changeSelectedPath:[selectedPath indexPathByAddingIndex:selectedIndex] withSelectedIndex:0];
165     }
166     else if([[theEvent charactersIgnoringModifiers] characterAtIndex:0] ==  NSLeftArrowFunctionKey && menuDisplayed)
167     {
168         if( [selectedPath length] > 0 )
169             [self changeSelectedPath:[selectedPath indexPathByRemovingLastIndex] withSelectedIndex:[selectedPath lastIndex]];
170         else
171             [self hideMenu];
172     }
173     else if(!menuDisplayed)
174     {
175         [self displayMenu];
176     }
177     else
178         [super keyDown: theEvent];
179
180 }
181 @end
182
183 /******************************************************************************
184  * VLCBrowsableVideoView (Private)
185  */
186 @implementation VLCBrowsableVideoView (Private)
187 + (CAScrollLayer *)menuLayer
188 {
189     CAScrollLayer * layer = [CAScrollLayer layer];
190     layer.scrollMode = kCAScrollVertically;
191             
192     [layer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMaxY
193                                        relativeTo:@"superlayer" attribute:kCAConstraintMaxY]];
194     [layer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMaxX
195                                        relativeTo:@"superlayer" attribute:kCAConstraintMaxX]];
196     [layer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMinX
197                                        relativeTo:@"superlayer" attribute:kCAConstraintMinX]];
198     [layer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMinY
199                                        relativeTo:@"superlayer" attribute:kCAConstraintMinY]];
200     return layer;
201 }
202
203 + (CALayer *)backLayer
204 {
205     CALayer * layer = [CALayer layer];
206             
207     [layer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMaxY
208                                        relativeTo:@"superlayer" attribute:kCAConstraintMaxY]];
209     [layer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMaxX
210                                        relativeTo:@"superlayer" attribute:kCAConstraintMaxX]];
211     [layer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMinX
212                                        relativeTo:@"superlayer" attribute:kCAConstraintMinX]];
213     [layer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMinY
214                                        relativeTo:@"superlayer" attribute:kCAConstraintMinY]];
215
216     layer.opacity = 1.0;
217     layer.backgroundColor = CGColorCreateGenericRGB(0., 0., 0., .5);
218     return layer;
219 }
220
221
222 - (void)loadItemsAtIndexPath:(NSIndexPath *)path inLayer:(CALayer *)layer
223 {
224     const CGFloat height=70.0;
225     const CGFloat fontSize=48.0;
226     NSArray * items = [self.itemsTree objectAtIndexPath:path withNodeKeyPath:self.nodeKeyPath]; 
227     int i;
228
229     for( i = 0; i < [items count]; i++ )
230     {
231         CATextLayer *menuItemLayer=[CATextLayer layer];
232         id item = [items objectAtIndex: i];
233         menuItemLayer.string = self.contentKeyPath ? [item valueForKeyPath:self.contentKeyPath] : @"No content Key path set";
234         menuItemLayer.font = @"BankGothic-Light";
235         menuItemLayer.fontSize = fontSize;
236         menuItemLayer.foregroundColor = CGColorCreateGenericRGB(1.0,1.0,1.0,1.0);
237         menuItemLayer.shadowColor = CGColorCreateGenericRGB(0.0,0.0,0.0,1.0);
238         menuItemLayer.shadowOpacity = 0.7;
239         menuItemLayer.shadowRadius = 2.0;
240
241         menuItemLayer.frame = CGRectMake( 40., height*(-i) + layer.visibleRect.size.height, 500.0f,70.);
242         [layer addSublayer: menuItemLayer];
243     }
244
245 /*    for(i=0; i < [[layer sublayers] count]; i++)
246         NSLog(@"%d, %@", i, [[[layer sublayers] objectAtIndex: i] string]);
247     NSLog(@"---");*/
248 }
249
250 - (void)changeSelectedIndex:(NSInteger)i
251 {
252     BOOL justCreatedSelectionLayer = NO;
253     if( !menuDisplayed )
254     {
255         selectedIndex = i;
256         return;
257     }
258
259     if( !selectionLayer )
260     {
261         justCreatedSelectionLayer = YES;
262         /* Rip-off from Apple's Sample code */
263         selectionLayer=[[CALayer layer] retain];
264         
265         selectionLayer.borderWidth=2.0;
266         selectionLayer.borderColor=CGColorCreateGenericRGB(1.0f,1.0f,1.0f,1.0f);
267         selectionLayer.backgroundColor=CGColorCreateGenericRGB(.9f,1.0f,1.0f,.1f);
268         
269         CIFilter *filter = [CIFilter filterWithName:@"CIBloom"];
270         [filter setDefaults];
271         [filter setValue:[NSNumber numberWithFloat:5.0] forKey:@"inputRadius"];
272         
273         [filter setName:@"pulseFilter"];
274         
275         [selectionLayer setFilters:[NSArray arrayWithObject:filter]];
276         
277         CABasicAnimation* pulseAnimation = [CABasicAnimation animation];
278         
279         pulseAnimation.keyPath = @"filters.pulseFilter.inputIntensity";
280         
281         pulseAnimation.fromValue = [NSNumber numberWithFloat: 0.0];
282         pulseAnimation.toValue = [NSNumber numberWithFloat: 3.0];
283         
284         pulseAnimation.duration = 2.0;
285         pulseAnimation.repeatCount = 1e100f;
286         pulseAnimation.autoreverses = YES;
287         
288         pulseAnimation.timingFunction = [CAMediaTimingFunction functionWithName:
289                                          kCAMediaTimingFunctionEaseInEaseOut];
290         
291         [selectionLayer addAnimation:pulseAnimation forKey:@"pulseAnimation"];
292         [[self layer] addSublayer:selectionLayer];
293     }
294     NSArray * items = [self.itemsTree objectAtIndexPath:selectedPath withNodeKeyPath:self.nodeKeyPath];
295     if( i < 0 ) i = 0;
296     if( i >= [items count] ) i = [items count] - 1;
297
298     CALayer * layer = [[menuLayer sublayers] objectAtIndex: i];
299     CGRect frame = layer.frame;
300     if( i == 0 )
301     {
302         frame.origin.y -= [self layer].bounds.size.height - frame.size.height;
303         frame.size.height = [self layer].bounds.size.height;
304     }
305     [(CAScrollLayer*)menuLayer scrollToRect:frame];
306
307     if( !justCreatedSelectionLayer ) /* Get around an artifact on first launch */
308         [CATransaction flush]; /* Make sure we get the "right" layer.frame */
309
310     frame = [[self layer] convertRect:layer.frame fromLayer:[layer superlayer]];
311     frame.size.width += 200.;
312     frame.origin.x -= 100.f;
313     selectionLayer.frame = frame;
314     
315     selectionLayer.cornerRadius = selectionLayer.bounds.size.height / 2.;
316     selectedIndex = i;
317 }
318
319 - (void)changeSelectedPath:(NSIndexPath *)newPath withSelectedIndex:(NSUInteger)newIndex
320 {
321     if( menuDisplayed )
322     {
323         id object = [itemsTree objectAtIndexPath:newPath withNodeKeyPath:nodeKeyPath];
324         /* Make sure we are in a node */
325         if( ![object isKindOfClass:[NSArray class]] )
326         {
327             self.selectedObject = object;
328             if( !self.target || !self.action )
329             {
330                 [NSException raise:@"VLCBrowsableVideoViewNoActionSpecified" format:@"*** Exception [%@]: No action specified.", [self class]];
331                 return;
332             }
333             void (*method)(id, SEL, id) = (void (*)(id, SEL, id))[self.target methodForSelector: self.action];
334
335             method( self.target, self.action, self);
336
337             [self hideMenu];
338             return;
339         }
340         
341         /* Make sure the node isn't empty */
342         if( ![object count] )
343         {
344             [self displayEmptyView];
345         }
346         else
347         {
348             CALayer * newMenuLayer = [VLCBrowsableVideoView menuLayer];
349             if( menuLayer )
350                 newMenuLayer.bounds = menuLayer.bounds; /* Get around some artifacts */
351             [self loadItemsAtIndexPath:newPath inLayer:newMenuLayer];
352             if( menuLayer )
353                 [[self layer] replaceSublayer:menuLayer with:newMenuLayer];
354             else
355                 [[self layer] addSublayer:newMenuLayer];
356             //[menuLayer autorelease]; /* warn: we need gc for that */
357             menuLayer = [newMenuLayer retain];
358         }
359     }
360     [selectedPath release];
361     selectedPath = [newPath retain];
362     [self changeSelectedIndex:newIndex];
363 }
364
365 - (void)displayEmptyView
366 {
367     CALayer * layer = [CALayer layer];
368     layer.layoutManager = [CAConstraintLayoutManager layoutManager];
369     [layer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMaxY
370                                        relativeTo:@"superlayer" attribute:kCAConstraintMaxY]];
371     [layer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMaxX
372                                        relativeTo:@"superlayer" attribute:kCAConstraintMaxX]];
373     [layer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMinX
374                                        relativeTo:@"superlayer" attribute:kCAConstraintMinX]];
375     [layer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMinY
376                                        relativeTo:@"superlayer" attribute:kCAConstraintMinY]];
377         
378     CATextLayer *menuItemLayer=[CATextLayer layer];
379     menuItemLayer.string = @"Empty";
380     menuItemLayer.font = @"BankGothic-Light";
381     menuItemLayer.fontSize = 48.f;
382     menuItemLayer.foregroundColor = CGColorCreateGenericRGB(1.0,1.0,1.0,1.0);
383     menuItemLayer.shadowColor = CGColorCreateGenericRGB(0.0,0.0,0.0,1.0);
384     menuItemLayer.shadowOpacity = 0.7;
385     menuItemLayer.shadowRadius = 2.0;
386
387     [menuItemLayer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMidX
388                                        relativeTo:@"superlayer" attribute:kCAConstraintMidX]];
389     [menuItemLayer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMidY
390                                        relativeTo:@"superlayer" attribute:kCAConstraintMidY]];
391     [layer addSublayer:menuItemLayer];
392
393     if( menuLayer )
394         [[self layer] replaceSublayer:menuLayer with:layer];
395     else
396         [[self layer] addSublayer:layer];
397     [selectionLayer removeFromSuperlayer];
398     //[selectionLayer autorelease] /* need gc */
399     //[menuLayer autorelease] /* need gc */
400     menuLayer = layer;
401     selectionLayer = nil;
402 }
403 @end