Module:    environment-framework
Synopsis:  Environment Framework
Author:    Andy Armstrong, Chris Page
Copyright:    Original Code is Copyright (c) 1995-2004 Functional Objects, Inc.
              All rights reserved.
License:      Functional Objects Library Public License Version 1.0
Dual-license: GNU Lesser General Public License
Warranty:     Distributed WITHOUT WARRANTY OF ANY KIND

/// History mechanism

define open abstract class <frame-history-mixin> (<frame>)
  constant slot %history = make(<deque>);
  constant slot %forward-history = make(<deque>);
  slot %history-back-button = #f;
  slot %history-forward-button = #f;
end class <frame-history-mixin>;

// Some label strings

define constant $history-back-title        = "Back";
define constant $history-back-to-prefix    = "Back to ";
define constant $history-forward-title     = "Forward";
define constant $history-forward-to-prefix = "Forward to ";


define open generic note-frame-history-changed (frame :: <frame>) => ();

define open generic frame-select-previous-object
    (frame :: <frame-history-mixin>) => (object, success? :: <boolean>);

define open generic frame-select-next-object
    (frame :: <frame-history-mixin>) => (object, success? :: <boolean>);

define open generic frame-view-history 
    (frame :: <frame-history-mixin>) => ();

define method initialize (frame :: <frame-history-mixin>, #key) => ()
  next-method();
  update-frame-commands(frame);
end method initialize;

define method frame-history
    (frame :: <frame-history-mixin>)
 => (history :: <deque>)
  frame.%history
end method frame-history;

define method update-frame-commands (frame :: <frame>) => ()
  let previous-history? = frame-has-previous-history?(frame);
  let next-history?     = frame-has-next-history?(frame);
  command-enabled?(frame-select-previous-object, frame) := previous-history?;
  command-enabled?(frame-select-next-object, frame)     := next-history?;
  command-enabled?(frame-view-history, frame)
    := ~empty?(frame-history(frame));

  // Update documentation strings to include the destination name
  gadget-documentation(frame.%history-back-button)
    := if (previous-history?)
	 let previous-object
	   = frame-coerce-raw-object(frame, second(frame.%history));
         concatenate($history-back-to-prefix, 
		     frame-primary-object-name(frame, previous-object))
       else
         $history-back-title
       end if;
  gadget-documentation(frame.%history-forward-button)
    := if (next-history?)
	 let next-object
	   = frame-coerce-raw-object(frame, first(frame.%forward-history));
         concatenate($history-forward-to-prefix, 
		     frame-primary-object-name(frame, next-object))
       else
         $history-forward-title
       end if;
end method update-frame-commands;

define method note-frame-history-changed (frame :: <frame>) => ()
  #f
end method note-frame-history-changed;

define method frame-select-object (frame :: <frame>, object) => ()
  update-frame-commands(frame);
  #f
end method frame-select-object;

define method frame-add-to-history
    (frame :: <frame-history-mixin>, object) => ()
  let history = frame.%history;
  let coerced-object = frame-coerce-raw-object(frame, object);
  let coerced-history-object = ~empty?(history) & frame-coerce-raw-object(frame, history[0]);
  unless (coerced-object = coerced-history-object)
    push(history, object);
    size(frame.%forward-history) := 0;
    frame-select-object(frame, object);
    note-frame-history-changed(frame)
  end;
end method frame-add-to-history;

define method frame-remove-from-history
    (frame :: <frame-history-mixin>, object) => ()
  let history = frame.%history;
  if (size(history) > 1)
    frame-select-previous-object(frame);
    remove!(frame.%forward-history, object)
  else
    size(history) := 0;
    frame-select-next-object(frame);
    if (size(history) = 0)
      note-frame-last-object-closed(frame);
    end
  end;
  note-frame-history-changed(frame)
end method frame-remove-from-history;

define method note-frame-last-object-closed
     (frame :: <frame-history-mixin>) => ()
  update-frame-commands(frame)
end method note-frame-last-object-closed;

define method frame-has-previous-history? 
    (frame :: <frame-history-mixin>) => (previous? :: <boolean>)
  size(frame.%history) > 1
end method frame-has-previous-history?;

define method frame-has-next-history? 
    (frame :: <frame-history-mixin>) => (next? :: <boolean>)
  ~empty?(frame.%forward-history)
end method frame-has-next-history?;

define method frame-select-previous-object
    (frame :: <frame-history-mixin>) => (object, success? :: <boolean>)
  let history = frame.%history;
  if (frame-has-previous-history?(frame))
    let old-item = pop(history);
    let item = history[0];
    push(frame.%forward-history, old-item);
    frame-select-object(frame, item);
    note-frame-history-changed(frame);
    values(item, #t)
  end;
end method frame-select-previous-object;

define method frame-select-next-object
    (frame :: <frame-history-mixin>) => (object, success? :: <boolean>)
  let forward-history = frame.%forward-history;
  if (frame-has-next-history?(frame))
    let item = pop(forward-history);
    push(frame.%history, item);
    frame-select-object(frame, item);
    note-frame-history-changed(frame);
    values(item, #t)
  end;
end method frame-select-next-object;

define constant $frame-history-limit = 30;

define method frame-most-recent-objects 
    (frame :: <frame-history-mixin>, #key count = $frame-history-limit)
  let history 
    = remove-duplicates(concatenate(frame.%forward-history, frame.%history),
			test: method (raw1, raw2) => (equal? :: <boolean>)
				let coerced1 = frame-coerce-raw-object(frame, raw1);
				let coerced2 = frame-coerce-raw-object(frame, raw2);
				coerced1 = coerced2
			      end);
  if (count & size(history) > count)
    copy-sequence(history, end: count)
  else
    history
  end
end method frame-most-recent-objects;

define variable $view-history-dialog-width  :: false-or(<integer>) = #f;
define variable $view-history-dialog-height :: false-or(<integer>) = #f;

define method frame-view-history
    (frame :: <frame-history-mixin>) => ()
  let (object, success?, width, height)
    = choose-from-dialog
        (frame-most-recent-objects(frame, count: #f),
         label-key: method (raw-object)
		      let object = frame-coerce-raw-object(frame, raw-object);
		      frame-primary-object-name(frame, object)
		    end,
         default-item: frame-raw-primary-object(frame),
         title: "History",
         owner: frame,
	 width:  $view-history-dialog-width,
	 height: $view-history-dialog-height);
  if (success? & object)
    $view-history-dialog-width  := width;
    $view-history-dialog-height := height;
    frame-primary-object(frame) := object
  end
end method frame-view-history;


/// History command table

define variable $back-bitmap    :: <label-type> = "<";
define variable $forward-bitmap :: <label-type> = ">";

define constant $back-doc    = "Goes back one step.";
define constant $forward-doc = "Goes forward one step.";
define constant $history-doc = "Displays the history.";

define command-table *history-movement-command-table* (*global-command-table*)
  menu-item $history-back-title    = frame-select-previous-object,
    accelerator:   make-keyboard-gesture(#"left", #"alt"),
    documentation: $back-doc,
    image: $back-bitmap;
  menu-item $history-forward-title = frame-select-next-object,
    accelerator:   make-keyboard-gesture(#"right", #"alt"),
    documentation: $forward-doc,
    image: $forward-bitmap;
end command-table *history-movement-command-table*;

define command-table *history-command-table* (*global-command-table*)
  include *history-movement-command-table*;
  separator;
  menu-item "View History" = frame-view-history,
    documentation: $history-doc;
end command-table *history-command-table*;

define method make-history-tool-bar-buttons
    (frame :: <frame-history-mixin>)
 => (buttons :: <sequence>)
  frame.%history-back-button
    := make(<button>,
           label: $back-bitmap,
           documentation: $history-back-title,
           command: frame-select-previous-object,
           activate-callback: method (sheet)
                                let frame = sheet-frame(sheet);
                                frame-select-previous-object(frame)
                              end);
  frame.%history-forward-button
    := make(<button>, 
           label: $forward-bitmap,
           documentation: $history-forward-title,
           command: frame-select-next-object,
           activate-callback: method (sheet)
                                let frame = sheet-frame(sheet);
                                frame-select-next-object(frame)
                              end);
  vector(frame.%history-back-button, frame.%history-forward-button)
end method make-history-tool-bar-buttons;