/*
    GNUstepAppLauncher.m

    Implementation of the GNUstepAppLauncher project module for the
    ProjectManager application.

    Copyright (C) 2005, 2006  Saso Kiselkov

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
*/

#import "GNUstepAppLauncher.h"

#import <Foundation/NSBundle.h>
#import <Foundation/NSException.h>
#import <Foundation/NSFileHandle.h>
#import <Foundation/NSNotification.h>
#import <Foundation/NSProcessInfo.h>
#import <Foundation/NSString.h>
#import <Foundation/NSTask.h>

#import <AppKit/NSColor.h>
#import <AppKit/NSFont.h>
#import <AppKit/NSFormCell.h>
#import <AppKit/NSForm.h>
#import <AppKit/NSImage.h>
#import <AppKit/NSMenuItem.h>
#import <AppKit/NSNibLoading.h>
#import <AppKit/NSOpenPanel.h>
#import <AppKit/NSPopUpButton.h>
#import <AppKit/NSTableColumn.h>
#import <AppKit/NSTableView.h>
#import <AppKit/NSTextStorage.h>
#import <AppKit/NSTextView.h>
#import <AppKit/NSWindow+Toolbar.h>
#import <AppKit/NSToolbar.h>
#import <AppKit/NSToolbarItem.h>

#import "GNUstepAppLauncherDelegate.h"

#import "../../NSImageAdditions.h"
#import "../../ProjectDocument.h"

/**
 * Notification sent when the launcher's project will be launched.
 * The userInfo dictionary contains these keys:
 * {
 *   Project = (ProjectDocument *) originatingProject;
 *   Target = "<launch-target>";
 * }
 */
NSString * const GNUstepAppLauncherProjectWillLaunchNotification =
  @"GNUstepAppLauncherProjectWillLaunchNotification";

/**
 * Notification sent when the launcher's project has been successfuly
 * launched. The userInfo dictionary contains these keys:
 * {
 *   Project = (ProjectDocument *) originatingProject;
 *   Target = "<launch-target>";
 *   Task = (NSTask *) launchedProgramTask;
 * }
 */
NSString * const GNUstepAppLauncherProjectDidLaunchNotification =
  @"GNUstepAppLauncherProjectDidLaunchNotification";

/**
 * Notification sent when the launcher's project has failed to launch
 * or launching has been stopped by the user. The userInfo dictionary
 * contains these keys:
 * {
 *   Project = (ProjectDocument *) originatingProject;
 *   Target = "<launch-target>";
 * }
 */
NSString * const GNUstepAppLauncherProjectDidFailToLaunchNotification =
  @"GNUstepAppLauncherProjectDidFailToLaunchNotification";

/**
 * Notification sent when the launcher's project has terminated.
 * The userInfo dictionary contains these keys:
 * {
 *   Project = (ProjectDocument *) originatingProject;
 *   Target = "<launch-target>";
 *   Task = (NSTask *) terminatedProgramTask;
 * }
 */
NSString * const GNUstepAppLauncherProjectDidTerminateNotification =
  @"GNUstepAppLauncherProjectDidTerminateNotification";

@interface GNUstepAppLauncher (Private)

- (void) clearState;

- (void) setLauncherState: (GNUstepAppLauncherState) state;

- (BOOL) validateControl: (id) control;

@end

@implementation GNUstepAppLauncher (Private)

/**
 * This method clears internal ivars which refer to the running
 * process, such as stdin, stdout and stderr pipes, and deregister
 * us at the notification center for NSTask-related notifications.
 */
- (void) clearState
{
  NSNotificationCenter * nc = [NSNotificationCenter defaultCenter];

  [nc removeObserver: self
                name: NSTaskDidTerminateNotification
              object: task];
  [nc removeObserver: self
                name: NSFileHandleDataAvailableNotification
              object: stdoutHandle];
  [nc removeObserver: self
                name: NSFileHandleDataAvailableNotification
              object: stderrHandle];

  DESTROY(task);
  DESTROY(stdinHandle);
  DESTROY(stdoutHandle);
  DESTROY(stderrHandle);
}

/**
 * Puts the receiver into the a provided launcher state and sets up it's
 * interface to represent the state correctly.
 *
 * @param setup The state to which to set up the receiver.
 */
- (void) setLauncherState: (GNUstepAppLauncherState) state
{
  launcherState = state;

  switch (launcherState)
    {
    case GNUstepAppLauncherReadyState:
      [workingDirectory setEditable: YES];
      [workingDirectoryButton setEnabled: YES];
      [targets setEnabled: YES];
      break;

    case GNUstepAppLauncherDelayedLaunchState:
      [workingDirectory setEditable: NO];
      [workingDirectoryButton setEnabled: NO];
      [targets setEnabled: NO];
      break;

    case GNUstepAppLauncherLaunchedState:
      [workingDirectory setEditable: NO];
      [workingDirectoryButton setEnabled: NO];
      [targets setEnabled: NO];
      break;
    }

  [[[view window] toolbar] validateVisibleItems];
}

/**
 * Validates a control based on it's action. This method is used by
 * both -validateMenuItem: and -validateToolbarItem:.
 *
 * @param control An object which responds to the -action message.
 *
 * @return YES if the control is valid, NO otherwise.
 */
- (BOOL) validateControl: (id) control
{
  SEL action = [control action];

  if (sel_eq(action, @selector(launch:)))
    {
      if (launcherState == GNUstepAppLauncherReadyState)
        {
          return YES;
        }
      else
        {
          return NO;
        }
    }
  else if (sel_eq(action, @selector(stopLaunch:)))
    {
      if (launcherState == GNUstepAppLauncherDelayedLaunchState)
        {
          return YES;
        }
      else
        {
          return NO;
        }
    }
  else if (sel_eq(action, @selector(kill:)))
    {
      if (launcherState == GNUstepAppLauncherLaunchedState)
        {
          return YES;
        }
      else
        {
          return NO;
        }
    }
  else
    {
      // allow other controls only when we're currently visible
      return [document currentProjectModule] == self;
    }
}

@end

@implementation GNUstepAppLauncher

+ (NSString *) moduleName
{
  return @"GNUstepAppLauncher";
}

+ (NSString *) humanReadableModuleName
{
  return _(@"Launcher");
}

- (NSToolbarItem *) toolbarItemForItemIdentifier: (NSString *) identifier
{
  NSToolbarItem * item = [[[NSToolbarItem alloc]
    initWithItemIdentifier: identifier]
    autorelease];

  [item setTarget: self];

  if ([identifier isEqualToString: @"GNUstepAppLauncherLaunchToolbarItem"])
    {
      [item setAction: @selector(launch:)];
      [item setImage: [NSImage imageNamed: @"Launch" owner: self]];
      [item setLabel: _(@"Launch")];
      [item setToolTip: _(@"Launch the project")];
    }
  else if ([identifier isEqualToString: @"GNUstepAppLauncherStopToolbarItem"])
    {
      [item setAction: @selector(stopLaunch:)];
      [item setImage: [NSImage imageNamed: @"Stop" owner: self]];
      [item setLabel: _(@"Stop")];
      [item setToolTip: _(@"Stop a launch in progress")];
    }
  else if ([identifier isEqualToString: @"GNUstepAppLauncherKillToolbarItem"])
    {
      [item setAction: @selector(kill:)];
      [item setImage: [NSImage imageNamed: @"Kill" owner: self]];
      [item setLabel: _(@"Kill")];
      [item setToolTip: _(@"Kill the running project")];
    }
  else if ([identifier isEqualToString: @"GNUstepAppLauncherShowArgumentsToolbarItem"])
    {
      [item setAction: @selector(showArguments:)];
      [item setImage: [NSImage imageNamed: @"Arguments" owner: self]];
      [item setLabel: _(@"Arguments")];
      [item setToolTip: _(@"Show the project's launch arguments")];
    }
  else if ([identifier isEqualToString: @"GNUstepAppLauncherShowEnvironmentToolbarItem"])
    {
      [item setAction: @selector(showEnvironment:)];
      [item setImage: [NSImage imageNamed: @"Environment" owner: self]];
      [item setLabel: _(@"Environment")];
      [item setToolTip: _(@"Show the project's environment variables")];
    }

  return item;
}

- (void) dealloc
{
  NSNotificationCenter * nc = [NSNotificationCenter defaultCenter];

  [nc removeObserver: self];

  TEST_RELEASE(view);

  if (task != nil && [task isRunning])
    {
      NSDictionary * userInfo;

      [task terminate];

      userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
        document, @"Project",
        target, @"Target",
        task, @"Task",
        nil];
      [nc postNotificationName: GNUstepAppLauncherProjectDidTerminateNotification
                        object: self
                      userInfo: userInfo];
    }

  [self clearState];

  TEST_RELEASE(arguments);
  TEST_RELEASE(environment);
  TEST_RELEASE(sortedEnvironmentNames);
  TEST_RELEASE(target);

  [super dealloc];
}

- initWithDocument: (ProjectDocument *) aDocument
    infoDictionary: (NSDictionary *) infoDict
{
  if ([self init])
    {
      document = aDocument;

      environment = [[[NSProcessInfo processInfo] environment] mutableCopy];
      ASSIGN(sortedEnvironmentNames, [[environment allKeys]
        sortedArrayUsingSelector: @selector(caseInsensitiveCompare:)]);
      arguments = [NSMutableArray new];

      // watch for current module changes in order to handle Arguments
      // and Environment panel closing and opening
      [[NSNotificationCenter defaultCenter]
        addObserver: self
           selector: @selector(moduleChanged:)
               name: CurrentProjectModuleDidChangeNotification
             object: nil];

      return self;
    }
  else
    {
      return nil;
    }
}

- (void) finishInit
{
  delegate = (id <GNUstepAppLauncherDelegate>) [document projectType];
}

- (NSView *) view
{
  if (view == nil)
    {
      [NSBundle loadNibNamed: @"GNUstepAppLauncher" owner: self];
    }

  return view;
}

- (NSDictionary *) infoDictionary
{
  return nil;
}

- (BOOL) regenerateDerivedFiles
{
  return YES;
}

- (ProjectDocument *) document
{
  return document;
}

- (void) awakeFromNib
{
  [view retain];
  [view removeFromSuperview];
  DESTROY(bogusWindow);

  [stdout setFont: [NSFont userFixedPitchFontOfSize: 0]];
  [stderr setFont: [NSFont userFixedPitchFontOfSize: 0]];
  [stderr setTextColor: [NSColor whiteColor]];

  [targets removeAllItems];
  [targets addItemsWithTitles: [delegate launchTargetsForAppLauncher: self]];
  if ([targets numberOfItems] > 0)
    {
      [targets selectItemAtIndex: 0];
    }
}

/**
 * Appends a message to the stdout log of the receiver. After the message
 * is appended the log is scrolled to it's end.
 *
 * @param aMessage The message to append.
 */
- (void) appendStdoutMessage: (NSString *) aMessage
{
  NSRange r;

  r = NSMakeRange([[stdout textStorage] length], 0);
  [stdout replaceCharactersInRange: r withString: aMessage];
  r = NSMakeRange([[stdout textStorage] length], 0);
  [stdout scrollRangeToVisible: r];
}

/**
 * Appends a message to the stderr log of the receiver. After the message
 * is appended the log is scrolled to it's end.
 *
 * @param aMessage The message to append.
 */
- (void) appendStderrMessage: (NSString *) aMessage
{
  NSRange r;

  r = NSMakeRange([[stderr textStorage] length], 0);
  [stderr replaceCharactersInRange: r withString: aMessage];
  r = NSMakeRange([[stderr textStorage] length], 0);
  [stderr scrollRangeToVisible: r];
}

/**
 * Action invoked by the `Launch' button when the app is to be killed.
 */
- (void) kill: (id) sender
{
  NSDictionary * userInfo;

  NSAssert(launcherState == GNUstepAppLauncherLaunchedState,
    _(@"Tried to kill task when not running."));

  userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
    document, @"Project",
    target, @"Target",
    task, @"Task",
    nil];

  [task terminate];
  [self clearState];
  [self setLauncherState: GNUstepAppLauncherReadyState];

  [[NSNotificationCenter defaultCenter]
    postNotificationName: GNUstepAppLauncherProjectDidTerminateNotification
                  object: self
                userInfo: userInfo];
}

/**
 * Action invoked by the `Launch' button when delayed launching of the
 * app is to be stopped.
 */
- (void) stopLaunch: (id) sender
{
  NSDictionary * userInfo;

  NSAssert(launcherState == GNUstepAppLauncherDelayedLaunchState,
    _(@"Tried to stop delayed launch when not in the according state."));

  [delegate stopDelayedLaunchForAppLauncher: self];
  [self setLauncherState: GNUstepAppLauncherReadyState];
  [document logMessage: _(@"Stopping launch")];

  userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
    document, @"Project",
    target, @"Target",
    nil];
  [[NSNotificationCenter defaultCenter]
    postNotificationName: GNUstepAppLauncherProjectDidFailToLaunchNotification
                  object: self
                userInfo: userInfo];
}

/**
 * Action invoked by the `Launch' button when the app is to be launched.
 * This method asks the delegate whether the receiver should delay
 * launching, and if the delegate answers NO, it immediatelly invokes
 * -[self proceedWithLaunch: YES], otherwise the method sets up the
 * user interface to allow stopping the delayed launch.
 */
- (void) launch: (id) sender
{
  NSDictionary * userInfo;

  NSAssert(launcherState == GNUstepAppLauncherReadyState,
    _(@"Tried to launch app when not ready."));

  // clear the output logs
  [stdout setString: @""];
  [stderr setString: @""];

  ASSIGN(target, [targets titleOfSelectedItem]);

  [document logMessage: [NSString stringWithFormat: _(@"Launching target %@"),
    target]];

  userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
    document, @"Project",
    target, @"Target",
    nil];
  [[NSNotificationCenter defaultCenter]
    postNotificationName: GNUstepAppLauncherProjectWillLaunchNotification
                  object: self
                userInfo: userInfo];

  if ([delegate     appLauncher: self
    shouldDelayLaunchWithTarget: target] == NO)
    {
      [self proceedWithLaunch: YES];
    }
  else
    {
      [self setLauncherState: GNUstepAppLauncherDelayedLaunchState];
    }
}

/**
 * Proceeds with launching if the sender sets so using the `flag' argument.
 * The launch is separated into two stages - preparation (from the
 * -launch: method) and actual launching (performed by this method). This
 * allows the delegate of the receiver to perform any additional processing
 * before the actual launch (such as building the project if necessary).
 * In case the delegate delayed the launch, it must afterwards, at some
 * point, invoke this method to finalize the launch process.
 *
 * @param flag A flag which tells the receiver whether to proceed with
 *      the launch or to abort. Passing YES contines the launch, NO
 *      aborts it.
 */
- (void) proceedWithLaunch: (BOOL) flag
{
  NSNotificationCenter * nc = [NSNotificationCenter defaultCenter];

  if (flag == YES)
    {
      NSPipe * stdinPipe, * stdoutPipe, * stderrPipe;
      NSDictionary * userInfo;

      task = [NSTask new];

      [task setLaunchPath: [delegate appLauncher: self
                       pathToProjectBinaryOfType: target]];

      [task setArguments: arguments];
      [task setEnvironment: environment];

      if ([[workingDirectory stringValue] length] > 0)
        {
          [task setCurrentDirectoryPath: [workingDirectory stringValue]];
        }

      [nc addObserver: self
             selector: @selector(taskTerminated)
                 name: NSTaskDidTerminateNotification
               object: task];

      stdinPipe = [NSPipe pipe];
      stdoutPipe = [NSPipe pipe];
      stderrPipe = [NSPipe pipe];

      [task setStandardInput: stdinPipe];
      [task setStandardOutput: stdoutPipe];
      [task setStandardError: stderrPipe];

      ASSIGN(stdinHandle, [stdinPipe fileHandleForWriting]);
      ASSIGN(stdoutHandle, [stdoutPipe fileHandleForReading]);
      ASSIGN(stderrHandle, [stderrPipe fileHandleForReading]);

      [nc addObserver: self
             selector: @selector(readStdout)
                 name: NSFileHandleDataAvailableNotification
               object: stdoutHandle];
      [nc addObserver: self
             selector: @selector(readStderr)
                 name: NSFileHandleDataAvailableNotification
               object: stderrHandle];

      NS_DURING
        [task launch];
      NS_HANDLER
        NSRunAlertPanel(_(@"Failed to launch"),
          _(@"Failed to launch the project.\n"
            @"%@"), nil, nil, nil, [localException reason]);

        [self clearState];
        [self setLauncherState: GNUstepAppLauncherReadyState];

        userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
          document, @"Project",
          target, @"Target",
          nil];
        [nc postNotificationName: GNUstepAppLauncherProjectDidFailToLaunchNotification
                          object: self
                        userInfo: userInfo];

        return;
      NS_ENDHANDLER

      [stdoutHandle waitForDataInBackgroundAndNotify];
      [stderrHandle waitForDataInBackgroundAndNotify];

      [self setLauncherState: GNUstepAppLauncherLaunchedState];

      userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
        document, @"Project",
        target, @"Target",
        task, @"Task",
        nil];
      [nc postNotificationName: GNUstepAppLauncherProjectDidLaunchNotification
                        object: self
                      userInfo: userInfo];
    }
  else
    {
      NSDictionary * userInfo;

      [self setLauncherState: GNUstepAppLauncherReadyState];

      userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
        document, @"Project",
        target, @"Target",
        nil];
      [nc postNotificationName: GNUstepAppLauncherProjectDidFailToLaunchNotification
                        object: self
                      userInfo: userInfo];
    }
}

/**
 * Action invoked by the `Choose...' button in the `Working Directory' box.
 */
- (void) chooseWorkingDirectory: (id)sender
{
  NSOpenPanel * op = [NSOpenPanel openPanel];

  [op setCanChooseDirectories: YES];
  [op setCanChooseFiles: NO];

  if ([op runModalForTypes: nil] == NSOKButton)
    {
      [workingDirectory setStringValue: [op filename]];
    }
}

/**
 * Action which opens up the receiver's arguments panel.
 */
- (void) showArguments: sender
{
  [argsPanel makeKeyAndOrderFront: nil];
}

/**
 * Action which opens up the receiver's environment variables panel.
 */
- (void) showEnvironment: sender
{
  [envPanel makeKeyAndOrderFront: nil];
}

/**
 * Action invoked when the user enter something into the `Standard Input'
 * text field and hits 'Return'.
 */
- (void) writeStdin: sender
{
  if (stdinHandle != nil)
    {
      NSString * string = [stdin stringValue];

      // append a trailing newline automagically
      string = [string stringByAppendingString: @"\n"];

      [stdinHandle writeData: [string
        dataUsingEncoding: NSUTF8StringEncoding]];
      [stdinHandle synchronizeFile];

      // reset the text field again
      [stdin setStringValue: nil];
    }
  else
    {
      NSRunAlertPanel(_(@"Not running"),
        _(@"The application is not running. You must launch\n"
          @"it before providing it any standard input."),
        nil, nil, nil);
    }
}

/**
 * Action to add an argument to the argument list.
 */
- (void) addArg: sender
{
  [arguments addObject: @"New Argument"];
  [args reloadData];
}

/**
 * Action to remove the selected argument from the argument list.
 */
- (void) removeArg: sender
{
  int row = [args selectedRow];

  if (row >= 0)
    {
      [arguments removeObjectAtIndex: row];
      [args reloadData];
    }
}

/**
 * Action to move the selected argument upwards in the list of arguments.
 */
- (void) moveArgUp: sender
{
  int row = [args selectedRow];

  if (row > 0)
    {
      id object;

      object = [arguments objectAtIndex: row - 1];
      [object retain];
      [arguments removeObjectAtIndex: row - 1];
      [arguments insertObject: object atIndex: row];
      [object release];

      [args reloadData];
      [args selectRow: row - 1 byExtendingSelection: NO];
    }
}

/**
 * Action to move the selected argument downwards in the list of arguments.
 */
- (void) moveArgDown: sender
{
  int row = [args selectedRow];

  if (row >= 0 && row + 1 < (int) [arguments count])
    {
      id object;

      object = [arguments objectAtIndex: row + 1];
      [object retain];
      [arguments removeObjectAtIndex: row + 1];
      [arguments insertObject: object atIndex: row];
      [object release];

      [args reloadData];
      [args selectRow: row + 1 byExtendingSelection: NO];
    }
}

/**
 * Action to add an environment variable.
 */
- (void) addEnv: sender
{
  NSString * newName = _(@"NewVariable");
  int i;

  for (i = 1;
       [[environment allKeys] containsObject: newName];
       newName = [NSString stringWithFormat: _(@"NewVariable%i"), i]);

  [environment setObject: _(@"A value") forKey: newName];
  ASSIGN(sortedEnvironmentNames, [[environment allKeys]
    sortedArrayUsingSelector: @selector(caseInsensitiveCompare:)]);

  [env reloadData];
  [env         selectRow: [sortedEnvironmentNames indexOfObject: newName]
    byExtendingSelection: NO];
}

/**
 * Action to remove an environment variable.
 */
- (void) removeEnv: sender
{
  int row = [env selectedRow];

  if (row >= 0)
    {
      [environment removeObjectForKey: [sortedEnvironmentNames
        objectAtIndex: row]];
      ASSIGN(sortedEnvironmentNames, [[environment allKeys]
        sortedArrayUsingSelector: @selector(caseInsensitiveCompare:)]);

      [env reloadData];
    }
}

/**
 * Notification method invoked when the task terminates itself.
 */
- (void) taskTerminated
{
  NSDictionary * userInfo;
  NSNotificationCenter * nc = [NSNotificationCenter defaultCenter];

  userInfo = [NSDictionary dictionaryWithObjectsAndKeys:
    document, @"Project",
    target, @"Target",
    task, @"Task",
    nil];

  [document logMessage: [NSString stringWithFormat:
    _(@"Application terminated with code %i"), [task terminationStatus]]];

  [self clearState];
  [self setLauncherState: GNUstepAppLauncherReadyState];

  [nc postNotificationName: GNUstepAppLauncherProjectDidTerminateNotification
                    object: self
                  userInfo: userInfo];
}

/**
 * Notification method invoked when we collect the subprocess' stdout.
 */
- (void) readStdout
{
  NSString * string = [[[NSString alloc]
    initWithData: [stdoutHandle availableData] encoding: NSUTF8StringEncoding]
    autorelease];

  [self appendStdoutMessage: string];
  [stdoutHandle waitForDataInBackgroundAndNotify];
}

/**
 * Notification method invoked when we collect the subprocess' stderr.
 */
- (void) readStderr
{
  NSString * string = [[[NSString alloc]
    initWithData: [stderrHandle availableData] encoding: NSUTF8StringEncoding]
    autorelease];

  [self appendStderrMessage: string];
  [stderrHandle waitForDataInBackgroundAndNotify];
}

- (void) moduleChanged: (NSNotification *) notif
{
  // if we became the current module, open the additional panels
  // as they were left in their previous state
  if ([[notif userInfo] objectForKey: @"Module"] == self)
    {
      if (argsPanelWasOpen)
        {
          [argsPanel orderFront: nil];
        }
      if (envPanelWasOpen)
        {
          [envPanel orderFront: nil];
        }
    }
  // otherwise close them, saving whether they were open or closed
  else
    {
      argsPanelWasOpen = [argsPanel isVisible];
      [argsPanel close];

      envPanelWasOpen = [envPanel isVisible];
      [envPanel close];
    }
}

- (NSArray *) moduleMenuItems
{
  return [NSArray arrayWithObjects:
    PMMakeMenuItem (_(@"Launch"), @selector(launch:), @"L", self),
    PMMakeMenuItem (_(@"Stop"), @selector(stopLaunch:), nil, self),
    PMMakeMenuItem (_(@"Kill"), @selector(kill:), nil, self),
    PMMakeMenuItem (_(@"Show Arguments..."), @selector(showArguments:),
      nil, self),
    PMMakeMenuItem (_(@"Show Environment..."), @selector(showEnvironment:),
      nil, self),
    nil];
}

- (NSArray *) toolbarItemIdentifiers
{
  return [NSArray arrayWithObjects:
    @"GNUstepAppLauncherLaunchToolbarItem",
    @"GNUstepAppLauncherStopToolbarItem",
    @"GNUstepAppLauncherKillToolbarItem",
    @"GNUstepAppLauncherShowArgumentsToolbarItem",
    @"GNUstepAppLauncherShowEnvironmentToolbarItem",
    nil];
}

- (BOOL) validateMenuItem: (id <NSMenuItem>) item
{
  return [self validateControl: item];
}

- (BOOL) validateToolbarItem: (NSToolbarItem *) item
{
  return [self validateControl: item];
}

- (int) numberOfRowsInTableView: (NSTableView *)aTableView
{
  if (aTableView == args)
    {
      return [arguments count];
    }
  else
    {
      return [environment count];
    }
}

- (id) tableView: (NSTableView *)aTableView 
objectValueForTableColumn: (NSTableColumn *)aTableColumn 
             row: (int)rowIndex
{
  NSString * identifier = [aTableColumn identifier];

  if (aTableView == args)
    {
      if ([identifier isEqualToString: @"Number"])
        {
          return [NSNumber numberWithInt: rowIndex + 1];
        }
      else
        {
          return [arguments objectAtIndex: rowIndex];
        }
    }
  else
    {
      if ([identifier isEqualToString: @"Name"])
        {
          return [sortedEnvironmentNames objectAtIndex: rowIndex];
        }
      else
        {
          return [environment objectForKey: [sortedEnvironmentNames
            objectAtIndex: rowIndex]];
        }
    }
}

- (void) tableView: (NSTableView *)aTableView 
    setObjectValue: (id)anObject 
    forTableColumn: (NSTableColumn *)aTableColumn
               row: (int)rowIndex
{
  if (aTableView == args)
    {
      [arguments replaceObjectAtIndex: rowIndex withObject: anObject];
    }
  else
    {
      NSString * identifier = [aTableColumn identifier];

      if ([identifier isEqualToString: @"Name"])
        {
          if ([environment objectForKey: anObject] != nil)
            {
              NSRunAlertPanel(_(@"Environment variable already present"),
                _(@"An environment variable named \"%@\"\n"
                  @"is already present. Delete the variable first."),
                nil, nil, nil, anObject);
            }
          else
            {
              id oldKey = [sortedEnvironmentNames objectAtIndex: rowIndex];
              id value = [environment objectForKey: oldKey];

              [environment setObject: value forKey: anObject];
              [environment removeObjectForKey: oldKey];
              ASSIGN(sortedEnvironmentNames, [[environment allKeys]
                sortedArrayUsingSelector: @selector(caseInsensitiveCompare:)]);

              [env reloadData];
              [env selectRow: [sortedEnvironmentNames indexOfObject: anObject]
                byExtendingSelection: NO];
            }
        }
      else
        {
          id key = [sortedEnvironmentNames objectAtIndex: rowIndex];

          [environment setObject: anObject forKey: key];
        }
    }
}

@end
