1/* DockIcon.m
2 *
3 * Copyright (C) 2005-2014 Free Software Foundation, Inc.
4 *
5 * Author: Enrico Sersale <enrico@imago.ro>
6 * Date: January 2005
7 *
8 * This file is part of the GNUstep GWorkspace application
9 *
10 * This program is free software; you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License as published by
12 * the Free Software Foundation; either version 2 of the License, or
13 * (at your option) any later version.
14 *
15 * This program is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18 * GNU General Public License for more details.
19 *
20 * You should have received a copy of the GNU General Public License
21 * along with this program; if not, write to the Free Software
22 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02111 USA.
23 */
24
25#include <math.h>
26#include <unistd.h>
27
28#import <Foundation/Foundation.h>
29#import <AppKit/AppKit.h>
30
31#import "DockIcon.h"
32#import "Dock.h"
33#import "GWDesktopManager.h"
34#import "GWorkspace.h"
35
36@implementation DockIcon
37
38- (void)dealloc
39{
40  RELEASE (appName);
41  RELEASE (highlightColor);
42  RELEASE (darkerColor);
43  RELEASE (highlightImage);
44  RELEASE (trashFullIcon);
45  RELEASE (dragIcon);
46
47  [super dealloc];
48}
49
50- (id)initForNode:(FSNode *)anode
51          appName:(NSString *)aname
52         iconSize:(int)isize
53{
54  self = [super initForNode: anode
55               nodeInfoType: FSNInfoNameType
56               extendedType: nil
57                   iconSize: isize
58               iconPosition: NSImageOnly
59                  labelFont: [NSFont systemFontOfSize: 12]
60                  textColor: [NSColor controlTextColor]
61                  gridIndex: 0
62                  dndSource: NO
63                  acceptDnd: NO
64                  slideBack: NO];
65
66  if (self) {
67    if (aname != nil) {
68      ASSIGN (appName, aname);
69    } else {
70      ASSIGN (appName, [[node name] stringByDeletingPathExtension]);
71    }
72
73    dragIcon = [icon copy];
74
75    docked = NO;
76    launched = NO;
77    apphidden = NO;
78
79    nc = [NSNotificationCenter defaultCenter];
80    fm = [NSFileManager defaultManager];
81    ws = [NSWorkspace sharedWorkspace];
82
83    [self setToolTip: appName];
84  }
85
86  return self;
87}
88
89- (NSString *)appName
90{
91  return appName;
92}
93
94- (void)setWsIcon:(BOOL)value
95{
96  isWsIcon = value;
97  if (isWsIcon) {
98    [self removeAllToolTips];
99  }
100}
101
102- (BOOL)isWsIcon
103{
104  return isWsIcon;
105}
106
107- (void)setTrashIcon:(BOOL)value
108{
109  if (value != isTrashIcon) {
110    isTrashIcon = value;
111
112    if (isTrashIcon) {
113      NSArray *subNodes;
114      NSUInteger i, count;
115
116      ASSIGN (icon, [fsnodeRep trashIconOfSize: ceil(icnBounds.size.width)]);
117      ASSIGN (trashFullIcon, [fsnodeRep trashFullIconOfSize: ceil(icnBounds.size.width)]);
118
119      subNodes = [node subNodes];
120      count = [subNodes count];
121
122      for (i = 0; i < [subNodes count]; i++) {
123        if ([[subNodes objectAtIndex: i] isReserved]) {
124          count --;
125        }
126      }
127
128      [self setTrashFull: !(count == 0)];
129
130    } else {
131      ASSIGN (icon, [fsnodeRep iconOfSize: ceil(icnBounds.size.width)
132                                  forNode: node]);
133    }
134  }
135
136  if (isTrashIcon) {
137    [self removeAllToolTips];
138  }
139}
140
141- (void)setTrashFull:(BOOL)value
142{
143  trashFull = value;
144  [self setNeedsDisplay: YES];
145}
146
147- (BOOL)isTrashIcon
148{
149  return isTrashIcon;
150}
151
152- (BOOL)isSpecialIcon
153{
154  return (isWsIcon || isTrashIcon);
155}
156
157- (void)setDocked:(BOOL)value
158{
159  docked = value;
160}
161
162- (BOOL)isDocked
163{
164  return docked;
165}
166
167- (void)setLaunched:(BOOL)value
168{
169  launched = value;
170  [self setNeedsDisplay: YES];
171}
172
173- (BOOL)isLaunched
174{
175  return launched;
176}
177
178- (void)setAppHidden:(BOOL)value
179{
180  apphidden = value;
181  [self setNeedsDisplay: YES];
182  [container setNeedsDisplayInRect: [self frame]];
183}
184
185- (BOOL)isAppHidden
186{
187  return apphidden;
188}
189
190- (void)animateLaunch
191{
192	launching = YES;
193	dissFract = 0.2;
194
195	while (1) {
196		NSDate *date = [NSDate dateWithTimeIntervalSinceNow: 0.02];
197		[[NSRunLoop currentRunLoop] runUntilDate: date];
198		[self display];
199
200    dissFract += 0.05;
201	  if (dissFract >= 1) {
202		  launching = NO;
203      break;
204	  }
205	}
206
207  [self setNeedsDisplay: YES];
208}
209
210- (void)setHighlightColor:(NSColor *)color
211{
212  ASSIGN (highlightColor, [color highlightWithLevel: 0.2]);
213  ASSIGN (darkerColor, [color shadowWithLevel: 0.4]);
214}
215
216- (void)setHighlightImage:(NSImage *)image
217{
218  DESTROY (highlightImage);
219
220  if (image) {
221    NSSize size = [self frame].size;
222
223    highlightImage = [[NSImage alloc] initWithSize: size];
224    [highlightImage lockFocus];
225    [image compositeToPoint: NSZeroPoint
226                   fromRect: [self frame]
227                  operation: NSCompositeCopy];
228    [highlightImage unlockFocus];
229  }
230}
231
232- (void)setUseHlightImage:(BOOL)value
233{
234  useHligtImage = value;
235}
236
237- (void)setIsDndSourceIcon:(BOOL)value
238{
239  if (isDndSourceIcon != value) {
240    isDndSourceIcon = value;
241    [self setNeedsDisplay: YES];
242  }
243}
244
245- (void)setIconSize:(int)isize
246{
247  icnBounds = NSMakeRect(0, 0, isize, isize);
248  if (isTrashIcon) {
249    ASSIGN (icon, [fsnodeRep trashIconOfSize: ceil(icnBounds.size.width)]);
250    ASSIGN (trashFullIcon, [fsnodeRep trashFullIconOfSize: ceil(icnBounds.size.width)]);
251  } else {
252    ASSIGN (icon, [fsnodeRep iconOfSize: ceil(icnBounds.size.width)
253                                forNode: node]);
254  }
255  hlightRect.size.width = ceil(isize / 3 * 4);
256  hlightRect.size.height = ceil(hlightRect.size.width * [fsnodeRep highlightHeightFactor]);
257  if ((hlightRect.size.height - isize) < 4) {
258    hlightRect.size.height = isize + 4;
259  }
260  hlightRect.origin.x = 0;
261  hlightRect.origin.y = 0;
262  ASSIGN (highlightPath, [fsnodeRep highlightPathOfSize: hlightRect.size]);
263  [self tile];
264}
265
266- (void)mouseUp:(NSEvent *)theEvent
267{
268  if ([theEvent clickCount] > 1) {
269    if ([self isSpecialIcon] == NO) {
270      if (launched == NO) {
271        [ws launchApplication: appName];
272      } else if (apphidden) {
273        [[GWorkspace gworkspace] unhideAppWithPath: [node path] andName: appName];
274      } else {
275        [[GWorkspace gworkspace] activateAppWithPath: [node path] andName: appName];
276      }
277    } else if (isWsIcon) {
278      [[GWDesktopManager desktopManager] showRootViewer];
279
280    } else if (isTrashIcon) {
281      NSString *path = [node path];
282      [[GWDesktopManager desktopManager] selectFile: path inFileViewerRootedAtPath: path];
283    }
284  }
285}
286
287- (void)mouseDown:(NSEvent *)theEvent
288{
289	NSEvent *nextEvent = nil;
290  BOOL startdnd = NO;
291
292	if ([theEvent clickCount] == 1) {
293    [self select];
294
295    dragdelay = 0;
296    [(Dock *)container setDndSourceIcon: nil];
297
298    while (1) {
299	    nextEvent = [[self window] nextEventMatchingMask:
300    							              NSLeftMouseUpMask | NSLeftMouseDraggedMask];
301
302      if ([nextEvent type] == NSLeftMouseUp) {
303        [[self window] postEvent: nextEvent atStart: NO];
304	      [self unselect];
305        break;
306
307      } else if (([nextEvent type] == NSLeftMouseDragged)
308                                          && ([self isSpecialIcon] == NO)) {
309	      if (dragdelay < 5) {
310          dragdelay++;
311        } else {
312          startdnd = YES;
313          break;
314        }
315      }
316    }
317
318    if (startdnd == YES) {
319      [self startExternalDragOnEvent: theEvent withMouseOffset: NSZeroSize];
320    }
321	}
322}
323
324- (NSMenu *)menuForEvent:(NSEvent *)theEvent
325{
326  if ([self isSpecialIcon] == NO) {
327    NSString *appPath = [ws fullPathForApplication: appName];
328
329    if (appPath) {
330      CREATE_AUTORELEASE_POOL(arp);
331      NSMenu *menu = [[NSMenu alloc] initWithTitle: appName];
332      NSMenuItem *item;
333      GWLaunchedApp *app;
334
335      item = [NSMenuItem new];
336      [item setTitle: NSLocalizedString(@"Show In File Viewer", @"")];
337      [item setTarget: (Dock *)container];
338      [item setAction: @selector(iconMenuAction:)];
339      [item setRepresentedObject: appPath];
340      [menu addItem: item];
341      RELEASE (item);
342
343      app = [[GWorkspace gworkspace] launchedAppWithPath: appPath
344                                                 andName: appName];
345      if (app && [app isRunning]) {
346        item = [NSMenuItem new];
347        [item setTarget: (Dock *)container];
348        [item setAction: @selector(iconMenuAction:)];
349        [item setRepresentedObject: app];
350
351        if ([app isHidden]) {
352          [item setTitle: NSLocalizedString(@"Unhide", @"")];
353        } else {
354          [item setTitle: NSLocalizedString(@"Hide", @"")];
355        }
356
357        [menu addItem: item];
358        RELEASE (item);
359
360        item = [NSMenuItem new];
361        [item setTitle: NSLocalizedString(@"Quit", @"")];
362        [item setTarget: (Dock *)container];
363        [item setAction: @selector(iconMenuAction:)];
364        [item setRepresentedObject: app];
365        [menu addItem: item];
366        RELEASE (item);
367      }
368
369      RELEASE (arp);
370
371      return AUTORELEASE (menu);
372    }
373  }
374
375  return [super menuForEvent: theEvent];
376}
377
378- (void)startExternalDragOnEvent:(NSEvent *)event
379                 withMouseOffset:(NSSize)offset
380{
381  NSPasteboard *pb = [NSPasteboard pasteboardWithName: NSDragPboard];
382  NSMutableDictionary *dict = [NSMutableDictionary dictionary];
383
384  [dict setObject: appName forKey: @"name"];
385  [dict setObject: [node path] forKey: @"path"];
386  [dict setObject: [NSNumber numberWithBool: docked]
387           forKey: @"docked"];
388  [dict setObject: [NSNumber numberWithBool: launched]
389           forKey: @"launched"];
390  [dict setObject: [NSNumber numberWithBool: apphidden]
391           forKey: @"hidden"];
392
393  [pb declareTypes: [NSArray arrayWithObject: @"DockIconPboardType"]
394             owner: nil];
395
396  if ([pb setData: [NSArchiver archivedDataWithRootObject: dict]
397          forType: @"DockIconPboardType"]) {
398    NSPoint dragPoint = [event locationInWindow];
399    NSSize fs = [self frame].size;
400
401    dragPoint.x -= ((fs.width - icnPoint.x) / 2);
402    dragPoint.y -= ((fs.height - icnPoint.y) / 2);
403
404    [self unselect];
405    [self setIsDndSourceIcon: YES];
406    [(Dock *)container setDndSourceIcon: self];
407    [(Dock *)container tile];
408
409    [[self window] dragImage: dragIcon
410                          at: dragPoint
411                      offset: NSZeroSize
412                       event: event
413                  pasteboard: pb
414                      source: self
415                   slideBack: NO];
416  }
417}
418
419- (void)draggedImage:(NSImage *)anImage
420						 endedAt:(NSPoint)aPoint
421					 deposited:(BOOL)flag
422{
423	dragdelay = 0;
424  [self setIsDndSourceIcon: NO];
425  [(Dock *)container setDndSourceIcon: nil];
426}
427
428- (void)drawRect:(NSRect)rect
429{
430#define DRAWDOT(c1, c2, p) \
431{ \
432[c1 set]; \
433NSRectFill(NSMakeRect(p.x, p.y, 3, 2)); \
434[c2 set]; \
435NSRectFill(NSMakeRect(p.x + 1, p.y, 2, 1)); \
436NSRectFill(NSMakeRect(p.x + 2, p.y + 1, 1, 1)); \
437}
438
439#define DRAWDOTS(c1, c2, p) \
440{ \
441int i, x = p.x, y = p.y; \
442for (i = 0; i < 3; i++) { \
443[c1 set]; \
444NSRectFill(NSMakeRect(x, y, 3, 2)); \
445[c2 set]; \
446NSRectFill(NSMakeRect(x + 1, y, 2, 1)); \
447NSRectFill(NSMakeRect(x + 2, y + 1, 1, 1)); \
448x += 6; \
449} \
450}
451
452  if (isSelected || launching) {
453    [highlightColor set];
454    NSRectFill(rect);
455
456    if (highlightImage && useHligtImage) {
457      [highlightImage dissolveToPoint: NSZeroPoint fraction: 0.2];
458    }
459  }
460
461  if (launching) {
462	  [icon dissolveToPoint: icnPoint fraction: dissFract];
463	  return;
464  }
465
466  if (isDndSourceIcon == NO) {
467    if (isTrashIcon == NO) {
468      [icon compositeToPoint: icnPoint operation: NSCompositeSourceOver];
469    } else {
470      if (trashFull) {
471        [trashFullIcon compositeToPoint: icnPoint operation: NSCompositeSourceOver];
472      } else {
473        [icon compositeToPoint: icnPoint operation: NSCompositeSourceOver];
474      }
475    }
476
477    if ((isWsIcon == NO) && (isTrashIcon == NO)) {
478      if (apphidden) {
479        DRAWDOT (darkerColor, [NSColor whiteColor], NSMakePoint(4, 2));
480      } else if (launched == NO) {
481        DRAWDOTS (darkerColor, [NSColor whiteColor], NSMakePoint(4, 2));
482      }
483    }
484  }
485}
486
487- (BOOL)acceptsDraggedPaths:(NSArray *)paths
488{
489  unsigned i;
490
491  if ([self isSpecialIcon] == NO) {
492    for (i = 0; i < [paths count]; i++) {
493      NSString *path = [paths objectAtIndex: i];
494      FSNode *nod = [FSNode nodeWithPath: path];
495
496      if (([nod isPlain] || ([nod isPackage] && ([nod isApplication] == NO))) == NO) {
497        return NO;
498      }
499    }
500
501    [self select];
502    return YES;
503
504  } else if (isTrashIcon) {
505    NSString *fromPath = [[paths objectAtIndex: 0] stringByDeletingLastPathComponent];
506    BOOL accept = YES;
507
508    if ([fromPath isEqual: [[GWDesktopManager desktopManager] trashPath]] == NO) {
509      NSArray *vpaths = [ws mountedLocalVolumePaths];
510
511      for (i = 0; i < [paths count]; i++) {
512        NSString *path = [paths objectAtIndex: i];
513
514        if (([vpaths containsObject: path] == NO)
515                          && ([fm isWritableFileAtPath: path] == NO)) {
516          accept = NO;
517          break;
518        }
519      }
520    } else {
521      accept = NO;
522    }
523
524    if (accept) {
525      [self select];
526    }
527
528    return accept;
529  }
530
531  return NO;
532}
533
534- (void)setDraggedPaths:(NSArray *)paths
535{
536  NSUInteger i;
537
538  [self unselect];
539
540  if ([self isSpecialIcon] == NO)
541    {
542      for (i = 0; i < [paths count]; i++)
543        {
544          NSString *path = [paths objectAtIndex: i];
545          FSNode *nod = [FSNode nodeWithPath: path];
546
547          if ([nod isPlain] || ([nod isPackage] && ([nod isApplication] == NO)))
548            {
549              NS_DURING
550                {
551                  [ws openFile: path withApplication: appName];
552                }
553              NS_HANDLER
554                {
555                  NSRunAlertPanel(NSLocalizedString(@"error", @""),
556                                  [NSString stringWithFormat: @"%@ %@!",
557                                            NSLocalizedString(@"Can't open ", @""), [path lastPathComponent]],
558                                  NSLocalizedString(@"OK", @""),
559                                  nil,
560                                  nil);
561                }
562              NS_ENDHANDLER
563             }
564        }
565    }
566  else if (isTrashIcon) // FIXME this is largely similar to RecyclerIcon ####
567    {
568      NSArray *vpaths = [ws mountedLocalVolumePaths];
569      NSMutableArray *files = [NSMutableArray array];
570      NSMutableArray *umountPaths = [NSMutableArray array];
571      NSMutableDictionary *opinfo = [NSMutableDictionary dictionary];
572
573      for (i = 0; i < [paths count]; i++)
574        {
575          NSString *srcpath = [paths objectAtIndex: i];
576
577          if ([vpaths containsObject: srcpath])
578            {
579              [umountPaths addObject: srcpath];
580            }
581          else
582            {
583              [files addObject: [srcpath lastPathComponent]];
584            }
585        }
586
587    for (i = 0; i < [umountPaths count]; i++)
588      {
589        NSString *umpath = [umountPaths objectAtIndex: i];
590
591        if (![ws unmountAndEjectDeviceAtPath: umpath])
592          {
593	    NSString *err = NSLocalizedString(@"Error", @"");
594	    NSString *msg = NSLocalizedString(@"You are not allowed to umount\n", @"");
595	    NSString *buttstr = NSLocalizedString(@"Continue", @"");
596            NSRunAlertPanel(err, [NSString stringWithFormat: @"%@ \"%@\"!\n", msg, umpath], buttstr, nil, nil);
597	    [[GWDesktopManager desktopManager] unlockVolumeAtPath:umpath];
598          }
599      }
600
601    if ([files count])
602      {
603        NSString *fromPath = [[paths objectAtIndex: 0] stringByDeletingLastPathComponent];
604
605        if ([fm isWritableFileAtPath: fromPath] == NO) {
606          NSString *err = NSLocalizedString(@"Error", @"");
607          NSString *msg = NSLocalizedString(@"You do not have write permission\nfor", @"");
608          NSString *buttstr = NSLocalizedString(@"Continue", @"");
609          NSRunAlertPanel(err, [NSString stringWithFormat: @"%@ \"%@\"!\n", msg, fromPath], buttstr, nil, nil);
610          return;
611        }
612
613        [opinfo setObject: NSWorkspaceRecycleOperation forKey: @"operation"];
614        [opinfo setObject: fromPath forKey: @"source"];
615        [opinfo setObject: [node path] forKey: @"destination"];
616        [opinfo setObject: files forKey: @"files"];
617
618        [[GWDesktopManager desktopManager] performFileOperation: opinfo];
619      }
620    }
621}
622
623@end
624
625