1/******************************************************************************
2 * Copyright (c) 2007-2012 Transmission authors and contributors
3 *
4 * Permission is hereby granted, free of charge, to any person obtaining a
5 * copy of this software and associated documentation files (the "Software"),
6 * to deal in the Software without restriction, including without limitation
7 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
8 * and/or sell copies of the Software, and to permit persons to whom the
9 * Software is furnished to do so, subject to the following conditions:
10 *
11 * The above copyright notice and this permission notice shall be included in
12 * all copies or substantial portions of the Software.
13 *
14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
19 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
20 * DEALINGS IN THE SOFTWARE.
21 *****************************************************************************/
22
23#import "GroupsController.h"
24#import "NSMutableArrayAdditions.h"
25
26#define ICON_WIDTH 16.0
27#define ICON_WIDTH_SMALL 12.0
28
29@interface GroupsController (Private)
30
31- (void) saveGroups;
32
33- (NSImage *) imageForGroup: (NSMutableDictionary *) dict;
34
35- (BOOL) torrent: (Torrent *) torrent doesMatchRulesForGroupAtIndex: (NSInteger) index;
36
37@end
38
39@implementation GroupsController
40
41GroupsController * fGroupsInstance = nil;
42+ (GroupsController *) groups
43{
44    if (!fGroupsInstance)
45        fGroupsInstance = [[GroupsController alloc] init];
46    return fGroupsInstance;
47}
48
49- (id) init
50{
51    if ((self = [super init]))
52    {
53        NSData * data;
54        if ((data = [[NSUserDefaults standardUserDefaults] dataForKey: @"GroupDicts"]))
55            fGroups = [NSKeyedUnarchiver unarchiveObjectWithData: data];
56        else if ((data = [[NSUserDefaults standardUserDefaults] dataForKey: @"Groups"])) //handle old groups
57        {
58            fGroups = [NSUnarchiver unarchiveObjectWithData: data];
59            [[NSUserDefaults standardUserDefaults] removeObjectForKey: @"Groups"];
60            [self saveGroups];
61        }
62        else
63        {
64            //default groups
65            NSMutableDictionary * red = [NSMutableDictionary dictionaryWithObjectsAndKeys:
66                                            [NSColor redColor], @"Color",
67                                            NSLocalizedString(@"Red", "Groups -> Name"), @"Name",
68                                            @0, @"Index", nil];
69
70            NSMutableDictionary * orange = [NSMutableDictionary dictionaryWithObjectsAndKeys:
71                                            [NSColor orangeColor], @"Color",
72                                            NSLocalizedString(@"Orange", "Groups -> Name"), @"Name",
73                                            @1, @"Index", nil];
74
75            NSMutableDictionary * yellow = [NSMutableDictionary dictionaryWithObjectsAndKeys:
76                                            [NSColor yellowColor], @"Color",
77                                            NSLocalizedString(@"Yellow", "Groups -> Name"), @"Name",
78                                            @2, @"Index", nil];
79
80            NSMutableDictionary * green = [NSMutableDictionary dictionaryWithObjectsAndKeys:
81                                            [NSColor greenColor], @"Color",
82                                            NSLocalizedString(@"Green", "Groups -> Name"), @"Name",
83                                            @3, @"Index", nil];
84
85            NSMutableDictionary * blue = [NSMutableDictionary dictionaryWithObjectsAndKeys:
86                                            [NSColor blueColor], @"Color",
87                                            NSLocalizedString(@"Blue", "Groups -> Name"), @"Name",
88                                            @4, @"Index", nil];
89
90            NSMutableDictionary * purple = [NSMutableDictionary dictionaryWithObjectsAndKeys:
91                                            [NSColor purpleColor], @"Color",
92                                            NSLocalizedString(@"Purple", "Groups -> Name"), @"Name",
93                                            @5, @"Index", nil];
94
95            NSMutableDictionary * gray = [NSMutableDictionary dictionaryWithObjectsAndKeys:
96                                            [NSColor grayColor], @"Color",
97                                            NSLocalizedString(@"Gray", "Groups -> Name"), @"Name",
98                                            @6, @"Index", nil];
99
100            fGroups = [[NSMutableArray alloc] initWithObjects: red, orange, yellow, green, blue, purple, gray, nil];
101            [self saveGroups]; //make sure this is saved right away
102        }
103    }
104
105    return self;
106}
107
108
109- (NSInteger) numberOfGroups
110{
111    return [fGroups count];
112}
113
114- (NSInteger) rowValueForIndex: (NSInteger) index
115{
116    if (index != -1)
117    {
118        for (NSUInteger i = 0; i < [fGroups count]; i++)
119            if (index == [fGroups[i][@"Index"] integerValue])
120                return i;
121    }
122    return -1;
123}
124
125- (NSInteger) indexForRow: (NSInteger) row
126{
127    return [fGroups[row][@"Index"] integerValue];
128}
129
130- (NSString *) nameForIndex: (NSInteger) index
131{
132    NSInteger orderIndex = [self rowValueForIndex: index];
133    return orderIndex != -1 ? fGroups[orderIndex][@"Name"] : nil;
134}
135
136- (void) setName: (NSString *) name forIndex: (NSInteger) index
137{
138    NSInteger orderIndex = [self rowValueForIndex: index];
139    fGroups[orderIndex][@"Name"] = name;
140    [self saveGroups];
141
142    [[NSNotificationCenter defaultCenter] postNotificationName: @"UpdateGroups" object: self];
143}
144
145- (NSImage *) imageForIndex: (NSInteger) index
146{
147    NSInteger orderIndex = [self rowValueForIndex: index];
148    return orderIndex != -1 ? [self imageForGroup: fGroups[orderIndex]]
149                            : [NSImage imageNamed: @"GroupsNoneTemplate"];
150}
151
152- (NSColor *) colorForIndex: (NSInteger) index
153{
154    NSInteger orderIndex = [self rowValueForIndex: index];
155    return orderIndex != -1 ? fGroups[orderIndex][@"Color"] : nil;
156}
157
158- (void) setColor: (NSColor *) color forIndex: (NSInteger) index
159{
160    NSMutableDictionary * dict = fGroups[[self rowValueForIndex: index]];
161    [dict removeObjectForKey: @"Icon"];
162
163    dict[@"Color"] = color;
164
165    [[GroupsController groups] saveGroups];
166    [[NSNotificationCenter defaultCenter] postNotificationName: @"UpdateGroups" object: self];
167}
168
169- (BOOL) usesCustomDownloadLocationForIndex: (NSInteger) index
170{
171    if (![self customDownloadLocationForIndex: index])
172        return NO;
173
174    NSInteger orderIndex = [self rowValueForIndex: index];
175    return [fGroups[orderIndex][@"UsesCustomDownloadLocation"] boolValue];
176}
177
178- (void) setUsesCustomDownloadLocation: (BOOL) useCustomLocation forIndex: (NSInteger) index
179{
180    NSMutableDictionary * dict = fGroups[[self rowValueForIndex: index]];
181
182    dict[@"UsesCustomDownloadLocation"] = @(useCustomLocation);
183
184    [[GroupsController groups] saveGroups];
185}
186
187- (NSString *) customDownloadLocationForIndex: (NSInteger) index
188{
189    NSInteger orderIndex = [self rowValueForIndex: index];
190    return orderIndex != -1 ? fGroups[orderIndex][@"CustomDownloadLocation"] : nil;
191}
192
193- (void) setCustomDownloadLocation: (NSString *) location forIndex: (NSInteger) index
194{
195    NSMutableDictionary * dict = fGroups[[self rowValueForIndex: index]];
196    dict[@"CustomDownloadLocation"] = location;
197
198    [[GroupsController groups] saveGroups];
199}
200
201- (BOOL) usesAutoAssignRulesForIndex: (NSInteger) index
202{
203    NSInteger orderIndex = [self rowValueForIndex: index];
204    if (orderIndex == -1)
205        return NO;
206
207    NSNumber * assignRules = fGroups[orderIndex][@"UsesAutoGroupRules"];
208    return assignRules && [assignRules boolValue];
209}
210
211- (void) setUsesAutoAssignRules: (BOOL) useAutoAssignRules forIndex: (NSInteger) index
212{
213    NSMutableDictionary * dict = fGroups[[self rowValueForIndex: index]];
214
215    dict[@"UsesAutoGroupRules"] = @(useAutoAssignRules);
216
217    [[GroupsController groups] saveGroups];
218}
219
220- (NSPredicate *) autoAssignRulesForIndex: (NSInteger) index
221{
222    NSInteger orderIndex = [self rowValueForIndex: index];
223    if (orderIndex == -1)
224        return nil;
225
226    return fGroups[orderIndex][@"AutoGroupRules"];
227}
228
229- (void) setAutoAssignRules: (NSPredicate *) predicate forIndex: (NSInteger) index
230{
231    NSMutableDictionary * dict = fGroups[[self rowValueForIndex: index]];
232
233    if (predicate)
234    {
235        dict[@"AutoGroupRules"] = predicate;
236        [[GroupsController groups] saveGroups];
237    }
238    else
239    {
240        [dict removeObjectForKey: @"AutoGroupRules"];
241        [self setUsesAutoAssignRules: NO forIndex: index];
242    }
243}
244
245- (void) addNewGroup
246{
247    //find the lowest index
248    NSMutableIndexSet * candidates = [NSMutableIndexSet indexSetWithIndexesInRange: NSMakeRange(0, [fGroups count]+1)];
249    for (NSDictionary * dict in fGroups)
250        [candidates removeIndex: [dict[@"Index"] integerValue]];
251
252    const NSInteger index = [candidates firstIndex];
253
254    [fGroups addObject: [NSMutableDictionary dictionaryWithObjectsAndKeys: @(index), @"Index",
255                            [NSColor colorWithCalibratedRed: 0.0 green: 0.65 blue: 1.0 alpha: 1.0], @"Color", @"", @"Name", nil]];
256
257    [[NSNotificationCenter defaultCenter] postNotificationName: @"UpdateGroups" object: self];
258    [self saveGroups];
259}
260
261- (void) removeGroupWithRowIndex: (NSInteger) row
262{
263    NSInteger index = [fGroups[row][@"Index"] integerValue];
264    [fGroups removeObjectAtIndex: row];
265
266    [[NSNotificationCenter defaultCenter] postNotificationName: @"GroupValueRemoved" object: self userInfo:
267     @{@"Index": @(index)}];
268
269    if (index == [[NSUserDefaults standardUserDefaults] integerForKey: @"FilterGroup"])
270        [[NSUserDefaults standardUserDefaults] setInteger: -2 forKey: @"FilterGroup"];
271
272    [[NSNotificationCenter defaultCenter] postNotificationName: @"UpdateGroups" object: self];
273    [self saveGroups];
274}
275
276- (void) moveGroupAtRow: (NSInteger) oldRow toRow: (NSInteger) newRow
277{
278    [fGroups moveObjectAtIndex: oldRow toIndex: newRow];
279
280    [self saveGroups];
281    [[NSNotificationCenter defaultCenter] postNotificationName: @"UpdateGroups" object: self];
282}
283
284- (NSMenu *) groupMenuWithTarget: (id) target action: (SEL) action isSmall: (BOOL) small
285{
286    NSMenu * menu = [[NSMenu alloc] initWithTitle: @"Groups"];
287
288    NSMenuItem * item = [[NSMenuItem alloc] initWithTitle: NSLocalizedString(@"None", "Groups -> Menu") action: action
289                            keyEquivalent: @""];
290    [item setTarget: target];
291    [item setTag: -1];
292
293    NSImage * icon = [NSImage imageNamed: @"GroupsNoneTemplate"];
294    if (small)
295    {
296        icon = [icon copy];
297        [icon setSize: NSMakeSize(ICON_WIDTH_SMALL, ICON_WIDTH_SMALL)];
298
299        [item setImage: icon];
300    }
301    else
302        [item setImage: icon];
303
304    [menu addItem: item];
305
306    for (NSMutableDictionary * dict in fGroups)
307    {
308        item = [[NSMenuItem alloc] initWithTitle: dict[@"Name"] action: action keyEquivalent: @""];
309        [item setTarget: target];
310
311        [item setTag: [dict[@"Index"] integerValue]];
312
313        NSImage * icon = [self imageForGroup: dict];
314        if (small)
315        {
316            icon = [icon copy];
317            [icon setSize: NSMakeSize(ICON_WIDTH_SMALL, ICON_WIDTH_SMALL)];
318
319            [item setImage: icon];
320        }
321        else
322            [item setImage: icon];
323
324        [menu addItem: item];
325    }
326
327    return menu;
328}
329
330- (NSInteger) groupIndexForTorrent: (Torrent *) torrent
331{
332    for (NSDictionary * group in fGroups)
333    {
334        NSInteger row = [group[@"Index"] integerValue];
335        if ([self torrent: torrent doesMatchRulesForGroupAtIndex: row])
336            return row;
337    }
338    return -1;
339}
340
341@end
342
343@implementation GroupsController (Private)
344
345- (void) saveGroups
346{
347    //don't archive the icon
348    NSMutableArray * groups = [NSMutableArray arrayWithCapacity: [fGroups count]];
349    for (NSDictionary * dict in fGroups)
350    {
351        NSMutableDictionary * tempDict = [dict mutableCopy];
352        [tempDict removeObjectForKey: @"Icon"];
353        [groups addObject: tempDict];
354    }
355
356    [[NSUserDefaults standardUserDefaults] setObject: [NSKeyedArchiver archivedDataWithRootObject: groups] forKey: @"GroupDicts"];
357}
358
359- (NSImage *) imageForGroup: (NSMutableDictionary *) dict
360{
361    NSImage * image;
362    if ((image = dict[@"Icon"]))
363        return image;
364
365    NSRect rect = NSMakeRect(0.0, 0.0, ICON_WIDTH, ICON_WIDTH);
366
367    NSBezierPath * bp = [NSBezierPath bezierPathWithRoundedRect: rect xRadius: 3.0 yRadius: 3.0];
368    NSImage * icon = [[NSImage alloc] initWithSize: rect.size];
369
370    NSColor * color = dict[@"Color"];
371
372    [icon lockFocus];
373
374    //border
375    NSGradient * gradient = [[NSGradient alloc] initWithStartingColor: [color blendedColorWithFraction: 0.45 ofColor:
376                                [NSColor whiteColor]] endingColor: color];
377    [gradient drawInBezierPath: bp angle: 270.0];
378
379    //inside
380    bp = [NSBezierPath bezierPathWithRoundedRect: NSInsetRect(rect, 1.0, 1.0) xRadius: 3.0 yRadius: 3.0];
381    gradient = [[NSGradient alloc] initWithStartingColor: [color blendedColorWithFraction: 0.75 ofColor: [NSColor whiteColor]]
382                endingColor: [color blendedColorWithFraction: 0.2 ofColor: [NSColor whiteColor]]];
383    [gradient drawInBezierPath: bp angle: 270.0];
384
385    [icon unlockFocus];
386
387    dict[@"Icon"] = icon;
388
389    return icon;
390}
391
392- (BOOL) torrent: (Torrent *) torrent doesMatchRulesForGroupAtIndex: (NSInteger) index
393{
394    if (![self usesAutoAssignRulesForIndex: index])
395        return NO;
396
397    NSPredicate * predicate = [self autoAssignRulesForIndex: index];
398    BOOL eval = NO;
399    @try
400    {
401        eval = [predicate evaluateWithObject: torrent];
402    }
403    @catch (NSException * exception)
404    {
405        NSLog(@"Error when evaluating predicate (%@) - %@", predicate, exception);
406    }
407    @finally
408    {
409        return eval;
410    }
411}
412
413@end
414