//
// CWin.java                              Copyright A C Norman, 2000
//
//
//
// This code is to provide a windowed interface to programs that
// were originally command-line oriented. Much of the design is
// based on a version I had implemented using the Microsoft Foundation
// Classes and used to support the CSL Lisp system.
//
// There is a single window that displays in a fixed-pitch font.
// It has menus:
//
// FILE:    read save_as save_selection to_file print print_selection exit
// EDIT:    cut copy paste select_all clear top bottom
// FONT:    reset-font reset-window-size
//          plain/bold (radio-style)
//          8/10/12/14/16/18/24/36/48 (radio-style)
//          (I should almost certainly have a "custom" font size too)
// BREAK:   interrupt backtrace page_mode exit
// HELP:    contents search help_on_help about
//
// Text on the screen falls into three parts:
//   (a) Text that has been generated by the program being run
//       together with the echo of earlier input from the user;
//   (b) Prompt text, generated by the system when input is wanted;
//   (c) A currently active input line that can be edited and that
//       will in due course be sent to the program
// there is also additional invisible additional components:
//   (d) Buffered key-strokes and events they have not yet been
//       reflected on the display;
//   (e) material on the clip-board as for CUT and PASTE operations.
//
// Each displayed character will have attributes, notably its font and
// colour. The text overall structured into paragraphs each of which
// contains a sequence of character-runs. The first paragraph starts
// at the start of the buffer and a new paragraph starts at the beginning
// of each bit of prompt text.  Output text is displayed in black,
// prompts in red and the active input line in dark blue (these colours
// are of course configurable at some level).
//
// There will in general be five positions noted on the screen:
//   dot:  where the mouse has most recently been clicked or
//         as positioned using cursor keys;
//   mark: a previous position of "dot", so that material from mark
//         to dot is "selected". If mark=dot then there is no selection;
//   save: the most recent position dot had had while it was within the
//         active input line. This is where user-generated input is
//         placed (keystrokes or PASTE operations);
//   last: the start of the active input line;
//   end:  the end of the entire material on the screen.
//
// (last is maybe the most important pointer!)
//
// Various events may be generated by mouse action or keystroke. Many of
// those can be handled just as if the whole document was a single simple
// area of text, so I will comment here on ones that I want to have
// behavious that does not match that of a Java default editor. I will
// list things in terms of the Java Action Names (but with "Action" trimmed):
//
// In the main description here the window can be in one of four states:
//   (1) The client program has asked for input. A prompt has been displayed
//       and any input that the user has typed can be edited before it
//       is sent to the client;
//   (2) The client has NOT yet asked for input, but output from it is being
//       displayed in an ordinary sort of manner;
//   (3) As (2) except that the user had selected "page mode" output and
//       the current page has been filled. No more output will be
//       added to the display until the user presses a key;
//   (4) starting from (1), (2) or (3) the user has activated an
//       "interrupt" facility. A message to that effect is being sent to the
//       client program and in the meanwhile any output it tries to send is
//       just ignored. Any input that the user had already typed (if in
//       state (1)) has been discarded. The client program is expected
//       to send an explicit acknowledgement at some stage to move things
//       back into state (2) and from there maybe into state (1).
// while in state (1) the client program is not permitted to send any
// output to the window: it is supposed to be silently awaiting input.
//
// Now back to my list of actions:
//
// copy
//       Send the current selection to the clipboard. I would like to do this
//       dumping stuff there in two formats. One should be simple text
//       and contains the characters visible on the screen with no
//       associated attributes (and if I later extend things to allow
//       non-textual information that gets turned into a very brief
//       text message). Then another format is specific to this code and
//       preserves character attributes.
// cut
//       Transfers all the text from the selected region to the clipbopard
//       as for "copy", but then reduces the selection to be its intersection
//       with the current active line before deleting anything. If the
//       current selection has no overlap with the current active line
//       I would like the CUT item on the edit menu disabled, and the
//       keyboard short-cut to give a beep.
// defaultKeyTyped
//       If in page break mode release screen and ignore character,
//       adjusting window title as relevant.
//       If there is a current selection reduce it to be its intersection
//       with the active input line. If a selection remains delete that text.
//       If no active region exists (see later) push this event onto a
//       queue of things to be done when one is created. Otherwise
//       if dot (which now is the same as mark) lies outside the active line
//       then move it to save. Insert the key there.
// insertBreak
//       If there is no active region queue this until there is one.
//       Stick newline character on end of active region.
//       Post active region to the client software as next line of input.
//       set status as "no active region present"
//============================================================================
// What I have just described has just one line editable. Part of a result
// of this is that the paste operation is a bit odd if the clipboard
// contains multiple lines of stuff. It also means that if an input expression
// is typed over several lines only the last of those can be corrected.
// An alternative would be to have a more general idea of "un-processed
// input text" which could then span several lines. But to make that seem
// really sensible an input sentence should end where the client program
// wanted it to, and accepting a chunk of input should make a break after
// the (typically) semicolon involved so that output from processing that
// sentence was put in the obvious place. By making it appear as if
// data transfers to the client were character by character it would be
// possible to tell where to insert the output.  But now one can not quite
// easily use the ENTER key to send data to the client program: I do not
// know quite what would be best to do. A half-baked version of this would
// let multi-line active regions arise just out of paste operations or
// maybe by haing a special key sequence to allow the user to insert
// newlines into text without activating the "accept" action.
//
// A further thought is that input should always be typed in without newlines
// but that the system formats it as you go if it is a bit long. I think I
// view as too hard for me to support in any general context at all. It would
// involve much more integration between the parser for my input syntax
// and the display systen.
//============================================================================
// insertTab
//       As defaultKeyTyped, but tabs should display as at least
//       one blank and then enough to pad to a multiple of 8 characters.
// [Maybe tab should insert the proper number of spaces rather than a
//  tab character]
// paste
//       Grab material from the clipboard. Inspect attributes and
//       discard characters that were part of a prompt string. Set up a queue
//       as if the remaining characters were typed by the user.
//       Note that stuff is retrieved from the clipboard when the PASTE is
//       requested but multi-line pastes may stay in an input queue for
//       processing for some time.
//       See above comments about the messiness here.
// insertContent
//       <just what is the relationship between this and PASTE?>
// deleteNextChar
//       If there is no active region queue this until there is one.
//       If dot is outside the active line move it to save before attempting
//       the delection. If dot is at the end of the document beep and do
//       nothing.
// deletePrevChar
//       If there is no active region queue this until there is one.
//       If dot is outside the active line move it to save before attempting
//       the delection. If dot is at the start of the active line beep
//       and do nothing.
// down (and also up)
//       Normal: move dot down/up one line (keepin in some column if possible)
//       Shift:  move so to create or extend a selection
//       Ctrl:   move dot down/up one paragraph
//       Shift/Ctrl: combine two effects
//       ALT:    MAYBE...
//           If no active region then queue up.
//           activate a doskey-like record of previous input lines
// [keyboard combinations to do with the arrow keys already seem pretty
//  heavy. Fitting doskey-like stuff in too seems slightly messy. I do not
//  know what is best here]
// left (and also right)
//       Normal: move dot one character
//       Shift:  move horizontally so as to create/extend a selection
//       Ctrl:   move to start of previous/next word
//       home/end keys move to start and end of current line
// <many actions which reset mark>
//       if new mark will be outside the active region but old was within
//       then set save.
//
// To allow discussion of other key sequences, eg based on ALT or
// the controll key (and possibly with additional menus in support
// that I have not listed before, I will include a psuedo action
// that is intended to cover both keyboard shortcuts implemented
// within Java's framework and special keys that I will define for
// myself:
//
// specialKeySequencePressed
//       Maybe ALT-up belongs here, since it is not even related to
//       the things that Java's default editorKit does.
//
// In addition I will want to take an action when the EDIT menu is
// about to be displayed:
//
// showEditMenu
//       This is to remind me that I want CUT disabled when the selection
//       is empty or totally outside the active region
//
// and I should consider some other menu items
//
// fileRead
//       for CSL I make this insert the text <in "filename";> as new
//       input. This is a jolly odd thing to do!
// fileSaveAs
// fileSaveSelection
// fileSaveExtras
//       PSL has special menu items for copying (etc) parts of the
//       text that are not just simple text.
// fileToFile
//       by which I mean spool/dripple or whatever you want to call it
// filePrint
// fileOPrintSelection
// fileExit
// editCut
//       disabled if no part of the selection is in the active region
// editCutCut
//       Maybe I should let people discard some of the historical output
//       if they REALLY want to.
// editCopy
// editPaste
// editSelectAll
// editClear
// editTop
// editBottom
//
// breakInterrupt
//       discards all queued up input events. If there is an active
//       area clears it and sends an INTERRUPT response to the client
//       application. If page mode is on resets so that another full page
//       can be displayed before a further pause. 
//       If no active input region then sets flag so that subsequent
//       printText requests will be ignored and sends an asynchronous
//       break request to the application. Restones sanity on getting
//       a response.
//
// the other menus are fairly decoupled from the display of text so
// I will not discuss them here.
//
// Yet further events reflect interaction with the program that this
// window is serving:
//
// requestLine(promptString)
//       Insert print test as start of a new paragraph. Establish new
//       active region, putting save in it. If there are queued input
//       events they are dispatched until one of them is an insertBreak.
// acceptLine
//       as mentioned under insertBreak. and breakInterrupt
// printText
//       This should not happen while there is an active region. It
//       just appends text to the end of the buffer. If page_mode has
//       been set then count lines and set a page break pause when
//       a screenful is there. This is released by any key-stroke.
//       Think a bit about users who re-size the screen while output is
//       being generated! Adjust window title while this delay is in
//       force, and also allow invisible buffer of at least a few more
//       lines of output to build up even while the screen is kept
//       stable.
// asynchronousBreak
//       sent to the client in nasty cases!
// breakACK
//       resonse from client to say that the break is acknowledged.
//
// printPicture
//       It MIGHT be that the program can "print" things other than text,
//       for instance graphical objects. Support for that sort of thing
//       is not considered here (yet). For REDUCE the idea of displays with
//       lots of fonts and general 2D layout may well be important!
//       Current THOUGHT
//          start2DOutput();
//          printText(...);   material sent in a format derived from
//                            TeX markup (since I want to display maths, and
//                            because REDUCE already contains an option to
//                            do this).
//          end2DOutput();
//
// Yet another issue is that the client program should be able to make
// calls that display (small amounts of) information on the title or
// menu bar (eg garbage collection or timing information). And the
// windowed front-end should buffer all output from the user (with great
// vigour to try to keep performance under control) but should flush the
// buffers before requesting input or 2 (say) second after the last
// screen update happened.
//
// For computer algebra the issue of 2D output is a very real one and the
// yet nastier issue of managing to COPY or CUT from it and get something
// that can be pasted in as input is hard enough for whole expressions (but
// one could have a protocol where the application always provided a
// re-inputable version as well as the display-friendly one) but gets
// worse if you want natural ways to allow mouse-drags to select
// semantically valid parts of a huge expression.
//
// With one earlier user-interface I had (CML) some user keystrokes could
// lead to displays that involved special symbols (in that case if you typed
// in "fn" you got a display of a Greek lambda, while 'a, 'b etc gave
// alpha, beta and so on. Then using the delete key has to repair things
// in a witty manner.

import java.io.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.text.*;

public class CWin extends JApplet
{

// This code should be launchable as either an applet or as
// an application. Note however that as of spring 2000 the use
// of Java 2 facilities means that web browsers will need a
// Java 1.2 plugin to be able to run this code, and that the HTML
// that launches the applet will have to dircet execution to that
// plugin. Furthermore in order that the file-system and clipboard
// can be accesses the applet will have to come from a signed
// source and suitable steps will have to be taken to grant it
// authority to perform these security-impinging actions.

static boolean isApplet;
String [] args = new String [0];

public static void main(String [] args)
{
// If the command line includes an argument "-w" then I will remove
// that so it doe not get looked at again and start the system up as a simple
// application to run from the command line rather than in a window.
//
// The effect is that I can rin things in THREE ways:
//    (1) As an applet, ie using a browser of some form. In such cases
//        it is hard to pass (variable) parameters;
//    (2) As an application but one that pops up a window and then behaves
//        much like an applet EXCEPT that it does not suffer from security
//        limits as much and you can re-size the window;
//    (3) As a command line application via "java -jar <whatever.jar> -w"
//        where the thing behaves as a traditional application.
//
    for (int i=0; i<args.length; i++)
    {   if (args[i].equalsIgnoreCase("-w"))
        {   String [] newArgs = new String[args.length-1];
            for (int j=0; j<i; j++) newArgs[j] = args[j];
            for (int j=i+1; j<args.length; j++) newArgs[j-1] = args[j];
            Jlisp.startup(newArgs,
                          new InputStreamReader(System.in),
                          new PrintWriter(System.out),
                          true);
            return;
        }
    }
    CWin m = new CWin();
    m.isApplet = false;
    m.args = args;
    JFrame f = new JFrame("Codemist");
    f.addWindowListener(new WindowAdapter()
    {   public void windowClosing(WindowEvent e)
        {   System.exit(0);
        }
    });
    f.getContentPane().add(m, BorderLayout.CENTER);
// The default size I set up here would not be too good for
// people running with low resolution screen-modes. The width of
// 850 is designed to give 80 columns of 16-point monospaced, at least
// on a Windows implementation at the time I first tried it. I will
// need to re-visit the issue of setting the size of my Frame and
// the choice of font. What I will do in the applet is to try to
// adjust my (default) font size to get around 80 characters across
// whatever window size happens. Actually when I create the fonts
// I will select them to accieve 80-columns, so what I mean here
// is that a width of 830 seems to manage to give me 80 columns at
// 16 points, which looks reasonable on a 1280x1024 display!
    f.setSize(830, 700);
// Note (horribly!) for the font-size adjustment to work I seem to have to
// make the Frame visible before I try to measure things.
    f.setVisible(true);
    m.init();
    m.start();
}

public CWin() 
{
    isApplet = true;  // will be reset to false if I am an application
}

// I put references to all the major sub-components in the
// top level of the CWin class. This has the disadvantage of
// tending to lock me into a single-document model, but the advantage
// that a single handle on the instance of a CWin gives me easy
// access to all the rest.

Container container;      // top of the JApplet
JScrollPane scroll;       // to make text scrollable
InputPane textpane;       // major visible component

MainThread program;       // task that is being supported

public void init()
{
// I want the window to be filled with a scrollable JTextPane that
// is editable and has my choice of font.
// The vertical scrollbar is always visible because that makes the
// width of the visible pane constant and so helps me keep my line-length
// understandable.

    textpane = new InputPane();
    scroll = new JScrollPane(
        textpane,
        JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
        JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
    (container = getContentPane()).add(scroll);


// because I have told the TextPane that its size can shrink below that
// of the ScrollPane I can be left with something else visible. So I need
// to make sure that ends up the same colour!
//
// Leave this off to see the size of the textpane more clearly
//  container.setBackground(new Color(yellowColor));   @@@@@@



// Menus hang off a menu bar and contain menu items
    JMenuBar bar;
    JMenu mm;

    setJMenuBar(bar = new JMenuBar());

    bar.add(mm = new JMenu("File"));
//============================================================================
// In various versions of the JDK (the issue is supposed to be closed
// from 1.2.2-002 onwards) if you type a key with the ALT qualifier then
// the character will be used BOTH to trigger the menu activity (as the
// mnemonic requested) and to send data (without an ALT qualifier visible!)
// to the application. So if you type ALT-F/S the there is a BIG change
// that an "f" character will end up inserted in your window. At present
// my view is that I will fetch a mended JDK as soon as I can and expect
// others who try this code to do the same. It seems there are no very
// tidy work-arounds. I hope very much that this comment will end up being
// one of those humerous historical asides that sometimes add ornament to
// code that would otherwise be very dull!
//============================================================================
    mm.setMnemonic('F');

// As I create each menu item I set up a listener that gets invoked
// whenever the user activates the menu concerned.

    addMenu(mm, "Clear", new ActionListener()
    {   public void actionPerformed(ActionEvent e)
        {   textpane.setText("");
            textpane.afterPrompt = 0;
        }
    }, 'C', KeyEvent.VK_N);  // as for "New"

    addMenu(mm, "Open", new ActionListener()
    {   public void actionPerformed(ActionEvent e)
        {   try
            {   JFileChooser ch = new JFileChooser("");
                ExtensionFileFilter f =
                    new ExtensionFileFilter(
                        new String [] {"red", "tst"},
                        "REDUCE source and test files");
                ch.setFileFilter(f);
                int r = ch.showOpenDialog(container);
                if (r == JFileChooser.APPROVE_OPTION)
                {   String fileName = ch.getSelectedFile().getAbsolutePath();
                    // toScreen("in \"" + fileName + "\";");
                    // and wait for user to hit ENTER to accept it!
                }
            }
            catch (Exception e1)
            {   // toScreen("++++ Could not access the file\n");
            }
        }
    }, 'O', 0);

    addMenu(mm, "Save", new ActionListener()
    {   public void actionPerformed(ActionEvent e)
        {
        }
    }, 'S', 0);

// If I am running as an applet I am not permitted to quit. I may
// be killed by my browser. If however I am an application I can
// provide an "Exit" menu item.

    if (!isApplet)
    {   addMenu(mm, "Exit", new ActionListener()
        {   public void actionPerformed(ActionEvent e)
            {   System.exit(0);
            }
        }, 'X', 0);
    }

    bar.add(mm = new JMenu("Edit"));
    mm.setMnemonic('E');

    addMenu(mm, "Cut", new ActionListener()
    {   public void actionPerformed(ActionEvent e)
        {   textpane.cut.actionPerformed(e);
        }
    }, 'T', KeyEvent.VK_X);

    addMenu(mm, "Copy", new ActionListener()
    {   public void actionPerformed(ActionEvent e)
        {   textpane.copy.actionPerformed(e);
        }
    }, 'C', KeyEvent.VK_C);

    addMenu(mm, "Paste", new ActionListener()
    {   public void actionPerformed(ActionEvent e)
        {   textpane.paste.actionPerformed(e);
        }
    }, 'P', KeyEvent.VK_V);

    bar.add(mm = new JMenu("Font"));
    mm.setMnemonic('O');
    ActionListener setFont = new ActionListener()
    {
        public void actionPerformed(ActionEvent e)
        {   String s = e.getActionCommand();
            int weight = textpane.fontWeight, size = textpane.fontWeight;
            if (s.equals("Bold")) weight = Font.BOLD;
            else if (s.equals("Plain")) weight = Font.PLAIN;
            else size = Integer.parseInt(s);
            textpane.setupFonts(size, weight);
        }
    };

    ButtonGroup buttonGroup = new ButtonGroup();
    addMenuRadio(mm, buttonGroup, "Plain", 'P', true,  setFont);
    addMenuRadio(mm, buttonGroup, "Bold",  'B', false, setFont);

    mm.addSeparator();
    buttonGroup = new ButtonGroup();

    int [] fontSizes = 
        {8, 10, 12, 14, 16, 18, 24, 36, 48};
    for (int i=0; i<fontSizes.length; i++)
    {   int sz = fontSizes[i];
        char c;
// Given the particular collection of font sizes that I support here
// I will provide mnemonics for as many as I easily can.
        if (sz == 8) c = 's';       // Small
        else if (sz < 20) c = (char)('0' + (sz - 10));
        else if (sz == 24) c = 'm'; // Medium
        else if (sz == 36) c = '3';
        else c = 'x';               // eXtra large
        addMenuRadio(mm, buttonGroup, Integer.toString(sz),
                     c, sz == textpane.fontSize, setFont);
    }

    bar.add(mm = new JMenu("Help")); // setHelpMenu? Not implemented yet!
    mm.setMnemonic('H');

    addMenu(mm, "Contents", new ActionListener()
    {   public void actionPerformed(ActionEvent e)
        {
        }
    }, 'C', 0);

    program = new MainThread(this);

}

int getLineLength()
{
    int w = textpane.metrics.charWidth('x');
    int w1 = getWidth(); // of the CWin JApplet
    Insets ins = textpane.getBorder().getBorderInsets(textpane);
    int scrollWidth = scroll.getVerticalScrollBar().getMinimumSize().width;
    w1 = w1 - ins.left - ins.right - scrollWidth;
    return w1/w;
}

public void start()
{
    program.start();
}


void addMenu(JMenu menu, String name, ActionListener a, 
             char mnemonic, int accel)
{
    JMenuItem menuItem = new JMenuItem(name);  
    menuItem.setMnemonic(mnemonic);
    if (accel != 0)
    {   menuItem.setAccelerator(
            KeyStroke.getKeyStroke(accel, KeyEvent.CTRL_MASK));
    }
    menu.add(menuItem);
    menuItem.addActionListener(a);
}

void addMenuRadio(JMenu menu, ButtonGroup buttonGroup, String name,
                  char mnemonic, boolean ticked, ActionListener a)
{
    JRadioButtonMenuItem menuItem = new JRadioButtonMenuItem(name, ticked);  
    menu.add(menuItem);
    if (mnemonic != ' ') menuItem.setMnemonic(mnemonic);   
    buttonGroup.add(menuItem);
    menuItem.addActionListener(a);
}

} // end of the CWin class

// The next collection of classes are concerned with moving data between
// the windowed front-end and the code that it is there to support.


class CWinReader extends Reader
{

    MainThread body;
    String s;
    int pos, len;

    CWinReader(MainThread body)
    {
        this.body = body;
        s = null;
    }

    public int available()
    {
        if (s != null) return 1;
        else return 0;
    }

    public void close()
    {
    }

    public boolean markSupported()
    {
        return false;
    }

    public int read()
    {
        if (s == null)
        {   body.out.flush();
            body.t.stop();    // do not want flush events while reading
            s = body.host.textpane.getInputLine();
            pos = 0;
            len = s.length();
            body.t.restart();
            body.t.start();
        }
        if (pos == len)
        {   s = null;
            return (int)'\n';
        }
        else return (int)s.charAt(pos++);
    }

// I am NOT expecting to want to read huge amounts from the
// input window, and I expect performance to be limited by the speed
// that humans can type. So I feel fairly happy implementing the
// block input methods so they actually work one byte at a time.
// This may be a BIT silly if the user has generated a load of stuff
// using PASTE and perhaps I ought to be a bit cleverer anyway.

    public int read(char [] b)
    {
        if (b.length == 0) return 0;
        b[0] = (char)read();
        return 1;
    }

    public int read(char [] b, int off, int len)
    {
        if (b.length == 0 || len == 0) return 0;
        b[off] = (char)read();
        return 1;
    }

}

class CWinWriter extends CharArrayWriter
{
// As with reading from the window, the interface I support here is one
// that works in terms of CHARACTERS not BYTES. This has to be synchronized
// because I set up a timer that can flush it at slightly unpredictable
// times (so that I can buffer LOTS when my code is generating output fast,
// and so get decent performance, but so that the screen is refereshed every
// couple of seconds)

    CWin host;

    CWinWriter(CWin host)
    {
        super(8000);      // nice big buffer by default
        this.host = host;
    }

    public void close()
    {
        flush();
    }

    public void flush()
    {
        super.flush();
        if (size() != 0)  // mild optimisation, I suppose!
        {
// The write-up of the Writer class telle me to lock this way in sub-classes.
// Here I MUST ensure that if I do the toString() that I do the reset()
// before anybody adds any more characters to this stream.
            synchronized (lock)
            {   host.textpane.toScreen(toString());
                reset();
            }
        }
        host.program.t.restart(); // wait 2 secs before forced flush
    }

}

// The MainThread class is used to encapsulate the behaviour that
// might otherwise have gone into a dull command-line application.

class MainThread extends Thread
{
    
    CWin host;

    Reader in;       // NB a Reader not an InputStream
    PrintWriter out; // NB a Writer not and OutputStream

    static final int screenRefreshInterval = 3000;
    Timer t;

    MainThread(CWin host)
    {
        this.host = host;
        in = new CWinReader(this);
        out = new PrintWriter(new CWinWriter(host), false);
        t = new Timer(screenRefreshInterval,
            new ActionListener() {
                public void actionPerformed(ActionEvent e)
                {
                    out.flush();
                }
            });
        t.setCoalesce(true);
        t.start();
    }

    public void run()
    {
        Jlisp j = new Jlisp();
        Jlisp.startup(host.args, in, out, false);
        out.flush();
    }


} // end of MainThread class

// end of CWin.java

