//
// 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