1/*****************************************************************************
2 * VLCExtensionsDialogProvider.m: Mac OS X Extensions Dialogs
3 *****************************************************************************
4 * Copyright (C) 2010-2015 VLC authors and VideoLAN
5 * $Id: f9c66f51be380738bb3aa6f7a85171a2700f6899 $
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
41static void extensionDialogCallback(extension_dialog_t *p_ext_dialog,
42                                    void *p_data);
43
44static 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                msg_Err(getIntf(), "Unhandled Widget type %i", widget->type);
144                return nil;
145        }
146    }
147}
148
149static 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
249static void extensionDialogCallback(extension_dialog_t *p_ext_dialog,
250                                    void *p_data)
251
252{
253    @autoreleasepool {
254        VLCExtensionsDialogProvider *provider = (__bridge VLCExtensionsDialogProvider *)p_data;
255        if (!provider)
256            return;
257
258        [provider manageDialog:p_ext_dialog];
259        return;
260    }
261}
262
263@implementation VLCExtensionsDialogProvider
264
265- (id)init
266{
267    self = [super init];
268    if (self) {
269        intf_thread_t *p_intf = getIntf();
270        vlc_dialog_provider_set_ext_callback(p_intf, extensionDialogCallback, (__bridge void *)self);
271    }
272    return self;
273}
274
275- (void)dealloc
276{
277    vlc_dialog_provider_set_ext_callback(getIntf(), NULL, NULL);
278}
279
280- (void)performEventWithObject:(NSValue *)objectValue ofType:(const char*)type
281{
282    NSString *typeString = toNSStr(type);
283
284    if ([typeString isEqualToString: @"dialog-extension"]) {
285        [self performSelectorOnMainThread:@selector(updateExtensionDialog:)
286                               withObject:objectValue
287                            waitUntilDone:YES];
288
289    }
290    else
291        msg_Err(getIntf(), "unhandled dialog type: '%s'", type);
292}
293
294- (void)triggerClick:(id)sender
295{
296    assert([sender isKindOfClass:[VLCDialogButton class]]);
297    VLCDialogButton *button = sender;
298    extension_widget_t *widget = [button widget];
299
300    vlc_mutex_lock(&widget->p_dialog->lock);
301    if (widget->type == EXTENSION_WIDGET_BUTTON)
302        extension_WidgetClicked(widget->p_dialog, widget);
303    else
304        widget->b_checked = [button state] == NSOnState;
305    vlc_mutex_unlock(&widget->p_dialog->lock);
306}
307
308- (void)syncTextField:(NSNotification *)notifcation
309{
310    id sender = [notifcation object];
311    assert([sender isKindOfClass:[VLCDialogTextField class]]);
312    VLCDialogTextField *field = sender;
313    extension_widget_t *widget = [field widget];
314
315    vlc_mutex_lock(&widget->p_dialog->lock);
316    free(widget->psz_text);
317    widget->psz_text = strdup([[field stringValue] UTF8String]);
318    vlc_mutex_unlock(&widget->p_dialog->lock);
319}
320
321- (void)tableViewSelectionDidChange:(NSNotification *)notifcation
322{
323    id sender = [notifcation object];
324    assert(sender && [sender isKindOfClass:[VLCDialogList class]]);
325    VLCDialogList *list = sender;
326
327    struct extension_widget_value_t *value;
328    unsigned i = 0;
329    NSIndexSet *selectedIndexes = [list selectedRowIndexes];
330    for (value = [list widget]->p_values; value != NULL; value = value->p_next, i++)
331        value->b_selected = (YES == [selectedIndexes containsIndex:i]);
332}
333
334- (void)popUpSelectionChanged:(id)sender
335{
336    assert([sender isKindOfClass:[VLCDialogPopUpButton class]]);
337    VLCDialogPopUpButton *popup = sender;
338    struct extension_widget_value_t *value;
339    unsigned i = 0;
340    for (value = [popup widget]->p_values; value != NULL; value = value->p_next, i++)
341        value->b_selected = (i == [popup indexOfSelectedItem]);
342
343}
344
345- (NSSize)windowWillResize:(NSWindow *)sender toSize:(NSSize)frameSize
346{
347    NSView *contentView = [sender contentView];
348    assert([contentView isKindOfClass:[VLCDialogGridView class]]);
349    VLCDialogGridView *gridView = (VLCDialogGridView *)contentView;
350
351    NSRect rect = NSMakeRect(0, 0, 0, 0);
352    rect.size = frameSize;
353    rect = [sender contentRectForFrameRect:rect];
354    rect.size = [gridView flexSize:rect.size];
355    rect = [sender frameRectForContentRect:rect];
356    return rect.size;
357}
358
359- (BOOL)windowShouldClose:(id)sender
360{
361    assert([sender isKindOfClass:[VLCDialogWindow class]]);
362    VLCDialogWindow *window = sender;
363    extension_dialog_t *dialog = [window dialog];
364    extension_DialogClosed(dialog);
365    dialog->p_sys_intf = NULL;
366    return YES;
367}
368
369- (void)updateWidgets:(extension_dialog_t *)dialog
370{
371    extension_widget_t *widget;
372    VLCDialogWindow *dialogWindow = (__bridge VLCDialogWindow *)(dialog->p_sys_intf);
373
374    FOREACH_ARRAY(widget, dialog->widgets) {
375        if (!widget)
376            continue; /* Some widgets may be NULL@this point */
377
378        BOOL shouldDestroy = widget->b_kill;
379
380        /* Ownership should not be transfered back to ARC here, as
381         * we might just want to update something.
382         */
383        NSView *control = (__bridge NSView *)widget->p_sys_intf;
384        BOOL update = widget->b_update;
385
386        if (!control && !shouldDestroy) {
387            control = createControlFromWidget(widget, self);
388            if (control == NULL)
389                msg_Err(getIntf(), "Failed to create control from widget!");
390            updateControlFromWidget(control, widget, self);
391            /* Ownership needs to be given-up, if ARC would remain with the
392             * ownership, the object could be freed while it is still referenced
393             * and the invalid reference would be used later.
394             */
395            widget->p_sys_intf = (__bridge_retained void *)control;
396            update = YES; // Force update and repositionning
397            [control setHidden:widget->b_hide];
398        }
399
400        if (update && !shouldDestroy) {
401            updateControlFromWidget(control, widget, self);
402            [control setHidden:widget->b_hide];
403
404            int row = widget->i_row - 1;
405            int col = widget->i_column - 1;
406            int hsp = __MAX(1, widget->i_horiz_span);
407            int vsp = __MAX(1, widget->i_vert_span);
408            if (row < 0) {
409                row = 4;
410                col = 0;
411            }
412
413            VLCDialogGridView *gridView = (VLCDialogGridView *)[dialogWindow contentView];
414            [gridView updateSubview:control atRow:row column:col rowSpan:vsp colSpan:hsp];
415
416            widget->b_update = false;
417        }
418
419        if (shouldDestroy) {
420            VLCDialogGridView *gridView = (VLCDialogGridView *)[dialogWindow contentView];
421            [gridView removeSubview:control];
422            /* Explicitily release here, as we do not have transfered ownership to ARC,
423             * given that not in all cases we want to destroy the widget.
424             */
425            if (widget->p_sys_intf) {
426                CFRelease(widget->p_sys_intf);
427                widget->p_sys_intf = NULL;
428            }
429        }
430    }
431    FOREACH_END()
432}
433
434/** Create a dialog
435 * Note: Lock on p_dialog->lock must be held. */
436- (VLCDialogWindow *)createExtensionDialog:(extension_dialog_t *)p_dialog
437{
438    VLCDialogWindow *dialogWindow;
439
440    BOOL shouldDestroy = p_dialog->b_kill;
441    if (!shouldDestroy) {
442        NSRect content = NSMakeRect(0, 0, 1, 1);
443        dialogWindow = [[VLCDialogWindow alloc] initWithContentRect:content
444                                                          styleMask:NSTitledWindowMask | NSClosableWindowMask | NSResizableWindowMask
445                                                            backing:NSBackingStoreBuffered
446                                                              defer:NO];
447        [dialogWindow setDelegate:self];
448        [dialogWindow setDialog:p_dialog];
449        [dialogWindow setTitle:toNSStr(p_dialog->psz_title)];
450
451        VLCDialogGridView *gridView = [[VLCDialogGridView alloc] init];
452        [gridView setAutoresizingMask:NSViewHeightSizable | NSViewWidthSizable];
453        [dialogWindow setContentView:gridView];
454
455        p_dialog->p_sys_intf = (void *)CFBridgingRetain(dialogWindow);
456    }
457
458    [self updateWidgets:p_dialog];
459
460    if (shouldDestroy) {
461        [dialogWindow setDelegate:nil];
462        [dialogWindow close];
463        p_dialog->p_sys_intf = NULL;
464        dialogWindow = nil;
465    }
466
467    return dialogWindow;
468}
469
470/** Destroy a dialog
471 * Note: Lock on p_dialog->lock must be held. */
472- (int)destroyExtensionDialog:(extension_dialog_t *)p_dialog
473{
474    assert(p_dialog);
475
476    /* FIXME: Creating the dialog, we CFBridgingRetain p_sys_intf but we can't
477     *        just CFBridgingRelease it here, as that causes a crash.
478     */
479    VLCDialogWindow *dialogWindow = (__bridge VLCDialogWindow*)p_dialog->p_sys_intf;
480    if (!dialogWindow) {
481        msg_Warn(getIntf(), "dialog window not found");
482        return VLC_EGENERIC;
483    }
484
485    [dialogWindow setDelegate:nil];
486    [dialogWindow close];
487    dialogWindow = nil;
488
489    p_dialog->p_sys_intf = NULL;
490    vlc_cond_signal(&p_dialog->cond);
491    return VLC_SUCCESS;
492}
493
494/**
495 * Update/Create/Destroy a dialog
496 **/
497- (VLCDialogWindow *)updateExtensionDialog:(NSValue *)o_value
498{
499    extension_dialog_t *p_dialog = [o_value pointerValue];
500
501    VLCDialogWindow *dialogWindow = (__bridge VLCDialogWindow*) p_dialog->p_sys_intf;
502    if (p_dialog->b_kill && !dialogWindow) {
503        /* This extension could not be activated properly but tried
504           to create a dialog. We must ignore it. */
505        return NULL;
506    }
507
508    vlc_mutex_lock(&p_dialog->lock);
509    if (!p_dialog->b_kill && !dialogWindow) {
510        dialogWindow = [self createExtensionDialog:p_dialog];
511
512        BOOL visible = !p_dialog->b_hide;
513        if (visible) {
514            [dialogWindow center];
515            [dialogWindow makeKeyAndOrderFront:self];
516        } else
517            [dialogWindow orderOut:nil];
518
519        [dialogWindow setHas_lock:NO];
520    }
521    else if (!p_dialog->b_kill && dialogWindow) {
522        [dialogWindow setHas_lock:YES];
523        [self updateWidgets:p_dialog];
524        if (strcmp([[dialogWindow title] UTF8String],
525                    p_dialog->psz_title) != 0) {
526            NSString *titleString = toNSStr(p_dialog->psz_title);
527
528            [dialogWindow setTitle:titleString];
529        }
530
531        [dialogWindow setHas_lock:NO];
532
533        BOOL visible = !p_dialog->b_hide;
534        if (visible)
535            [dialogWindow makeKeyAndOrderFront:self];
536        else
537            [dialogWindow orderOut:nil];
538    }
539    else if (p_dialog->b_kill) {
540        [self destroyExtensionDialog:p_dialog];
541    }
542    vlc_cond_signal(&p_dialog->cond);
543    vlc_mutex_unlock(&p_dialog->lock);
544    return dialogWindow;
545}
546
547/**
548 * Ask the dialog manager to create/update/kill the dialog. Thread-safe.
549 **/
550- (void)manageDialog:(extension_dialog_t *)p_dialog
551{
552    assert(p_dialog);
553
554    NSValue *o_value = [NSValue valueWithPointer:p_dialog];
555    [self performSelectorOnMainThread:@selector(updateExtensionDialog:)
556                           withObject:o_value
557                        waitUntilDone:YES];
558}
559
560@end
561