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