b6b38ab045bb3beabc22def69bd751b36b7963d7
[vlc.git] / modules / gui / macosx / VLCAddonsWindowController.m
1 /*****************************************************************************
2  * VLCAddonsWindowController.m: Addons manager for the Mac
3  ****************************************************************************
4  * Copyright (C) 2014 VideoLAN and authors
5  * Author:       Felix Paul Kühne <fkuehne # videolan.org>
6  *
7  * This program is free software; you can redistribute it and/or modify
8  * it under the terms of the GNU General Public License as published by
9  * the Free Software Foundation; either version 2 of the License, or
10  * (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program; if not, write to the Free Software
19  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
20  *****************************************************************************/
21
22 #import <vlc_common.h>
23 #import <vlc_addons.h>
24
25 #import "VLCAddonsWindowController.h"
26 #import "VLCMain.h"
27 #import "VLCMainWindow.h"
28 #import "VLCAddonListItem.h"
29
30 @interface VLCAddonsWindowController() <NSTableViewDataSource, NSTableViewDelegate>
31 {
32     addons_manager_t *_manager;
33     NSMutableArray *_addons;
34     NSArray *_displayedAddons;
35     BOOL _shouldRefreshSideBarOnAddonChange;
36 }
37
38 - (void)addAddon:(NSValue *)o_value;
39 - (void)discoveryEnded;
40 - (void)addonChanged:(NSValue *)o_value;
41
42 @end
43
44 static void addonFoundCallback( addons_manager_t *manager,
45                                 addon_entry_t *entry )
46 {
47     VLCAddonsWindowController *controller = (__bridge VLCAddonsWindowController *) manager->owner.sys;
48
49     @autoreleasepool {
50         [controller performSelectorOnMainThread:@selector(addAddon:) withObject:[NSValue valueWithPointer:entry] waitUntilDone:NO];
51     }
52 }
53
54 static void addonsDiscoveryEndedCallback( addons_manager_t *manager )
55 {
56     VLCAddonsWindowController *controller = (__bridge VLCAddonsWindowController *) manager->owner.sys;
57
58     @autoreleasepool {
59         [controller performSelectorOnMainThread:@selector(discoveryEnded) withObject:nil waitUntilDone:NO];
60     }
61 }
62
63 static void addonsChangedCallback( addons_manager_t *manager,
64                                    addon_entry_t *entry )
65 {
66     VLCAddonsWindowController *controller = (__bridge VLCAddonsWindowController *) manager->owner.sys;
67
68     @autoreleasepool {
69         [controller performSelectorOnMainThread:@selector(addonChanged:) withObject:[NSValue valueWithPointer:entry] waitUntilDone:NO];
70     }
71 }
72
73 @implementation VLCAddonsWindowController
74
75 #pragma mark - object handling
76
77 - (id)init
78 {
79     self = [super initWithWindowNibName:@"AddonManager"];
80     if (self) {
81         [self setWindowFrameAutosaveName:@"addons"];
82         _addons = [[NSMutableArray alloc] init];
83     }
84
85     return self;
86 }
87
88 - (void)dealloc
89 {
90     if (_manager)
91         addons_manager_Delete(_manager);
92 }
93
94 #pragma mark - UI handling
95
96 - (void)windowDidLoad
97 {
98     [_typeSwitcher removeAllItems];
99     [_typeSwitcher addItemWithTitle:_NS("All")];
100     [[_typeSwitcher lastItem] setTag: -1];
101     /* no skins on OS X so far
102     [_typeSwitcher addItemWithTitle:_NS("Skins")];
103     [[_typeSwitcher lastItem] setTag:ADDON_SKIN2]; */
104     [_typeSwitcher addItemWithTitle:_NS("Playlist parsers")];
105     [[_typeSwitcher lastItem] setTag:ADDON_PLAYLIST_PARSER];
106     [_typeSwitcher addItemWithTitle:_NS("Service Discovery")];
107     [[_typeSwitcher lastItem] setTag:ADDON_SERVICE_DISCOVERY];
108     [_typeSwitcher addItemWithTitle:_NS("Interfaces")];
109     [[_typeSwitcher lastItem] setTag:ADDON_INTERFACE];
110     [_typeSwitcher addItemWithTitle:_NS("Art and meta fetchers")];
111     [[_typeSwitcher lastItem] setTag:ADDON_META];
112     [_typeSwitcher addItemWithTitle:_NS("Extensions")];
113     [[_typeSwitcher lastItem] setTag:ADDON_EXTENSION];
114
115     [_localAddonsOnlyCheckbox setTitle:_NS("Show Installed Only")];
116     [_localAddonsOnlyCheckbox setState:NSOffState];
117     [_downloadCatalogButton setTitle:_NS("Find more addons online")];
118     [_spinner setUsesThreadedAnimation:YES];
119
120     [self updateInstallButton:NO];
121     [_installButton setHidden:YES];
122
123     [_name setStringValue:@""];
124     [_author setStringValue:@""];
125     [_version setStringValue:@""];
126     [_description setString:@""];
127     [[self window] setTitle:_NS("Addons Manager")];
128
129     [[[_addonsTable tableColumnWithIdentifier:@"installed"] headerCell] setStringValue:_NS("Installed")];
130     [[[_addonsTable tableColumnWithIdentifier:@"name"] headerCell] setStringValue:_NS("Name")];
131     [[[_addonsTable tableColumnWithIdentifier:@"author"] headerCell] setStringValue:_NS("Author")];
132     [[[_addonsTable tableColumnWithIdentifier:@"type"] headerCell] setStringValue:_NS("Type")];
133
134     struct addons_manager_owner owner =
135     {
136         (__bridge void *)self,
137         addonFoundCallback,
138         addonsDiscoveryEndedCallback,
139         addonChangedCallback,
140     };
141
142     _manager = addons_manager_New((vlc_object_t *)getIntf(), &owner);
143     if (!_manager)
144         return;
145
146     [self _findInstalled];
147 }
148
149 - (IBAction)switchType:(id)sender
150 {
151     [self _refactorDataModel];
152 }
153
154 - (IBAction)toggleLocalCheckbox:(id)sender
155 {
156     [self _refactorDataModel];
157 }
158
159 - (IBAction)downloadCatalog:(id)sender
160 {
161     [self _findNewAddons];
162     [_downloadCatalogButton setHidden:YES];
163     [_localAddonsOnlyCheckbox setHidden:NO];
164 }
165
166 - (IBAction)installSelection:(id)sender
167 {
168     NSInteger selectedRow = [_addonsTable selectedRow];
169     if (selectedRow > _displayedAddons.count - 1 || selectedRow < 0)
170         return;
171
172     VLCAddonListItem *currentAddon = [_displayedAddons objectAtIndex:selectedRow];
173     [self _installAddonWithID:[currentAddon uuid] type:[currentAddon type]];
174
175     [_installButton setEnabled:NO];
176 }
177
178 - (IBAction)uninstallSelection:(id)sender
179 {
180     NSInteger selectedRow = [_addonsTable selectedRow];
181     if (selectedRow > _displayedAddons.count - 1 || selectedRow < 0)
182         return;
183
184     VLCAddonListItem *currentAddon = [_displayedAddons objectAtIndex:selectedRow];
185     [self _removeAddonWithID:[currentAddon uuid] type:[currentAddon type]];
186
187     [_installButton setEnabled:NO];
188 }
189
190 - (void)updateInstallButton:(BOOL)b_is_installed
191 {
192     [_installButton setHidden:NO];
193     [_installButton setEnabled:YES];
194
195     if (b_is_installed) {
196         [_installButton setTitle:_NS("Uninstall")];
197         [_installButton setAction:@selector(uninstallSelection:)];
198     } else {
199         [_installButton setTitle:_NS("Install")];
200         [_installButton setAction:@selector(installSelection:)];
201     }
202 }
203
204 - (NSInteger)numberOfRowsInTableView:(NSTableView *)aTableView
205 {
206     return [_displayedAddons count];
207 }
208
209 - (void)tableViewSelectionDidChange:(NSNotification *)aNotification
210 {
211     NSInteger selectedRow = [_addonsTable selectedRow];
212     if (selectedRow > _displayedAddons.count - 1 || selectedRow < 0) {
213         [_name setStringValue:@""];
214         [_author setStringValue:@""];
215         [_version setStringValue:@""];
216         [_description setString:@""];
217         [_installButton setHidden:YES];
218         return;
219     }
220
221     VLCAddonListItem *currentItem = [_displayedAddons objectAtIndex:selectedRow];
222     [_name setStringValue:[currentItem name]];
223     [_author setStringValue:[currentItem author]];
224     [_version setStringValue:[currentItem version]];
225
226     // Parse HTML description properly
227     NSMutableString *htmlDescription = [NSMutableString stringWithFormat:@"<style>body{ font-family: -apple-system-body, -apple-system, HelveticaNeue, Arial, sans-serif; }</style>%@", [currentItem description]];
228     [htmlDescription replaceOccurrencesOfString:@"\n" withString:@"<br />" options:NSLiteralSearch range:NSMakeRange(0, [htmlDescription length])];
229     NSAttributedString *attributedDescription = [[NSAttributedString alloc] initWithHTML:[htmlDescription dataUsingEncoding:NSUTF8StringEncoding]
230                                                                       documentAttributes:NULL];
231     [[_description textStorage] setAttributedString:attributedDescription];
232
233     [self updateInstallButton:[currentItem isInstalled]];
234 }
235
236 - (id)tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(NSInteger)rowIndex
237 {
238     NSString *identifier = [aTableColumn identifier];
239     if ([identifier isEqualToString:@"installed"]) {
240         return [[_displayedAddons objectAtIndex:rowIndex] isInstalled] ? @"✔" : @"✘";
241     } else if([identifier isEqualToString:@"name"])
242         return [[_displayedAddons objectAtIndex:rowIndex] name];
243
244     return @"";
245 }
246
247 #pragma mark - data handling
248
249 - (void)addAddon:(NSValue *)o_value
250 {
251     addon_entry_t *p_entry = [o_value pointerValue];
252     /* no skin support on OS X so far */
253     if (p_entry->e_type != ADDON_SKIN2)
254         [_addons addObject:[[VLCAddonListItem alloc] initWithAddon:p_entry]];
255 }
256
257 - (void)discoveryEnded
258 {
259     [self _refactorDataModel];
260     [_spinner stopAnimation:nil];
261 }
262
263 - (void)addonChanged:(NSValue *)o_value
264 {
265     [self _refactorDataModel];
266     if (_shouldRefreshSideBarOnAddonChange) {
267         [[[VLCMain sharedInstance] mainWindow] performSelector:@selector(reloadSidebar) withObject:nil afterDelay:0.5];
268         _shouldRefreshSideBarOnAddonChange = NO;
269     }
270 }
271
272 #pragma mark - helpers
273
274 - (void)_refactorDataModel
275 {
276     BOOL installedOnly = _localAddonsOnlyCheckbox.state == NSOnState;
277     int type = [[_typeSwitcher selectedItem] tag];
278
279     NSUInteger count = _addons.count;
280     NSMutableArray *filteredItems = [[NSMutableArray alloc] initWithCapacity:count];
281     for (NSUInteger x = 0; x < count; x++) {
282         VLCAddonListItem *currentItem = [_addons objectAtIndex:x];
283         if (type != -1) {
284             if ([currentItem type] == type) {
285                 if (installedOnly) {
286                     if ([currentItem isInstalled])
287                         [filteredItems addObject:currentItem];
288                 } else
289                     [filteredItems addObject:currentItem];
290             }
291         } else {
292             if (installedOnly) {
293                 if ([currentItem isInstalled])
294                     [filteredItems addObject:currentItem];
295             } else
296                 [filteredItems addObject:currentItem];
297         }
298     }
299
300     _displayedAddons = [NSArray arrayWithArray:filteredItems];
301
302     // update ui
303     [_addonsTable reloadData];
304     [[NSNotificationCenter defaultCenter] postNotificationName:NSTableViewSelectionDidChangeNotification object:_addonsTable];
305 }
306
307 - (void)_findNewAddons
308 {
309     [_spinner startAnimation:nil];
310     addons_manager_Gather(_manager, "repo://");
311 }
312
313 /* FIXME: un-used */
314 - (void)_findDesignatedAddon:(NSString *)uri
315 {
316     addons_manager_Gather(_manager, [uri UTF8String]);
317 }
318
319 - (void)_findInstalled
320 {
321     addons_manager_LoadCatalog(_manager);
322
323     // enqueue, to process the addons first
324     [self performSelectorOnMainThread:@selector(_refactorDataModel) withObject:nil waitUntilDone:NO];
325 }
326
327 - (void)_installAddonWithID:(NSData *)o_data type:(addon_type_t)type
328 {
329     addon_uuid_t uuid;
330     [o_data getBytes:uuid length:sizeof(uuid)];
331
332     if (type == ADDON_SERVICE_DISCOVERY)
333         _shouldRefreshSideBarOnAddonChange = YES;
334
335     addons_manager_Install(_manager, uuid);
336 }
337
338 - (void)_removeAddonWithID:(NSData *)o_data type:(addon_type_t)type
339 {
340     addon_uuid_t uuid;
341     [o_data getBytes:uuid length:sizeof(uuid)];
342
343     if (type == ADDON_SERVICE_DISCOVERY)
344         _shouldRefreshSideBarOnAddonChange = YES;
345
346     addons_manager_Remove(_manager, uuid);
347 }
348
349 - (NSString *)_getAddonType:(int)i_type
350 {
351     switch (i_type)
352     {
353         case ADDON_SKIN2:
354             return _NS("Skins");
355         case ADDON_PLAYLIST_PARSER:
356             return _NS("Playlist parsers");
357         case ADDON_SERVICE_DISCOVERY:
358             return _NS("Service Discovery");
359         case ADDON_EXTENSION:
360             return _NS("Extensions");
361         default:
362             return _NS("Unknown");
363     }
364 }
365
366 @end