e9e28a3b4017e297785073cb6fb4c409d8e25aff
[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
66 - (void)setItemsTree:(NSArray *)newItemsTree
67 {
68     [itemsTree release];
69     itemsTree = [newItemsTree retain];
70     [self changeSelectedPath:[[[NSIndexPath alloc] init] autorelease] withSelectedIndex:0];
71 }
72
73 - (BOOL)fullScreen
74 {
75     return [super isInFullScreenMode];
76 }
77
78 - (void)setFullScreen:(BOOL)newFullScreen
79 {
80     if( newFullScreen == self.fullScreen )
81         return;
82     
83     if( newFullScreen )
84     {
85         [super enterFullScreenMode:[[self window] screen] withOptions:nil];
86     }
87     else
88     {
89         [super exitFullScreenModeWithOptions:nil];
90     }
91 }
92
93 /* Initializer */
94 - (void)awakeFromNib
95 {
96     // FIXME: do that in -initWithFrame:
97     [self setWantsLayer:YES];
98     menuDisplayed = NO;
99     displayedItems = NSMakeRange( -1, 0 );
100     selectedIndex = -1;
101     selectionLayer = backLayer = nil;
102     menuLayer = nil;
103     selectedPath = [[NSIndexPath alloc] init];
104     /* Observe our bindings */
105     //[self displayMenu];
106     //[self changeSelectedIndex:0];
107 }
108
109 /* Hiding/Displaying the menu */
110
111 - (void)hideMenu
112 {
113     if( !menuDisplayed )
114         return; /* Nothing to do */
115
116     [menuLayer removeFromSuperlayer];
117     [selectionLayer removeFromSuperlayer];
118     [backLayer removeFromSuperlayer];
119     //[menuLayer autorelease]; /* Need gc for that */
120     //[selectionLayer autorelease];
121     //[backLayer autorelease];
122     selectionLayer = backLayer = nil;
123     menuLayer = nil;
124     menuDisplayed = NO;
125 }
126
127 - (void)displayMenu
128 {
129     if( menuDisplayed || !self.itemsTree )
130         return; /* Nothing to do */
131
132     if( !menuLayer )
133     {
134         CALayer * rootLayer = [self layer];
135         rootLayer.backgroundColor = CGColorGetConstantColor(kCGColorBlack);
136         rootLayer.layoutManager = [CAConstraintLayoutManager layoutManager];
137         menuLayer = [VLCBrowsableVideoView menuLayer];
138         [self loadItemsAtIndexPath: selectedPath inLayer: menuLayer];
139     }
140     if( !backLayer )
141     {
142         backLayer = [[VLCBrowsableVideoView backLayer] retain];
143     }
144     [[self layer] addSublayer:backLayer];
145     [[self layer] addSublayer:menuLayer];
146
147     [[self layer] setNeedsLayout];
148     [[self layer] setNeedsDisplay];
149
150     menuDisplayed = YES;
151     [self changeSelectedPath:selectedPath withSelectedIndex:selectedIndex];
152 }
153
154 - (void)toggleMenu
155 {
156     if( menuDisplayed )
157         [self hideMenu];
158     else
159         [self displayMenu];
160 }
161
162
163 /* Event handling */
164
165 - (BOOL)acceptsFirstResponder
166 {
167     return YES;
168 }
169
170 -(void)moveUp:(id)sender
171 {
172     [self changeSelectedIndex:selectedIndex-1];
173 }
174
175 -(void)moveDown:(id)sender
176 {
177     [self changeSelectedIndex:selectedIndex+1];
178 }
179
180 - (void)mouseDown:(NSEvent *)theEvent
181 {
182     if([theEvent clickCount] != 2)
183         return;
184
185     self.fullScreen = !self.fullScreen;
186 }
187
188 - (void)keyDown:(NSEvent *)theEvent
189 {
190     if(([[theEvent charactersIgnoringModifiers] characterAtIndex:0] == 13) && menuDisplayed)
191     {
192         [self changeSelectedPath:[selectedPath indexPathByAddingIndex:selectedIndex] withSelectedIndex:0];
193     }
194     else if([[theEvent charactersIgnoringModifiers] characterAtIndex:0] ==  NSLeftArrowFunctionKey && menuDisplayed)
195     {
196         if( [selectedPath length] > 0 )
197             [self changeSelectedPath:[selectedPath indexPathByRemovingLastIndex] withSelectedIndex:[selectedPath lastIndex]];
198         else
199             [self hideMenu];
200     }
201     else if(!menuDisplayed)
202     {
203         [self displayMenu];
204     }
205     else
206         [super keyDown: theEvent];
207
208 }
209 @end
210
211 /******************************************************************************
212  * VLCBrowsableVideoView (Private)
213  */
214 @implementation VLCBrowsableVideoView (Private)
215 + (CAScrollLayer *)menuLayer
216 {
217     CAScrollLayer * layer = [CAScrollLayer layer];
218     layer.scrollMode = kCAScrollVertically;
219             
220     [layer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMaxY
221                                        relativeTo:@"superlayer" attribute:kCAConstraintMaxY]];
222     [layer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMaxX
223                                        relativeTo:@"superlayer" attribute:kCAConstraintMaxX]];
224     [layer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMinX
225                                        relativeTo:@"superlayer" attribute:kCAConstraintMinX]];
226     [layer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMinY
227                                        relativeTo:@"superlayer" attribute:kCAConstraintMinY]];
228     return layer;
229 }
230
231 + (CALayer *)backLayer
232 {
233     CALayer * layer = [CALayer layer];
234             
235     [layer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMaxY
236                                        relativeTo:@"superlayer" attribute:kCAConstraintMaxY]];
237     [layer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMaxX
238                                        relativeTo:@"superlayer" attribute:kCAConstraintMaxX]];
239     [layer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMinX
240                                        relativeTo:@"superlayer" attribute:kCAConstraintMinX]];
241     [layer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMinY
242                                        relativeTo:@"superlayer" attribute:kCAConstraintMinY]];
243
244     layer.opacity = 1.0;
245     layer.backgroundColor = CGColorCreateGenericRGB(0., 0., 0., .5);
246     return layer;
247 }
248
249
250 - (void)loadItemsAtIndexPath:(NSIndexPath *)path inLayer:(CALayer *)layer
251 {
252     const CGFloat height=70.0;
253     const CGFloat fontSize=48.0;
254     NSArray * items = [self.itemsTree objectAtIndexPath:path withNodeKeyPath:self.nodeKeyPath]; 
255     int i;
256
257     for( i = 0; i < [items count]; i++ )
258     {
259         CATextLayer *menuItemLayer=[CATextLayer layer];
260         id item = [items objectAtIndex: i];
261         menuItemLayer.string = self.contentKeyPath ? [item valueForKeyPath:self.contentKeyPath] : @"No content Key path set";
262         menuItemLayer.font = @"BankGothic-Light";
263         menuItemLayer.fontSize = fontSize;
264         menuItemLayer.foregroundColor = CGColorCreateGenericRGB(1.0,1.0,1.0,1.0);
265         menuItemLayer.shadowColor = CGColorCreateGenericRGB(0.0,0.0,0.0,1.0);
266         menuItemLayer.shadowOpacity = 0.7;
267         menuItemLayer.shadowRadius = 2.0;
268
269         menuItemLayer.frame = CGRectMake( 40., height*(-i) + layer.visibleRect.size.height, 500.0f,70.);
270         [layer addSublayer: menuItemLayer];
271     }
272
273 /*    for(i=0; i < [[layer sublayers] count]; i++)
274         NSLog(@"%d, %@", i, [[[layer sublayers] objectAtIndex: i] string]);
275     NSLog(@"---");*/
276 }
277
278 - (void)changeSelectedIndex:(NSInteger)i
279 {
280     BOOL justCreatedSelectionLayer = NO;
281     if( !menuDisplayed )
282     {
283         selectedIndex = i;
284         return;
285     }
286
287     if( !selectionLayer )
288     {
289         justCreatedSelectionLayer = YES;
290         /* Rip-off from Apple's Sample code */
291         selectionLayer=[[CALayer layer] retain];
292         
293         selectionLayer.borderWidth=2.0;
294         selectionLayer.borderColor=CGColorCreateGenericRGB(1.0f,1.0f,1.0f,1.0f);
295         selectionLayer.backgroundColor=CGColorCreateGenericRGB(.9f,1.0f,1.0f,.1f);
296         
297         CIFilter *filter = [CIFilter filterWithName:@"CIBloom"];
298         [filter setDefaults];
299         [filter setValue:[NSNumber numberWithFloat:5.0] forKey:@"inputRadius"];
300         
301         [filter setName:@"pulseFilter"];
302         
303         [selectionLayer setFilters:[NSArray arrayWithObject:filter]];
304         
305         CABasicAnimation* pulseAnimation = [CABasicAnimation animation];
306         
307         pulseAnimation.keyPath = @"filters.pulseFilter.inputIntensity";
308         
309         pulseAnimation.fromValue = [NSNumber numberWithFloat: 0.0];
310         pulseAnimation.toValue = [NSNumber numberWithFloat: 3.0];
311         
312         pulseAnimation.duration = 2.0;
313         pulseAnimation.repeatCount = 1e100f;
314         pulseAnimation.autoreverses = YES;
315         
316         pulseAnimation.timingFunction = [CAMediaTimingFunction functionWithName:
317                                          kCAMediaTimingFunctionEaseInEaseOut];
318         
319         [selectionLayer addAnimation:pulseAnimation forKey:@"pulseAnimation"];
320         [[self layer] addSublayer:selectionLayer];
321     }
322     NSArray * items = [self.itemsTree objectAtIndexPath:selectedPath withNodeKeyPath:self.nodeKeyPath];
323     if( i < 0 ) i = 0;
324     if( i >= [items count] ) i = [items count] - 1;
325
326     CALayer * layer = [[menuLayer sublayers] objectAtIndex: i];
327     CGRect frame = layer.frame;
328     if( i == 0 )
329     {
330         frame.origin.y -= [self layer].bounds.size.height - frame.size.height;
331         frame.size.height = [self layer].bounds.size.height;
332     }
333     [(CAScrollLayer*)menuLayer scrollToRect:frame];
334
335     if( !justCreatedSelectionLayer ) /* Get around an artifact on first launch */
336         [CATransaction flush]; /* Make sure we get the "right" layer.frame */
337
338     frame = [[self layer] convertRect:layer.frame fromLayer:[layer superlayer]];
339     frame.size.width += 200.;
340     frame.origin.x -= 100.f;
341     selectionLayer.frame = frame;
342     
343     selectionLayer.cornerRadius = selectionLayer.bounds.size.height / 2.;
344     selectedIndex = i;
345 }
346
347 - (void)changeSelectedPath:(NSIndexPath *)newPath withSelectedIndex:(NSUInteger)newIndex
348 {
349     if( menuDisplayed )
350     {
351         id object = [itemsTree objectAtIndexPath:newPath withNodeKeyPath:nodeKeyPath];
352         /* Make sure we are in a node */
353         if( ![object isKindOfClass:[NSArray class]] )
354         {
355             self.selectedObject = object;
356             if( !self.target || !self.action )
357             {
358                 [NSException raise:@"VLCBrowsableVideoViewNoActionSpecified" format:@"*** Exception [%@]: No action specified.", [self class]];
359                 return;
360             }
361             void (*method)(id, SEL, id) = (void (*)(id, SEL, id))[self.target methodForSelector: self.action];
362
363             method( self.target, self.action, self);
364
365             [self hideMenu];
366             return;
367         }
368         
369         /* Make sure the node isn't empty */
370         if( ![object count] )
371         {
372             [self displayEmptyView];
373         }
374         else
375         {
376             CALayer * newMenuLayer = [VLCBrowsableVideoView menuLayer];
377             if( menuLayer )
378                 newMenuLayer.bounds = menuLayer.bounds; /* Get around some artifacts */
379             [self loadItemsAtIndexPath:newPath inLayer:newMenuLayer];
380             if( menuLayer )
381                 [[self layer] replaceSublayer:menuLayer with:newMenuLayer];
382             else
383                 [[self layer] addSublayer:newMenuLayer];
384             //[menuLayer autorelease]; /* warn: we need gc for that */
385             menuLayer = [newMenuLayer retain];
386         }
387     }
388     [selectedPath release];
389     selectedPath = [newPath retain];
390     [self changeSelectedIndex:newIndex];
391 }
392
393 - (void)displayEmptyView
394 {
395     CALayer * layer = [CALayer layer];
396     layer.layoutManager = [CAConstraintLayoutManager layoutManager];
397     [layer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMaxY
398                                        relativeTo:@"superlayer" attribute:kCAConstraintMaxY]];
399     [layer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMaxX
400                                        relativeTo:@"superlayer" attribute:kCAConstraintMaxX]];
401     [layer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMinX
402                                        relativeTo:@"superlayer" attribute:kCAConstraintMinX]];
403     [layer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMinY
404                                        relativeTo:@"superlayer" attribute:kCAConstraintMinY]];
405         
406     CATextLayer *menuItemLayer=[CATextLayer layer];
407     menuItemLayer.string = @"Empty";
408     menuItemLayer.font = @"BankGothic-Light";
409     menuItemLayer.fontSize = 48.f;
410     menuItemLayer.foregroundColor = CGColorCreateGenericRGB(1.0,1.0,1.0,1.0);
411     menuItemLayer.shadowColor = CGColorCreateGenericRGB(0.0,0.0,0.0,1.0);
412     menuItemLayer.shadowOpacity = 0.7;
413     menuItemLayer.shadowRadius = 2.0;
414
415     [menuItemLayer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMidX
416                                        relativeTo:@"superlayer" attribute:kCAConstraintMidX]];
417     [menuItemLayer addConstraint:[CAConstraint constraintWithAttribute:kCAConstraintMidY
418                                        relativeTo:@"superlayer" attribute:kCAConstraintMidY]];
419     [layer addSublayer:menuItemLayer];
420
421     if( menuLayer )
422         [[self layer] replaceSublayer:menuLayer with:layer];
423     else
424         [[self layer] addSublayer:layer];
425     [selectionLayer removeFromSuperlayer];
426     //[selectionLayer autorelease] /* need gc */
427     //[menuLayer autorelease] /* need gc */
428     menuLayer = layer;
429     selectionLayer = nil;
430 }
431 @end