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