1/** <title>GSTheme</title>
2
3   <abstract>Useful/configurable drawing functions</abstract>
4
5   Copyright (C) 2004 Free Software Foundation, Inc.
6
7   Author: Adam Fedor <fedor@gnu.org>
8   Date: Jan 2004
9
10   This file is part of the GNU Objective C User interface library.
11
12   This library is free software; you can redistribute it and/or
13   modify it under the terms of the GNU Lesser General Public
14   License as published by the Free Software Foundation; either
15   version 2 of the License, or (at your option) any later version.
16
17   This library 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 GNU
20   Lesser General Public License for more details.
21
22   You should have received a copy of the GNU Lesser General Public
23   License along with this library; see the file COPYING.LIB.
24   If not, see <http://www.gnu.org/licenses/> or write to the
25   Free Software Foundation, 51 Franklin Street, Fifth Floor,
26   Boston, MA 02110-1301, USA.
27*/
28
29#import <Foundation/NSBundle.h>
30#import <Foundation/NSDebug.h>
31#import <Foundation/NSDictionary.h>
32#import <Foundation/NSException.h>
33#import <Foundation/NSFileManager.h>
34#import <Foundation/NSInvocation.h>
35#import <Foundation/NSMapTable.h>
36#import <Foundation/NSMethodSignature.h>
37#import <Foundation/NSNotification.h>
38#import <Foundation/NSNull.h>
39#import <Foundation/NSPathUtilities.h>
40#import <Foundation/NSSet.h>
41#import <Foundation/NSUserDefaults.h>
42#import "GNUstepGUI/GSTheme.h"
43#import "AppKit/NSApplication.h"
44#import "AppKit/NSButtonCell.h"
45#import "AppKit/NSButton.h"
46#import "AppKit/NSColor.h"
47#import "AppKit/NSColorList.h"
48#import "AppKit/NSGraphics.h"
49#import "AppKit/NSImage.h"
50#import "AppKit/NSImageView.h"
51#import "AppKit/NSMatrix.h"
52#import "AppKit/NSMenu.h"
53#import "AppKit/NSPanel.h"
54#import "AppKit/NSScrollView.h"
55#import "AppKit/NSSegmentedControl.h"
56#import "AppKit/NSTextContainer.h"
57#import "AppKit/NSTextField.h"
58#import "AppKit/NSTextView.h"
59#import "AppKit/NSScrollView.h"
60#import "AppKit/NSView.h"
61#import "AppKit/NSWindow.h"
62#import "AppKit/NSBezierPath.h"
63#import "AppKit/PSOperators.h"
64#import "GSThemePrivate.h"
65
66NSString	*GSSwitch = @"GSSwitch";
67NSString        *GSRadio = @"GSRadio";
68
69// Scroller part names
70NSString	*GSScrollerDownArrow = @"GSScrollerDownArrow";
71NSString	*GSScrollerHorizontalKnob = @"GSScrollerHorizontalKnob";
72NSString	*GSScrollerHorizontalSlot = @"GSScrollerHorizontalSlot";
73NSString	*GSScrollerLeftArrow = @"GSScrollerLeftArrow";
74NSString	*GSScrollerRightArrow = @"GSScrollerRightArrow";
75NSString	*GSScrollerUpArrow = @"GSScrollerUpArrow";
76NSString	*GSScrollerVerticalKnob = @"GSScrollerVerticalKnob";
77NSString	*GSScrollerVerticalSlot = @"GSScrollerVerticalSlot";
78
79// Table view part names
80NSString	*GSTableHeader = @"GSTableHeader";
81NSString	*GSTableCorner = @"GSTableCorner";
82
83// Browser part names
84NSString        *GSBrowserHeader = @"GSBrowserHeader";
85
86// Menu part names
87NSString        *GSMenuHorizontalBackground = @"GSMenuHorizontalBackground";
88NSString        *GSMenuVerticalBackground = @"GSMenuVerticalBackground";
89NSString        *GSMenuTitleBackground = @"GSMenuTitleBackground";
90NSString        *GSMenuHorizontalItem = @"GSMenuHorizontalItem";
91NSString        *GSMenuVerticalItem = @"GSMenuVerticalItem";
92NSString        *GSMenuSeparatorItem = @"GSMenuSeparatorItem";
93
94// NSPopUpButton part names
95NSString        *GSPopUpButton = @"GSPopUpButton";
96
97// Progress indicator part names
98NSString        *GSProgressIndicatorBezel = @"GSProgressIndicatorBezel";
99NSString        *GSProgressIndicatorBarDeterminate
100  = @"GSProgressIndicatorBarDeterminate";
101
102// Color well part names
103NSString	*GSColorWell = @"GSColorWell";
104NSString        *GSColorWellInnerBorder = @"GSColorWellInnerBorder";
105
106// Slider part names
107NSString        *GSSliderHorizontalTrack = @"GSSliderHorizontalTrack";
108NSString        *GSSliderVerticalTrack = @"GSSliderVerticalTrack";
109
110// NSBox parts
111NSString        *GSBoxBorder = @"GSBoxBorder";
112
113/* NSTabView parts */
114NSString        *GSTabViewSelectedTabFill
115  = @"GSTabViewSelectedTabFill";
116NSString        *GSTabViewUnSelectedTabFill
117  = @"GSTabViewUnSelectedTabFill";
118NSString        *GSTabViewBackgroundTabFill
119  = @"GSTabViewBackgroundTabFill";
120NSString        *GSTabViewBottomSelectedTabFill
121  = @"GSTabViewBottomSelectedTabFill";
122NSString        *GSTabViewBottomUnSelectedTabFill
123  = @"GSTabViewBottomUnSelectedTabFill";
124NSString        *GSTabViewBottomBackgroundTabFill
125  = @"GSTabViewBottomBackgroundTabFill";
126NSString        *GSTabViewLeftSelectedTabFill
127  = @"GSTabViewLeftSelectedTabFill";
128NSString        *GSTabViewLeftUnSelectedTabFill
129  = @"GSTabViewLeftUnSelectedTabFill";
130NSString        *GSTabViewLeftBackgroundTabFill
131  = @"GSTabViewLeftBackgroundTabFill";
132NSString        *GSTabViewRightSelectedTabFill
133  = @"GSTabViewRightSelectedTabFill";
134NSString        *GSTabViewRightUnSelectedTabFill
135  = @"GSTabViewRightUnSelectedTabFill";
136NSString        *GSTabViewRightBackgroundTabFill
137  = @"GSTabViewRightBackgroundTabFill";
138
139
140NSString	*GSThemeDidActivateNotification
141  = @"GSThemeDidActivateNotification";
142NSString	*GSThemeDidDeactivateNotification
143  = @"GSThemeDidDeactivateNotification";
144NSString	*GSThemeWillActivateNotification
145  = @"GSThemeWillActivateNotification";
146NSString	*GSThemeWillDeactivateNotification
147  = @"GSThemeWillDeactivateNotification";
148
149NSString *
150GSThemeStringFromFillStyle(GSThemeFillStyle s)
151{
152  switch (s)
153    {
154      case GSThemeFillStyleNone: return @"None";
155      case GSThemeFillStyleScale: return @"Scale";
156      case GSThemeFillStyleRepeat: return @"Repeat";
157      case GSThemeFillStyleCenter: return @"Center";
158      case GSThemeFillStyleMatrix: return @"Matrix";
159      case GSThemeFillStyleScaleAll: return @"ScaleAll";
160    }
161  return nil;
162}
163
164GSThemeFillStyle
165GSThemeFillStyleFromString(NSString *s)
166{
167  if (s == nil || [s isEqualToString: @"None"])
168    {
169      return GSThemeFillStyleNone;
170    }
171  if ([s isEqualToString: @"Scale"])
172    {
173      return GSThemeFillStyleScale;
174    }
175  if ([s isEqualToString: @"Repeat"])
176    {
177      return GSThemeFillStyleRepeat;
178    }
179  if ([s isEqualToString: @"Center"])
180    {
181      return GSThemeFillStyleCenter;
182    }
183  if ([s isEqualToString: @"Matrix"])
184    {
185      return GSThemeFillStyleMatrix;
186    }
187  if ([s isEqualToString: @"ScaleAll"])
188    {
189      return GSThemeFillStyleScaleAll;
190    }
191  return GSThemeFillStyleNone;
192}
193
194NSString *
195GSStringFromSegmentStyle(NSSegmentStyle segmentStyle)
196{
197  switch (segmentStyle)
198    {
199      case NSSegmentStyleAutomatic:
200        return @"NSSegmentStyleAutomatic";
201      case NSSegmentStyleRounded:
202        return @"NSSegmentStyleRounded";
203      case NSSegmentStyleTexturedRounded:
204        return @"NSSegmentStyleTexturedRounded";
205      case NSSegmentStyleRoundRect:
206        return @"NSSegmentStyleRoundRect";
207      case NSSegmentStyleTexturedSquare:
208        return @"NSSegmentStyleTexturedSquare";
209      case NSSegmentStyleCapsule:
210        return @"NSSegmentStyleCapsule";
211      case NSSegmentStyleSmallSquare:
212        return @"NSSegmentStyleSmallSquare";
213      default:
214        return nil;
215    }
216}
217
218NSString *
219GSStringFromBezelStyle(NSBezelStyle bezelStyle)
220{
221  switch (bezelStyle)
222    {
223      case NSRoundedBezelStyle:
224        return @"NSRoundedBezelStyle";
225      case NSRegularSquareBezelStyle:
226        return @"NSRegularSquareBezelStyle";
227      case NSThickSquareBezelStyle:
228        return @"NSThickSquareBezelStyle";
229      case NSThickerSquareBezelStyle:
230        return @"NSThickerSquareBezelStyle";
231      case NSDisclosureBezelStyle:
232        return @"NSDisclosureBezelStyle";
233      case NSShadowlessSquareBezelStyle:
234        return @"NSShadowlessSquareBezelStyle";
235      case NSCircularBezelStyle:
236        return @"NSCircularBezelStyle";
237      case NSTexturedSquareBezelStyle:
238        return @"NSTexturedSquareBezelStyle";
239      case NSHelpButtonBezelStyle:
240        return @"NSHelpButtonBezelStyle";
241      case NSSmallSquareBezelStyle:
242        return @"NSSmallSquareBezelStyle";
243      case NSTexturedRoundedBezelStyle:
244        return @"NSTexturedRoundedBezelStyle";
245      case NSRoundRectBezelStyle:
246        return @"NSRoundRectBezelStyle";
247      case NSRecessedBezelStyle:
248        return @"NSRecessedBezelStyle";
249      case NSRoundedDisclosureBezelStyle:
250        return @"NSRoundedDisclosureBezelStyle";
251      case NSNeXTBezelStyle:
252        return @"NSNeXTBezelStyle";
253      case NSPushButtonBezelStyle:
254        return @"NSPushButtonBezelStyle";
255      case NSSmallIconButtonBezelStyle:
256        return @"NSSmallIconButtonBezelStyle";
257      case NSMediumIconButtonBezelStyle:
258        return @"NSMediumIconButtonBezelStyle";
259      case NSLargeIconButtonBezelStyle:
260        return @"NSLargeIconButtonBezelStyle";
261      default:
262        return nil;
263    }
264}
265
266NSString *
267GSStringFromBorderType(NSBorderType borderType)
268{
269  switch (borderType)
270    {
271      case NSNoBorder:                    return @"NSNoBorder";
272      case NSLineBorder:                  return @"NSLineBorder";
273      case NSBezelBorder:                 return @"NSBezelBorder";
274      case NSGrooveBorder:                return @"NSGrooveBorder";
275      default:                            return nil;
276    }
277}
278
279NSString *
280GSStringFromTabViewType(NSTabViewType type)
281{
282  switch (type)
283    {
284      case NSTopTabsBezelBorder: return @"NSTopTabsBezelBorder";
285      case NSBottomTabsBezelBorder: return @"NSBottomTabsBezelBorder";
286      case NSLeftTabsBezelBorder: return @"NSLeftTabsBezelBorder";
287      case NSRightTabsBezelBorder: return @"NSRightTabsBezelBorder";
288      case NSNoTabsBezelBorder: return @"NSNoTabsBezelBorder";
289      case NSNoTabsLineBorder: return @"NSNoTabsLineBorder";
290      case NSNoTabsNoBorder: return @"NSNoTabsNoBorder";
291      default: return nil;
292    }
293}
294
295NSString *
296GSStringFromImageFrameStyle(NSImageFrameStyle type)
297{
298  switch (type)
299    {
300      case NSImageFrameNone: return @"NSImageFrameNone";
301      case NSImageFramePhoto: return @"NSImageFramePhoto";
302      case NSImageFrameGrayBezel: return @"NSImageFrameGrayBezel";
303      case NSImageFrameGroove: return @"NSImageFrameGroove";
304      case NSImageFrameButton: return @"NSImageFrameButton";
305      default: return nil;
306    }
307}
308
309@interface	NSImage (Private)
310+ (void) _setImagePath: (NSString*)path name: (NSString*)name;
311+ (void) _reloadCachedImages;
312@end
313
314@interface	GSTheme (Private)
315- (void) _revokeOwnerships;
316@end
317
318/* This private internal class is used to store information about a method
319 * in some other class which is overridden while the current theme is
320 * active.
321 */
322@interface GSThemeMethod : NSObject
323{
324@public
325  Class		cls;
326  SEL		sel;
327  IMP		imp;	// The new method implementation
328  IMP		old;	// The original method implementation
329  Method	mth;	// The method information
330}
331@end
332
333@implementation	GSThemeMethod
334@end
335
336@implementation GSTheme
337
338static GSTheme			*defaultTheme = nil;
339static GSTheme			*theTheme = nil;
340static NSMutableDictionary	*themes = nil;
341static NSNull			*null = nil;
342static NSMapTable		*names = 0;
343
344typedef	struct {
345  NSBundle		*bundle;
346  NSColorList		*colors;
347  NSColorList		*extraColors[GSThemeSelectedState+1];
348  NSMutableSet		*imageNames;
349  NSMutableDictionary	*tiles[GSThemeSelectedState+1];
350  NSMutableSet		*owned;
351  NSImage		*icon;
352  NSString		*name;
353  Class			colorClass;
354  Class			imageClass;
355  NSMutableArray	*overrides;
356} internal;
357
358#define	_internal 		((internal*)_reserved)
359#define	_bundle			_internal->bundle
360#define	_colors			_internal->colors
361#define	_extraColors		_internal->extraColors
362#define	_imageNames		_internal->imageNames
363#define	_tiles			_internal->tiles
364#define	_owned			_internal->owned
365#define	_icon			_internal->icon
366#define	_name			_internal->name
367#define	_colorClass		_internal->colorClass
368#define	_imageClass		_internal->imageClass
369#define	_overrides		_internal->overrides
370
371+ (void) defaultsDidChange: (NSNotification*)n
372{
373  NSUserDefaults	*defs;
374  NSString		*name;
375
376  defs = [NSUserDefaults standardUserDefaults];
377  name = [defs stringForKey: @"GSTheme"];
378  if (0 == [name length])
379    {
380      name = @"GNUstep";
381    }
382  else if ([[name pathExtension] isEqual: @"theme"])
383    {
384      name = [name stringByDeletingPathExtension];
385    }
386  if (NO == [[name lastPathComponent] isEqual: [theTheme name]])
387    {
388      [self setTheme: [self loadThemeNamed: name]];
389    }
390}
391
392+ (void) initialize
393{
394  if (themes == nil)
395    {
396      themes = [NSMutableDictionary new];
397      null = RETAIN([NSNull null]);
398      defaultTheme = [[self alloc] initWithBundle: nil];
399      ASSIGN(theTheme, defaultTheme);
400      names = NSCreateMapTable(NSNonOwnedPointerMapKeyCallBacks,
401	NSIntMapValueCallBacks, 0);
402      /* Establish the theme specified by the user defaults (if any);
403       */
404      [self defaultsDidChange: nil];
405    }
406}
407
408+ (GSTheme*) loadThemeNamed: (NSString*)aName
409{
410  NSBundle	*bundle;
411  Class		cls;
412  GSTheme	*instance;
413  NSString	*theme;
414
415  if ([[aName pathExtension] isEqual: @"theme"])
416    {
417      aName = [aName stringByDeletingPathExtension];
418    }
419  if ([aName length] == 0 || [[aName lastPathComponent] isEqual: @"GNUstep"])
420    {
421      return defaultTheme;
422    }
423
424  if ([aName isAbsolutePath] == YES)
425    {
426      theme = aName;
427    }
428  else
429    {
430      aName = [aName lastPathComponent];
431    }
432
433  /* Ensure that the theme name has the 'theme' extension.
434   */
435  if ([[aName pathExtension] isEqualToString: @"theme"] == YES)
436    {
437      theme = aName;
438    }
439  else
440    {
441      theme = [aName stringByAppendingPathExtension: @"theme"];
442    }
443
444  bundle = [themes objectForKey: theme];
445  if (bundle == nil)
446    {
447      NSString		*path = nil;
448      NSFileManager	*mgr = [NSFileManager defaultManager];
449      BOOL 		isDir;
450
451      /* A theme may be either an absolute path or a filename to be located
452       * in the Themes subdirectory of one of the standard Library directories.
453       */
454      if ([theme isAbsolutePath] == YES)
455        {
456	  if ([mgr fileExistsAtPath: theme isDirectory: &isDir] == YES
457	    && isDir == YES)
458	    {
459	      path = theme;
460	    }
461	}
462      else
463        {
464	  NSEnumerator	*enumerator;
465
466	  enumerator = [NSSearchPathForDirectoriesInDomains
467	    (NSAllLibrariesDirectory, NSAllDomainsMask, YES) objectEnumerator];
468	  while ((path = [enumerator nextObject]) != nil)
469	    {
470	      path = [path stringByAppendingPathComponent: @"Themes"];
471	      path = [path stringByAppendingPathComponent: theme];
472	      if ([mgr fileExistsAtPath: path isDirectory: &isDir])
473		{
474		  break;
475		}
476	    }
477	}
478
479      if (path == nil)
480	{
481	  NSLog (@"No theme named '%@' found", aName);
482	  return nil;
483	}
484      else
485        {
486	  bundle = [NSBundle bundleWithPath: path];
487	  [themes setObject: bundle forKey: theme];
488	  [bundle load];	// Ensure code is loaded.
489	}
490    }
491
492  cls = [bundle principalClass];
493  if (cls == 0)
494    {
495      cls = self;
496    }
497  instance = [[cls alloc] initWithBundle: bundle];
498  return AUTORELEASE(instance);
499}
500
501+ (void) orderFrontSharedThemePanel: (id)sender
502{
503  GSThemePanel *panel;
504
505  panel = [GSThemePanel sharedThemePanel];
506  [panel update: self];
507  [panel center];
508  [panel orderFront: self];
509}
510
511+ (void) setTheme: (GSTheme*)theme
512{
513  if (theme == nil)
514    {
515      theme = defaultTheme;
516    }
517  if (theme != theTheme)
518    {
519      /*
520       * Remove any previous observers...
521       */
522      [[NSNotificationCenter defaultCenter]
523	removeObserver: self];
524
525      [theTheme deactivate];
526      ASSIGN (theTheme, theme);
527      [theTheme activate];
528
529      /*
530       * Listen to notifications...
531       */
532      [[NSNotificationCenter defaultCenter]
533	addObserver: self
534	   selector: @selector(defaultsDidChange:)
535	       name: NSUserDefaultsDidChangeNotification
536	     object: nil];
537    }
538}
539
540+ (GSTheme*) theme
541{
542  return theTheme;
543}
544
545- (void) activate
546{
547  NSUserDefaults	*defs;
548  NSMutableArray	*searchList;
549  NSEnumerator		*enumerator;
550  NSDictionary		*infoDict;
551  NSWindow		*window;
552  GSThemeControlState	state;
553
554  NSDebugMLLog(@"GSTheme", @"%@ %p", [self name], self);
555  /* Get rid of any cached colors list so that we regenerate it when needed
556   */
557  [_colors release];
558  _colors = nil;
559  for (state = 0; state <= GSThemeSelectedState; state++)
560    {
561      [_extraColors[state] release];
562      _extraColors[state] = nil;
563    }
564
565  /*
566   * Reload NSImage's cache of image by name
567   */
568  [NSImage _reloadCachedImages];
569
570  /*
571   * Use the GSThemeDomain key in the info dictionary of the theme to
572   * set a defaults domain which will establish user defaults values
573   * but will not override any defaults set explicitly by the user.
574   * NB. For subclasses, the theme info dictionary may not be the same
575   * as that of the bundle, so we don't use the bundle method directly.
576   */
577  infoDict = [self infoDictionary];
578  defs = [NSUserDefaults standardUserDefaults];
579  searchList = [[defs searchList] mutableCopy];
580  if ([[infoDict objectForKey: @"GSThemeDomain"] isKindOfClass:
581    [NSDictionary class]] == YES)
582    {
583      [defs removeVolatileDomainForName: @"GSThemeDomain"];
584      [defs setVolatileDomain: [infoDict objectForKey: @"GSThemeDomain"]
585		      forName: @"GSThemeDomain"];
586      if ([searchList containsObject: @"GSThemeDomain"] == NO)
587	{
588	  NSUInteger index;
589
590	  /*
591	   * Higher priority than GSConfigDomain and NSRegistrationDomain,
592	   * but lower than NSGlobalDomain, NSArgumentDomain, and others
593	   * set by the user to be application specific.
594	   */
595	  index = [searchList indexOfObject: GSConfigDomain];
596	  if (index == NSNotFound)
597	    {
598	      index = [searchList indexOfObject: NSRegistrationDomain];
599	      if (index == NSNotFound)
600	        {
601		  index = [searchList count];
602		}
603	    }
604	  [searchList insertObject: @"GSThemeDomain" atIndex: index];
605	  [defs setSearchList: searchList];
606	}
607    }
608  else
609    {
610      [searchList removeObject: @"GSThemeDomain"];
611      [defs removeVolatileDomainForName: @"GSThemeDomain"];
612    }
613  RELEASE(searchList);
614
615  /* Install any overridden methods.
616   */
617  if (_overrides != nil)
618    {
619      NSEnumerator	*e = [_overrides objectEnumerator];
620      GSThemeMethod	*m;
621
622      while ((m = [e nextObject]) != nil)
623	{
624	  method_setImplementation(m->mth, m->imp);
625	}
626    }
627
628  /*
629   * Tell subclass that basic activation is done and it can do its own.
630   */
631  [[NSNotificationCenter defaultCenter]
632    postNotificationName: GSThemeWillActivateNotification
633    object: self
634    userInfo: nil];
635
636  /*
637   * Tell all other classes that new theme information is present.
638   */
639  [[NSNotificationCenter defaultCenter]
640    postNotificationName: GSThemeDidActivateNotification
641    object: self
642    userInfo: nil];
643
644  /*
645   * Reset main menu to change between styles if necessary
646   */
647  [[NSApp mainMenu] setMain: YES];
648
649  /*
650   * Mark all windows as needing redisplaying to show the new theme.
651   */
652  enumerator = [[NSApp windows] objectEnumerator];
653  while ((window = [enumerator nextObject]) != nil)
654    {
655      [[[window contentView] superview] setNeedsDisplay: YES];
656    }
657}
658
659- (NSArray*) authors
660{
661  return [[self infoDictionary] objectForKey: @"GSThemeAuthors"];
662}
663
664- (NSBundle*) bundle
665{
666  return _bundle;
667}
668
669- (Class) colorClass
670{
671  return [NSColorList class];
672}
673
674- (void) colorFlush: (NSString*)aName
675	      state: (GSThemeControlState)elementState
676{
677  int	pos;
678  int	end;
679
680  if (elementState > GSThemeSelectedState)
681    {
682      pos = 0;
683      end = GSThemeSelectedState;
684    }
685  else
686    {
687      pos = elementState;
688      end = elementState;
689    }
690  while (pos <= end)
691    {
692      if (_extraColors[pos] != nil)
693	{
694	  [_extraColors[pos] release];
695	  _extraColors[pos] = nil;
696	}
697      pos++;
698    }
699}
700
701- (NSColor*) colorNamed: (NSString*)aName
702		  state: (GSThemeControlState)elementState
703{
704  NSColor	*c = nil;
705
706  NSAssert(elementState <= GSThemeSelectedState, NSInvalidArgumentException);
707  NSAssert(elementState >= 0, NSInvalidArgumentException);
708
709  if (aName != nil)
710    {
711      if (_extraColors[elementState] == nil)
712	{
713	  NSString	*colorsPath;
714	  NSString	*listName;
715	  NSString	*resourceName;
716
717	  /* Attempt to load color list ... if the list is not found
718	   * or the load fails, set a null marker.
719	   */
720	  switch (elementState)
721	    {
722              default:
723	      case GSThemeNormalState:
724		listName = @"ThemeExtra";
725		break;
726	      case GSThemeHighlightedState:
727		listName = @"ThemeExtraHighlighted";
728		break;
729	      case GSThemeSelectedState:
730		listName = @"ThemeExtraSelected";
731		break;
732	    }
733	  resourceName = [listName stringByAppendingString: @"Colors"];
734	  colorsPath = [_bundle pathForResource: resourceName
735					 ofType: @"clr"];
736	  if (colorsPath != nil)
737	    {
738	      _extraColors[elementState]
739		= [[_colorClass alloc] initWithName: listName
740					   fromFile: colorsPath];
741	      /* If the list is actually empty, we get rid of it to avoid
742	       * unnecessary lookups.
743	       */
744	      if ([[_extraColors[elementState] allKeys] count] == 0)
745		{
746		  [_extraColors[elementState] release];
747		  _extraColors[elementState] = nil;
748		}
749	    }
750	  if (_extraColors[elementState] == nil)
751	    {
752	      _extraColors[elementState] = (id)[null retain];
753	    }
754	}
755      if (_extraColors[elementState] != (id)null)
756	{
757          c = [_extraColors[elementState] colorWithKey: aName];
758	}
759    }
760  return c;
761}
762
763- (NSColorList*) colors
764{
765  if (_colors == nil)
766    {
767      NSString	*colorsPath;
768
769      colorsPath = [_bundle pathForResource: @"ThemeColors" ofType: @"clr"];
770      if (colorsPath == nil)
771	{
772	  _colors = (id)[null retain];
773	}
774      else
775	{
776	  _colors = [[_colorClass alloc] initWithName: @"System"
777					     fromFile: colorsPath];
778	}
779    }
780  if ((id)_colors == (id)null)
781    {
782      return nil;
783    }
784  return _colors;
785}
786
787- (void) deactivate
788{
789  NSDebugMLLog(@"GSTheme", @"%@ %p", [self name], self);
790
791  /* Tell everything that we will become inactive.
792   */
793  [[NSNotificationCenter defaultCenter]
794    postNotificationName: GSThemeWillDeactivateNotification
795    object: self
796    userInfo: nil];
797
798  /* Remove any overridden methods.
799   */
800  if (_overrides != nil)
801    {
802      NSEnumerator	*e = [_overrides objectEnumerator];
803      GSThemeMethod	*m;
804
805      while ((m = [e nextObject]) != nil)
806	{
807	  method_setImplementation(m->mth, m->old);
808	}
809    }
810
811  [self _revokeOwnerships];
812
813  /* Tell everything that we have become inactive.
814   */
815  [[NSNotificationCenter defaultCenter]
816    postNotificationName: GSThemeDidDeactivateNotification
817    object: self
818    userInfo: nil];
819}
820
821- (void) dealloc
822{
823  if (_reserved != 0)
824    {
825      GSThemeControlState	state;
826
827      for (state = 0; state <= GSThemeSelectedState; state++)
828	{
829          RELEASE(_extraColors[state]);
830          RELEASE(_tiles[state]);
831	}
832      RELEASE(_bundle);
833      RELEASE(_colors);
834      RELEASE(_imageNames);
835      RELEASE(_icon);
836      [self _revokeOwnerships];
837      RELEASE(_overrides);
838      RELEASE(_owned);
839      NSZoneFree ([self zone], _reserved);
840    }
841  [super dealloc];
842}
843
844- (NSImage*) icon
845{
846  if (_icon == nil)
847    {
848      NSString	*path;
849
850      path = [[self infoDictionary] objectForKey: @"GSThemeIcon"];
851      if (path != nil)
852        {
853	  NSString	*ext = [path pathExtension];
854
855	  path = [path stringByDeletingPathExtension];
856	  path = [_bundle pathForResource: path ofType: ext];
857	  if (path != nil)
858	    {
859	      _icon = [[_imageClass alloc] initWithContentsOfFile: path];
860	    }
861	}
862      if (_icon == nil)
863        {
864	  _icon = RETAIN([_imageClass imageNamed: @"GNUstep"]);
865	}
866      else
867	{
868	  NSSize	s = [_icon size];
869	  float		scale = 1.0;
870
871	  if (s.height > 48.0)
872	    scale = 48.0 / s.height;
873	  if (48.0 / s.width < scale)
874	    scale = 48.0 / s.width;
875	  if (scale != 1.0)
876	    {
877	      [_icon setScalesWhenResized: YES];
878	      s.height *= scale;
879	      s.width *= scale;
880	      [_icon setSize: s];
881	    }
882	}
883    }
884  return _icon;
885}
886
887- (Class) imageClass
888{
889  return [NSImage class];
890}
891
892- (id) initWithBundle: (NSBundle*)bundle
893{
894  Class				c = [self class];
895  unsigned int			count;
896  Method			*methods;
897  GSThemeMethod			*mth;
898  GSThemeControlState		state;
899
900  _reserved = NSZoneCalloc ([self zone], 1, sizeof(internal));
901
902  ASSIGN(_bundle, bundle);
903  _imageNames = [NSMutableSet new];
904  for (state = 0; state <= GSThemeSelectedState; state++)
905    {
906      _tiles[state] = [NSMutableDictionary new];
907    }
908  _owned = [NSMutableSet new];
909
910  ASSIGN(_name,
911    [[[_bundle bundlePath] lastPathComponent] stringByDeletingPathExtension]);
912
913  _colorClass = [self colorClass];
914  _imageClass = [self imageClass];
915
916  /* Now we look through our methods to find those which are actually
917   * replacements to override methods in other classes.
918   * That's determined by method name ... any method of the form
919   * '_override' <classname> 'Method_' <originalmethodname>
920   * is used to replace the original method in the class.
921   * We maintain dictionaries (keyed by class) for instance and class
922   * methods, so we can look up the original methods at runtime if the
923   * replacement methods want to call them.
924   */
925  methods = class_copyMethodList(c, &count);
926  if (methods != NULL)
927    {
928      int counter = 0;
929
930      while (methods[counter] != 0)
931	{
932          Method	method = methods[counter++];
933	  const char	*name = sel_getName(method_getName(method));
934	  const char	*ptr;
935
936	  if (strncmp(name, "_override", 9) == 0
937	    && (ptr = strstr(name, "Method_")) > 0)
938	    {
939	      char		buf[strlen(name)];
940	      const char	*types;
941
942	      mth = [[GSThemeMethod new] autorelease];
943	      types = method_getTypeEncoding(method);
944	      mth->imp = method_getImplementation(method);
945	      memcpy(buf, name + 9, (ptr - name) + 9);
946	      buf[(ptr - name) + 9] = '\0';
947	      mth->cls = objc_lookUpClass(buf);
948	      if (mth->cls == 0)
949		{
950		  NSLog(@"Unable to find class '%s' for '%s'", buf, name);
951		  continue;
952		}
953	      memcpy(buf, ptr + 7, strlen(ptr + 7));
954	      buf[strlen(ptr + 7)] = '\0';
955	      mth->sel = sel_getUid(buf);
956	      if (mth->sel == 0)
957		{
958		  NSLog(@"Unable to find selector '-%s' for '%s'", buf, name);
959		  continue;
960		}
961	      if (NO == [mth->cls instancesRespondToSelector: mth->sel])
962		{
963		  NSLog(@"Instances do not respond for '%s'", name);
964		  continue;
965		}
966	      mth->old = [mth->cls instanceMethodForSelector: mth->sel];
967	      class_addMethod(mth->cls, mth->sel, mth->imp, types);
968	      mth->mth = class_getInstanceMethod(mth->cls, mth->sel);
969
970	      if (_overrides == nil)
971		{
972		  _overrides = [NSMutableArray new];
973		}
974	      [_overrides addObject: mth];
975	    }
976	}
977      free(methods);
978    }
979
980  methods = class_copyMethodList(object_getClass(c), &count);
981  if (methods != NULL)
982    {
983      int counter = 0;
984
985      while (methods[counter] != 0)
986	{
987          Method	method = methods[counter++];
988	  const char	*name = sel_getName(method_getName(method));
989	  const char	*ptr;
990
991	  if (strncmp(name, "_override", 9) == 0
992	    && (ptr = strstr(name, "Method_")) > 0)
993	    {
994	      char		buf[strlen(name)];
995	      const char	*types;
996	      Class		cls;
997
998	      mth = [[GSThemeMethod new] autorelease];
999	      types = method_getTypeEncoding(method);
1000		      mth->imp = method_getImplementation(method);
1001	      memcpy(buf, name + 9, (ptr - name) + 9);
1002	      buf[(ptr - name) + 9] = '\0';
1003	      cls = objc_lookUpClass(buf);
1004	      if (cls == 0)
1005		{
1006		  NSLog(@"Unable to find class '%s' for '%s'", buf, name);
1007		  continue;
1008		}
1009	      mth->cls = object_getClass(cls);
1010	      memcpy(buf, ptr + 7, strlen(ptr + 7));
1011	      buf[strlen(ptr + 7)] = '\0';
1012	      mth->sel = sel_getUid(buf);
1013	      if (mth->sel == 0)
1014		{
1015		  NSLog(@"Unable to find selector '-%s' for '%s'", buf, name);
1016		  continue;
1017		}
1018	      if (NO == [cls respondsToSelector: mth->sel])
1019		{
1020		  NSLog(@"Class does not respond for '%s'", name);
1021		  continue;
1022		}
1023	      mth->old = [cls methodForSelector: mth->sel];
1024	      class_addMethod(mth->cls, mth->sel, mth->imp, types);
1025	      mth->mth = class_getClassMethod(cls, mth->sel);
1026
1027	      if (_overrides == nil)
1028		{
1029		  _overrides = [NSMutableArray new];
1030		}
1031	      [_overrides addObject: mth];
1032	    }
1033	}
1034      free(methods);
1035    }
1036
1037  return self;
1038}
1039
1040- (NSDictionary*) infoDictionary
1041{
1042  return [_bundle infoDictionary];
1043}
1044
1045- (NSString*) name
1046{
1047  if (self == defaultTheme)
1048    {
1049      _name = @"GNUstep";
1050    }
1051  return _name;
1052}
1053
1054- (NSString*) nameForElement: (id)anObject
1055{
1056  NSString	*name = (NSString*)NSMapGet(names, (void*)anObject);
1057
1058  return name;
1059}
1060
1061- (IMP) overriddenMethod: (SEL)selector for: (id)receiver
1062{
1063  Class		cls = object_getClass(receiver);
1064  NSEnumerator	*e = [_overrides objectEnumerator];
1065  GSThemeMethod	*m;
1066
1067  while ((m = [e nextObject]) != nil)
1068    {
1069      if (m->cls == cls && sel_isEqual(selector, m->sel))
1070	{
1071	  return m->old;
1072	}
1073    }
1074  return (IMP)0;
1075}
1076
1077- (void) setName: (NSString*)aString
1078{
1079  if (self != defaultTheme)
1080    {
1081      ASSIGNCOPY(_name, aString);
1082    }
1083}
1084
1085- (void) setName: (NSString*)aString
1086      forElement: (id)anObject
1087       temporary: (BOOL)takeOwnership
1088{
1089  if (aString == nil)
1090    {
1091      if (anObject == nil)
1092	{
1093	  /* Ignore this ... it's most likely a partially initialised
1094	   * control being deallocated and removing the name for a
1095	   * subsidiary item which was never allocated in the first place.
1096	   */
1097	  return;
1098	}
1099      NSMapRemove(names, (void*)anObject);
1100      [_owned removeObject: anObject];
1101    }
1102  else
1103    {
1104      if (anObject == nil)
1105	{
1106	  [NSException raise: NSInvalidArgumentException
1107		      format: @"[%@-%@] nil object supplied",
1108	    NSStringFromClass([self class]), NSStringFromSelector(_cmd)];
1109	}
1110      NSMapInsert(names, (void*)anObject, (void*)aString);
1111      if (takeOwnership == YES)
1112	{
1113	  [_owned addObject: anObject];
1114	}
1115      else
1116	{
1117	  [_owned removeObject: anObject];
1118	}
1119    }
1120}
1121
1122- (NSWindow*) themeInspector
1123{
1124  return [GSThemeInspector sharedThemeInspector];
1125}
1126
1127- (void) tilesFlush: (NSString*)aName
1128	      state: (GSThemeControlState)elementState
1129{
1130  int	pos;
1131  int	end;
1132
1133  if (elementState > GSThemeSelectedState)
1134    {
1135      pos = 0;
1136      end = GSThemeSelectedState;
1137    }
1138  else
1139    {
1140      pos = elementState;
1141      end = elementState;
1142    }
1143  while (pos <= end)
1144    {
1145      NSMutableDictionary	*cache;
1146
1147      cache = _tiles[pos++];
1148      if (aName == nil)
1149	{
1150	  return [cache removeAllObjects];
1151	}
1152      else
1153	{
1154	  [cache removeObjectForKey: aName];
1155	}
1156    }
1157}
1158
1159- (GSDrawTiles*) tilesNamed: (NSString*)aName
1160		      state: (GSThemeControlState)elementState
1161{
1162  GSDrawTiles		*tiles;
1163  NSMutableDictionary	*cache;
1164
1165  NSAssert(elementState <= GSThemeSelectedState, NSInvalidArgumentException);
1166  NSAssert(elementState >= 0, NSInvalidArgumentException);
1167  if (aName == nil)
1168    {
1169      return nil;
1170    }
1171  cache = _tiles[elementState];
1172  tiles = [cache objectForKey: aName];
1173  if (tiles == nil)
1174    {
1175      NSDictionary	*info;
1176      NSImage		*image;
1177      NSString		*fullName;
1178
1179      switch (elementState)
1180	{
1181          default:
1182	  case GSThemeNormalState:
1183	    fullName = aName;
1184	    break;
1185	  case GSThemeFirstResponderState:
1186	    fullName = [aName stringByAppendingString: @"FirstResponder"];
1187	    break;
1188	  case GSThemeDisabledState:
1189	    fullName = [aName stringByAppendingString: @"Disabled"];
1190	    break;
1191	  case GSThemeHighlightedFirstResponderState:
1192	    fullName
1193              = [aName stringByAppendingString: @"HighlightedFirstResponder"];
1194	    break;
1195	  case GSThemeHighlightedState:
1196	    fullName = [aName stringByAppendingString: @"Highlighted"];
1197	    break;
1198	  case GSThemeSelectedFirstResponderState:
1199	    fullName
1200              = [aName stringByAppendingString: @"SelectedFirstResponder"];
1201	    break;
1202	  case GSThemeSelectedState:
1203	    fullName = [aName stringByAppendingString: @"Selected"];
1204	    break;
1205	}
1206
1207      /* The GSThemeTiles entry in the info dictionary should be a
1208       * dictionary containing information about each set of tiles.
1209       * Keys are:
1210       * FileName		Name of the file in the ThemeTiles directory
1211       * HorizontalDivision	Where to divide the image into columns.
1212       * VerticalDivision	Where to divide the image into rows.
1213       */
1214      info = [self infoDictionary];
1215      info = [[info objectForKey: @"GSThemeTiles"] objectForKey: fullName];
1216      if ([info isKindOfClass: [NSDictionary class]] == YES)
1217        {
1218	  float			x;
1219	  float			y;
1220	  NSString		*name;
1221	  NSString		*path;
1222	  NSString		*file;
1223	  NSString		*ext;
1224	  GSThemeFillStyle	style;
1225
1226	  name = [info objectForKey: @"FillStyle"];
1227	  style = GSThemeFillStyleFromString(name);
1228	  if (style < GSThemeFillStyleNone) style = GSThemeFillStyleNone;
1229	  x = [[info objectForKey: @"HorizontalDivision"] floatValue];
1230	  y = [[info objectForKey: @"VerticalDivision"] floatValue];
1231	  file = [info objectForKey: @"FileName"];
1232	  ext = [file pathExtension];
1233	  file = [file stringByDeletingPathExtension];
1234	  path = [_bundle pathForResource: file
1235				   ofType: ext
1236			      inDirectory: @"ThemeTiles"];
1237	  if (path == nil)
1238	    {
1239	      NSLog(@"File %@.%@ not found in ThemeTiles", file, ext);
1240	    }
1241	  else
1242	    {
1243	      image = [[_imageClass alloc] initWithContentsOfFile: path];
1244	      if (image != nil)
1245		{
1246                  if ([[info objectForKey: @"NinePatch"] boolValue]
1247		      || [file hasSuffix: @".9"])
1248                    {
1249                      tiles = [[GSDrawTiles alloc]
1250			initWithNinePatchImage: image];
1251		      [tiles setFillStyle: GSThemeFillStyleScaleAll];
1252                    }
1253                  else
1254                    {
1255		      tiles = [[GSDrawTiles alloc] initWithImage: image
1256                                                      horizontal: x
1257                                                        vertical: y];
1258		      [tiles setFillStyle: style];
1259                    }
1260		  RELEASE(image);
1261		}
1262	    }
1263	}
1264
1265      if (tiles == nil)
1266        {
1267	  NSString	*imagePath;
1268
1269	  // Try 9-patch first
1270	  imagePath = [_bundle pathForResource: fullName
1271					ofType: @"9.png"
1272				   inDirectory: @"ThemeTiles"];
1273	  if (imagePath != nil)
1274	    {
1275	      image
1276		= [[_imageClass alloc] initWithContentsOfFile: imagePath];
1277	      if (image != nil)
1278		{
1279		  tiles = [[GSDrawTiles alloc]
1280			    initWithNinePatchImage: image];
1281		  [tiles setFillStyle: GSThemeFillStyleScaleAll];
1282		  RELEASE(image);
1283		}
1284	    }
1285	}
1286
1287      if (tiles == nil)
1288        {
1289	  NSArray	*imageTypes;
1290	  NSString	*imagePath;
1291	  unsigned	count;
1292
1293	  imageTypes = [_imageClass imageFileTypes];
1294	  for (count = 0; count < [imageTypes count]; count++)
1295	    {
1296	      NSString	*ext = [imageTypes objectAtIndex: count];
1297
1298	      imagePath = [_bundle pathForResource: fullName
1299					    ofType: ext
1300				       inDirectory: @"ThemeTiles"];
1301	      if (imagePath != nil)
1302		{
1303		  image
1304		    = [[_imageClass alloc] initWithContentsOfFile: imagePath];
1305		  if (image != nil)
1306		    {
1307		      tiles = [[GSDrawTiles alloc] initWithImage: image];
1308		      RELEASE(image);
1309		      break;
1310		    }
1311		}
1312	    }
1313	}
1314
1315      if (tiles == nil)
1316        {
1317	  [cache setObject: null forKey: aName];
1318	}
1319      else
1320        {
1321	  [cache setObject: tiles forKey: aName];
1322	  RELEASE(tiles);
1323	}
1324    }
1325  if (tiles == (id)null)
1326    {
1327      tiles = nil;
1328    }
1329  return tiles;
1330}
1331
1332- (NSString*) versionString
1333{
1334  return [[self infoDictionary] objectForKey: @"GSThemeVersion"];
1335}
1336
1337- (NSString *) license
1338{
1339  return [[self infoDictionary] objectForKey: @"GSThemeLicense"];
1340}
1341
1342@end
1343
1344@implementation	GSTheme (Private)
1345/* Remove all temporarily named objects from our registry, releasing them.
1346 */
1347- (void) _revokeOwnerships
1348{
1349  id	o;
1350
1351  while ((o = [_owned anyObject]) != nil)
1352    {
1353      [self setName: nil forElement: o temporary: YES];
1354    }
1355}
1356@end
1357
1358@implementation	GSThemeProxy
1359- (id) _resource
1360{
1361  return _resource;
1362}
1363- (void) _setResource: (id)resource
1364{
1365  ASSIGN(_resource, resource);
1366}
1367- (void) dealloc
1368{
1369  DESTROY(_resource);
1370  [super dealloc];
1371}
1372- (NSString*) description
1373{
1374  return [_resource description];
1375}
1376- (id) forwardingTargetForSelector:(SEL)aSelector
1377{
1378  return _resource;
1379}
1380- (void) forwardInvocation: (NSInvocation*)anInvocation
1381{
1382  [anInvocation invokeWithTarget: _resource];
1383}
1384- (NSMethodSignature*) methodSignatureForSelector: (SEL)aSelector
1385{
1386  if (_resource != nil)
1387    {
1388      return [_resource methodSignatureForSelector: aSelector];
1389    }
1390  else
1391    {
1392      /*
1393       * Evil hack to prevent recursion - if we are asking a remote
1394       * object for a method signature, we can't ask it for the
1395       * signature of methodSignatureForSelector:, so we hack in
1396       * the signature required manually :-(
1397       */
1398      if (sel_isEqual(aSelector, _cmd))
1399	{
1400	  static	NSMethodSignature	*sig = nil;
1401
1402	  if (sig == nil)
1403	    {
1404	      sig = RETAIN([NSMethodSignature signatureWithObjCTypes: "@@::"]);
1405	    }
1406	  return sig;
1407	}
1408      return nil;
1409    }
1410}
1411@end
1412
1413