ba1abd1965492cbb203f293aee730fba70f3a1f7
[vlc.git] / modules / gui / macosx / ExtensionsDialogProvider.m
1 /*****************************************************************************
2  * ExtensionsDialogProvider.m: Mac OS X Extensions Dialogs
3  *****************************************************************************
4  * Copyright (C) 2010-2013 VLC authors and VideoLAN
5  * $Id$
6  *
7  * Authors: Pierre d'Herbemont <pdherbemont # videolan org>
8  *          Brendon Justin <brendonjustin@gmail.com>,
9  *          Derk-Jan Hartman <hartman@videolan dot org>,
10  *          Felix Paul K├╝hne <fkuehne@videolan dot org>
11  *
12  * This program is free software; you can redistribute it and/or modify
13  * it under the terms of the GNU General Public License as published by
14  * the Free Software Foundation; either version 2 of the License, or
15  * (at your option) any later version.
16  *
17  * This program is distributed in the hope that it will be useful,
18  * but WITHOUT ANY WARRANTY; without even the implied warranty of
19  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20  * GNU General Public License for more details.
21  *
22  * You should have received a copy of the GNU General Public License
23  * along with this program; if not, write to the Free Software
24  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
25  *****************************************************************************/
26
27 #import "ExtensionsDialogProvider.h"
28
29 #import "intf.h"
30 #import "ExtensionsManager.h"
31 #import "misc.h"
32 #import "VLCUIWidgets.h"
33
34 #import <WebKit/WebKit.h>
35 #import <stdlib.h>
36
37 /*****************************************************************************
38  * VLCExtensionsDialogProvider implementation
39  *****************************************************************************/
40
41 static int extensionDialogCallback(vlc_object_t *p_this, const char *psz_variable,
42                                    vlc_value_t old_val, vlc_value_t new_val,
43                                    void *param);
44
45 static NSView *createControlFromWidget(extension_widget_t *widget, id self)
46 {
47     @autoreleasepool {
48         assert(!widget->p_sys_intf);
49         switch (widget->type) {
50             case EXTENSION_WIDGET_HTML:
51             {
52                 WebView *webView = [[WebView alloc] initWithFrame:NSMakeRect (0,0,1,1)];
53                 [webView setAutoresizingMask:NSViewHeightSizable | NSViewWidthSizable];
54                 [webView setDrawsBackground:NO];
55                 return webView;
56             }
57             case EXTENSION_WIDGET_LABEL:
58             {
59                 NSTextField *field = [[NSTextField alloc] init];
60                 [field setEditable:NO];
61                 [field setBordered:NO];
62                 [field setDrawsBackground:NO];
63                 [field setFont:[NSFont systemFontOfSize:0]];
64                 [[field cell] setControlSize:NSRegularControlSize];
65                 [field setAutoresizingMask:NSViewNotSizable];
66                 return field;
67             }
68             case EXTENSION_WIDGET_TEXT_FIELD:
69             {
70                 VLCDialogTextField *field = [[VLCDialogTextField alloc] init];
71                 [field setWidget:widget];
72                 [field setAutoresizingMask:NSViewWidthSizable];
73                 [field setFont:[NSFont systemFontOfSize:0]];
74                 [[field cell] setControlSize:NSRegularControlSize];
75                 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(syncTextField:)  name:NSControlTextDidChangeNotification object:field];
76                 return field;
77             }
78             case EXTENSION_WIDGET_CHECK_BOX:
79             {
80                 VLCDialogButton *button = [[VLCDialogButton alloc] init];
81                 [button setButtonType:NSSwitchButton];
82                 [button setWidget:widget];
83                 [button setAction:@selector(triggerClick:)];
84                 [button setTarget:self];
85                 [[button cell] setControlSize:NSRegularControlSize];
86                 [button setAutoresizingMask:NSViewWidthSizable];
87                 return button;
88             }
89             case EXTENSION_WIDGET_BUTTON:
90             {
91                 VLCDialogButton *button = [[VLCDialogButton alloc] init];
92                 [button setBezelStyle:NSRoundedBezelStyle];
93                 [button setWidget:widget];
94                 [button setAction:@selector(triggerClick:)];
95                 [button setTarget:self];
96                 [[button cell] setControlSize:NSRegularControlSize];
97                 [button setAutoresizingMask:NSViewNotSizable];
98                 return button;
99             }
100             case EXTENSION_WIDGET_DROPDOWN:
101             {
102                 VLCDialogPopUpButton *popup = [[VLCDialogPopUpButton alloc] init];
103                 [popup setAction:@selector(popUpSelectionChanged:)];
104                 [popup setTarget:self];
105                 [popup setWidget:widget];
106                 return popup;
107             }
108             case EXTENSION_WIDGET_LIST:
109             {
110                 NSScrollView *scrollView = [[NSScrollView alloc] init];
111                 [scrollView setHasVerticalScroller:YES];
112                 VLCDialogList *list = [[VLCDialogList alloc] init];
113                 [list setUsesAlternatingRowBackgroundColors:YES];
114                 [list setHeaderView:nil];
115                 [list setAllowsMultipleSelection:YES];
116                 [scrollView setDocumentView:list];
117                 [scrollView setAutoresizingMask:NSViewHeightSizable | NSViewWidthSizable];
118
119                 NSTableColumn *column = [[NSTableColumn alloc] init];
120                 [list addTableColumn:column];
121                 [list setDataSource:list];
122                 [list setDelegate:self];
123                 [list setWidget:widget];
124                 return scrollView;
125             }
126             case EXTENSION_WIDGET_IMAGE:
127             {
128                 NSImageView *imageView = [[NSImageView alloc] init];
129                 [imageView setAutoresizingMask:NSViewHeightSizable | NSViewWidthSizable];
130                 [imageView setImageFrameStyle:NSImageFramePhoto];
131                 [imageView setImageScaling:NSImageScaleProportionallyUpOrDown];
132                 return imageView;
133             }
134             case EXTENSION_WIDGET_SPIN_ICON:
135             {
136                 NSProgressIndicator *spinner = [[NSProgressIndicator alloc] init];
137                 [spinner setUsesThreadedAnimation:YES];
138                 [spinner setStyle:NSProgressIndicatorSpinningStyle];
139                 [spinner setDisplayedWhenStopped:YES];
140                 [spinner startAnimation:self];
141                 return spinner;
142             }
143             default:
144                 return nil;
145         }
146     }
147 }
148
149 static void updateControlFromWidget(NSView *control, extension_widget_t *widget, id self)
150 {
151     @autoreleasepool {
152         switch (widget->type) {
153             case EXTENSION_WIDGET_HTML:
154             {
155                 // Get the web view
156                 assert([control isKindOfClass:[WebView class]]);
157                 WebView *webView = (WebView *)control;
158                 NSString *string = toNSStr(widget->psz_text);
159                 [[webView mainFrame] loadHTMLString:string baseURL:[NSURL URLWithString:@""]];
160                 [webView setNeedsDisplay:YES];
161                 break;
162             }
163             case EXTENSION_WIDGET_LABEL:
164             case EXTENSION_WIDGET_PASSWORD:
165             case EXTENSION_WIDGET_TEXT_FIELD:
166             {
167                 if (!widget->psz_text)
168                     break;
169                 assert([control isKindOfClass:[NSControl class]]);
170                 NSControl *field = (NSControl *)control;
171                 NSString *string = toNSStr(widget->psz_text);
172                 NSAttributedString *attrString = [[NSAttributedString alloc] initWithHTML:[string dataUsingEncoding: NSISOLatin1StringEncoding] documentAttributes:NULL];
173                 [field setAttributedStringValue:attrString];
174                 break;
175             }
176             case EXTENSION_WIDGET_CHECK_BOX:
177             case EXTENSION_WIDGET_BUTTON:
178             {
179                 assert([control isKindOfClass:[NSButton class]]);
180                 NSButton *button = (NSButton *)control;
181                 [button setTitle:toNSStr(widget->psz_text)];
182                 if (widget->type == EXTENSION_WIDGET_CHECK_BOX)
183                     [button setState:widget->b_checked ? NSOnState : NSOffState];
184                 break;
185             }
186             case EXTENSION_WIDGET_DROPDOWN:
187             {
188                 assert([control isKindOfClass:[NSPopUpButton class]]);
189                 NSPopUpButton *popup = (NSPopUpButton *)control;
190                 [popup removeAllItems];
191                 struct extension_widget_value_t *value;
192                 for (value = widget->p_values; value != NULL; value = value->p_next)
193                     [[popup menu] addItemWithTitle:toNSStr(value->psz_text) action:nil keyEquivalent:@""];
194
195                 [popup synchronizeTitleAndSelectedItem];
196                 [self popUpSelectionChanged:popup];
197                 break;
198             }
199             case EXTENSION_WIDGET_LIST:
200             {
201                 assert([control isKindOfClass:[NSScrollView class]]);
202                 NSScrollView *scrollView = (NSScrollView *)control;
203                 assert([[scrollView documentView] isKindOfClass:[VLCDialogList class]]);
204                 VLCDialogList *list = (VLCDialogList *)[scrollView documentView];
205
206                 NSMutableArray *contentArray = [NSMutableArray array];
207                 struct extension_widget_value_t *value;
208                 for (value = widget->p_values; value != NULL; value = value->p_next)
209                 {
210                     NSDictionary *entry = [NSDictionary dictionaryWithObjectsAndKeys:
211                                            [NSNumber numberWithInt:value->i_id], @"id",
212                                            toNSStr(value->psz_text), @"text",
213                                            nil];
214                     [contentArray addObject:entry];
215                 }
216                 list.contentArray = contentArray;
217                 [list reloadData];
218                 break;
219             }
220             case EXTENSION_WIDGET_IMAGE:
221             {
222                 assert([control isKindOfClass:[NSImageView class]]);
223                 NSImageView *imageView = (NSImageView *)control;
224                 NSString *string = widget->psz_text ? toNSStr(widget->psz_text) : nil;
225                 NSImage *image = nil;
226                 if (string)
227                     image = [[NSImage alloc] initWithContentsOfURL:[NSURL fileURLWithPath:string]];
228                 [imageView setImage:image];
229                 break;
230             }
231             case EXTENSION_WIDGET_SPIN_ICON:
232             {
233                 assert([control isKindOfClass:[NSProgressIndicator class]]);
234                 NSProgressIndicator *progressIndicator = (NSProgressIndicator *)control;
235                 if (widget->i_spin_loops != 0)
236                     [progressIndicator startAnimation:self];
237                 else
238                     [progressIndicator stopAnimation:self];
239                 break;
240             }
241         }
242     }
243 }
244
245 /**
246  * Ask the dialogs provider to create a new dialog
247  **/
248 static int extensionDialogCallback(vlc_object_t *p_this, const char *psz_variable,
249                                    vlc_value_t old_val, vlc_value_t new_val,
250                                    void *param)
251 {
252     @autoreleasepool {
253         (void) p_this;
254         (void) psz_variable;
255         (void) old_val;
256
257         ExtensionsDialogProvider *provider = (__bridge ExtensionsDialogProvider *)param;
258         if (!new_val.p_address)
259             return VLC_EGENERIC;
260
261         extension_dialog_t *p_dialog = (extension_dialog_t*) new_val.p_address;
262         [provider manageDialog:p_dialog];
263         return VLC_SUCCESS;
264     }
265 }
266
267 @implementation ExtensionsDialogProvider
268
269 - (id)init
270 {
271     self = [super init];
272     if (self) {
273         intf_thread_t *p_intf = VLCIntf;
274         var_Create(p_intf, "dialog-extension", VLC_VAR_ADDRESS);
275         var_AddCallback(p_intf, "dialog-extension", extensionDialogCallback, (__bridge void *)self);
276         dialog_Register(p_intf);
277     }
278     return self;
279 }
280
281 - (void)dealloc
282 {
283     var_DelCallback(VLCIntf, "dialog-extension", extensionDialogCallback, (__bridge void *)self);
284 }
285
286 - (void)performEventWithObject:(NSValue *)objectValue ofType:(const char*)type
287 {
288     NSString *typeString = toNSStr(type);
289
290     if ([typeString isEqualToString: @"dialog-extension"]) {
291         [self performSelectorOnMainThread:@selector(updateExtensionDialog:)
292                                withObject:objectValue
293                             waitUntilDone:YES];
294
295     }
296     else
297         msg_Err(VLCIntf, "unhandled dialog type: '%s'", type);
298 }
299
300 - (void)triggerClick:(id)sender
301 {
302     assert([sender isKindOfClass:[VLCDialogButton class]]);
303     VLCDialogButton *button = sender;
304     extension_widget_t *widget = [button widget];
305
306     vlc_mutex_lock(&widget->p_dialog->lock);
307     if (widget->type == EXTENSION_WIDGET_BUTTON)
308         extension_WidgetClicked(widget->p_dialog, widget);
309     else
310         widget->b_checked = [button state] == NSOnState;
311     vlc_mutex_unlock(&widget->p_dialog->lock);
312 }
313
314 - (void)syncTextField:(NSNotification *)notifcation
315 {
316     id sender = [notifcation object];
317     assert([sender isKindOfClass:[VLCDialogTextField class]]);
318     VLCDialogTextField *field = sender;
319     extension_widget_t *widget = [field widget];
320
321     vlc_mutex_lock(&widget->p_dialog->lock);
322     free(widget->psz_text);
323     widget->psz_text = strdup([[field stringValue] UTF8String]);
324     vlc_mutex_unlock(&widget->p_dialog->lock);
325 }
326
327 - (void)tableViewSelectionDidChange:(NSNotification *)notifcation
328 {
329     id sender = [notifcation object];
330     assert(sender && [sender isKindOfClass:[VLCDialogList class]]);
331     VLCDialogList *list = sender;
332
333     struct extension_widget_value_t *value;
334     unsigned i = 0;
335     NSIndexSet *selectedIndexes = [list selectedRowIndexes];
336     for (value = [list widget]->p_values; value != NULL; value = value->p_next, i++)
337         value->b_selected = (YES == [selectedIndexes containsIndex:i]);
338 }
339
340 - (void)popUpSelectionChanged:(id)sender
341 {
342     assert([sender isKindOfClass:[VLCDialogPopUpButton class]]);
343     VLCDialogPopUpButton *popup = sender;
344     struct extension_widget_value_t *value;
345     unsigned i = 0;
346     for (value = [popup widget]->p_values; value != NULL; value = value->p_next, i++)
347         value->b_selected = (i == [popup indexOfSelectedItem]);
348
349 }
350
351 - (NSSize)windowWillResize:(NSWindow *)sender toSize:(NSSize)frameSize
352 {
353     NSView *contentView = [sender contentView];
354     assert([contentView isKindOfClass:[VLCDialogGridView class]]);
355     VLCDialogGridView *gridView = (VLCDialogGridView *)contentView;
356
357     NSRect rect = NSMakeRect(0, 0, 0, 0);
358     rect.size = frameSize;
359     rect = [sender contentRectForFrameRect:rect];
360     rect.size = [gridView flexSize:rect.size];
361     rect = [sender frameRectForContentRect:rect];
362     return rect.size;
363 }
364
365 - (BOOL)windowShouldClose:(id)sender
366 {
367     assert([sender isKindOfClass:[VLCDialogWindow class]]);
368     VLCDialogWindow *window = sender;
369     extension_dialog_t *dialog = [window dialog];
370     extension_DialogClosed(dialog);
371     dialog->p_sys_intf = NULL;
372     return YES;
373 }
374
375 - (void)updateWidgets:(extension_dialog_t *)dialog
376 {
377     extension_widget_t *widget;
378     VLCDialogWindow *dialogWindow = (__bridge VLCDialogWindow *)(dialog->p_sys_intf);
379
380     FOREACH_ARRAY(widget, dialog->widgets) {
381         if (!widget)
382             continue; /* Some widgets may be NULL@this point */
383
384         BOOL shouldDestroy = widget->b_kill;
385         NSView *control = CFBridgingRelease(widget->p_sys_intf);
386         BOOL update = widget->b_update;
387
388         if (!control && !shouldDestroy) {
389             control = createControlFromWidget(widget, self);
390             updateControlFromWidget(control, widget, self);
391             widget->p_sys_intf = (void *)CFBridgingRetain(control);
392             update = YES; // Force update and repositionning
393             [control setHidden:widget->b_hide];
394         }
395
396         if (update && !shouldDestroy) {
397             updateControlFromWidget(control, widget, self);
398             [control setHidden:widget->b_hide];
399
400             int row = widget->i_row - 1;
401             int col = widget->i_column - 1;
402             int hsp = __MAX(1, widget->i_horiz_span);
403             int vsp = __MAX(1, widget->i_vert_span);
404             if (row < 0) {
405                 row = 4;
406                 col = 0;
407             }
408
409             VLCDialogGridView *gridView = (VLCDialogGridView *)[dialogWindow contentView];
410             [gridView addSubview:control atRow:row column:col rowSpan:vsp colSpan:hsp];
411
412             widget->b_update = false;
413         }
414
415         if (shouldDestroy) {
416             VLCDialogGridView *gridView = (VLCDialogGridView *)[dialogWindow contentView];
417             [gridView removeSubview:control];
418             widget->p_sys_intf = NULL;
419         }
420     }
421     FOREACH_END()
422 }
423
424 /** Create a dialog
425  * Note: Lock on p_dialog->lock must be held. */
426 - (VLCDialogWindow *)createExtensionDialog:(extension_dialog_t *)p_dialog
427 {
428     VLCDialogWindow *dialogWindow;
429
430     BOOL shouldDestroy = p_dialog->b_kill;
431     if (!shouldDestroy) {
432         NSRect content = NSMakeRect(0, 0, 1, 1);
433         dialogWindow = [[VLCDialogWindow alloc] initWithContentRect:content
434                                                           styleMask:NSTitledWindowMask | NSClosableWindowMask | NSResizableWindowMask
435                                                             backing:NSBackingStoreBuffered
436                                                               defer:NO];
437         [dialogWindow setDelegate:self];
438         [dialogWindow setDialog:p_dialog];
439         [dialogWindow setTitle:toNSStr(p_dialog->psz_title)];
440
441         VLCDialogGridView *gridView = [[VLCDialogGridView alloc] init];
442         [gridView setAutoresizingMask:NSViewHeightSizable | NSViewWidthSizable];
443         [dialogWindow setContentView:gridView];
444
445         p_dialog->p_sys_intf = (void *)CFBridgingRetain(dialogWindow);
446     }
447
448     [self updateWidgets:p_dialog];
449
450     if (shouldDestroy) {
451         [dialogWindow setDelegate:nil];
452         [dialogWindow close];
453         p_dialog->p_sys_intf = NULL;
454         dialogWindow = nil;
455     }
456
457     return dialogWindow;
458 }
459
460 /** Destroy a dialog
461  * Note: Lock on p_dialog->lock must be held. */
462 - (int)destroyExtensionDialog:(extension_dialog_t *)p_dialog
463 {
464     assert(p_dialog);
465
466     VLCDialogWindow *dialogWindow = CFBridgingRelease(p_dialog->p_sys_intf);
467     if (!dialogWindow) {
468         msg_Warn(VLCIntf, "dialog window not found");
469         return VLC_EGENERIC;
470     }
471
472     [dialogWindow setDelegate:nil];
473     dialogWindow = nil;
474
475     p_dialog->p_sys_intf = NULL;
476     vlc_cond_signal(&p_dialog->cond);
477     return VLC_SUCCESS;
478 }
479
480 /**
481  * Update/Create/Destroy a dialog
482  **/
483 - (VLCDialogWindow *)updateExtensionDialog:(NSValue *)o_value
484 {
485     extension_dialog_t *p_dialog = [o_value pointerValue];
486
487     VLCDialogWindow *dialogWindow = (__bridge VLCDialogWindow*) p_dialog->p_sys_intf;
488     if (p_dialog->b_kill && !dialogWindow) {
489         /* This extension could not be activated properly but tried
490            to create a dialog. We must ignore it. */
491         return NULL;
492     }
493
494     vlc_mutex_lock(&p_dialog->lock);
495     if (!p_dialog->b_kill && !dialogWindow) {
496         dialogWindow = [self createExtensionDialog:p_dialog];
497
498         BOOL visible = !p_dialog->b_hide;
499         if (visible) {
500             [dialogWindow center];
501             [dialogWindow makeKeyAndOrderFront:self];
502         } else
503             [dialogWindow orderOut:nil];
504
505         [dialogWindow setHas_lock:NO];
506     }
507     else if (!p_dialog->b_kill && dialogWindow) {
508         [dialogWindow setHas_lock:YES];
509         [self updateWidgets:p_dialog];
510         if (strcmp([[dialogWindow title] UTF8String],
511                     p_dialog->psz_title) != 0) {
512             NSString *titleString = toNSStr(p_dialog->psz_title);
513
514             [dialogWindow setTitle:titleString];
515         }
516
517         [dialogWindow setHas_lock:NO];
518
519         BOOL visible = !p_dialog->b_hide;
520         if (visible)
521             [dialogWindow makeKeyAndOrderFront:self];
522         else
523             [dialogWindow orderOut:nil];
524     }
525     else if (p_dialog->b_kill) {
526         [self destroyExtensionDialog:p_dialog];
527     }
528     vlc_cond_signal(&p_dialog->cond);
529     vlc_mutex_unlock(&p_dialog->lock);
530     return dialogWindow;
531 }
532
533 /**
534  * Ask the dialog manager to create/update/kill the dialog. Thread-safe.
535  **/
536 - (void)manageDialog:(extension_dialog_t *)p_dialog
537 {
538     assert(p_dialog);
539
540     NSValue *o_value = [NSValue valueWithPointer:p_dialog];
541     [self performSelectorOnMainThread:@selector(updateExtensionDialog:)
542                            withObject:o_value
543                         waitUntilDone:YES];
544 }
545
546 @end