Artifact cc1710d490b39648738efb9a6c2b8a90e0fd4c8f8f98f2289ef40d691f3994ee:
- Executable file
r37/lisp/csl/jlisp/InputPane.java
— part of check-in
[f2fda60abd]
at
2011-09-02 18:13:33
on branch master
— Some historical releases purely for archival purposes
git-svn-id: https://svn.code.sf.net/p/reduce-algebra/code/trunk/historical@1375 2bfe0521-f11c-4a00-b80e-6202646ff360 (user: arthurcnorman@users.sourceforge.net, size: 26137) [annotate] [blame] [check-ins using] [more...]
// // InputPane.java Copyright A C Norman, 2000 // // // import java.io.*; import java.awt.*; import java.awt.event.*; import java.awt.datatransfer.*; import javax.swing.*; import javax.swing.event.*; import javax.swing.text.*; import java.util.*; class InputPane extends JTextPane { // This extends a JTextPane since that lets me hook onto all the // actions and general facilities that that provides me with. But what // I first do here is to get myself hooks onto all those existing actions // and then install a new keymap that directs most things to my own set of // actions. During development these will just invoke the one that I had // inherited, often printing a message so I can verify what is going on. // But eventually they implement the behaviour that I really want, much of // which is described in the file CWin.java StyledDocument doc; // A InputPane can be in one of four states. int state = OUTPUTMODE; static final int INPUTMODE = 0; static final int OUTPUTMODE = 1; static final int PAGEWAIT = 2; static final int INTERRUPTED = 3; static final int redColor = 0x00ff0040; // slight purple tinge static final int yellowColor = 0x00ffffc0; // rather pale yellow static final int blueColor = 0x000000c0; // darkish blue static final int greenColor = 0x00008040; // darkish green static final int blackColor = 0x00000040; // very dark blue SimpleAttributeSet redText; SimpleAttributeSet yellowText; SimpleAttributeSet blueText; SimpleAttributeSet greenText; SimpleAttributeSet blackText; String monospacedFontName = "MonoSpaced"; String fontName = "Serif"; int fontSize = 16; int fontWeight = Font.PLAIN; Font fixedFont, font, smallFont, tinyFont; Font italicFont, smallItalicFont, tinyItalicFont; FontMetrics metrics; DefaultCaret caret; public boolean getScrollableTracksViewportWidth() { // System.out.println("check width option"); return true; // (doc.getLength() == 0); } class ToScreen implements Runnable { String b; ToScreen(String s) { b = s; } public void run() { try { doc.insertString(doc.getLength(), b, null); caret.setDot(doc.getLength()); } catch (BadLocationException e) {} } } // toScreen is called from other threads - I must protect Swing against // that! void toScreen(String b) { SwingUtilities.invokeLater(new ToScreen(b)); } int afterPrompt = 0; void showPrompt() { try { doc.insertString(doc.getLength(), (Fns.prompt==null ? "> " : Fns.prompt), redText); afterPrompt = doc.getLength(); caret.setDot(afterPrompt); scrollRectToVisible(caret); } catch (BadLocationException e) {} } Action [] pendingActions = new Action[10]; ActionEvent [] pendingEvents = new ActionEvent[10]; int pendingIn = 0, pendingOut = 0; void deferAction(Action a, ActionEvent e) { int l = pendingActions.length; pendingActions[pendingIn] = a; pendingEvents[pendingIn++] = e; if (pendingIn == l) pendingIn = 0; if (pendingIn == pendingOut) // buffer now full { Action [] w = new Action[2*l]; ActionEvent [] w1 = new ActionEvent[2*l]; pendingIn = 0; for (int i=pendingOut; i<l; i++) { w[pendingIn] = pendingActions[pendingOut+i]; w1[pendingIn++] = pendingEvents[pendingOut+i]; } for (int i=0; i<pendingOut; i++) { w[pendingIn] = pendingActions[i]; w1[pendingIn++] = pendingEvents[i]; } pendingOut = 0; pendingActions = w; pendingEvents = w1; } } void performPendedActions() { int l = pendingActions.length; while (state == INPUTMODE && pendingIn != pendingOut) { Action a = pendingActions[pendingOut]; ActionEvent e = pendingEvents[pendingOut++]; if (pendingOut == l) pendingOut = 0; a.actionPerformed(e); } } String inputData = null; // getInputLine will be performed on a task thread, not on the // event dispatch thread. Therefor I have to do funny things to // make it valid for working with Swing. class RequestInput implements Runnable { public void run() { showPrompt(); state = INPUTMODE; performPendedActions(); } } Runnable request = new RequestInput(); synchronized String getInputLine() { SwingUtilities.invokeLater(request); // wake up the event dispatch thread! // I use a shared variable "inputData" to transfer data back. But if I // make all methods that touch it synchonized I should be OK even there. while (inputData == null) { // Suppose I am unlucky and the pended actions do not include a newline // but one is typed while I am just about HERE. Then a notify() is // done but I miss it! Well in fact by giving a time-out here I am pretty // much OK. In around 0.2 of a second I will come round and detect that // inputData has become non-null and so I will exit the loop. Whew! // The 0.2 sec choice is to keep the overhead of busy-waiting under // some degree of control while trying to appear responsive in even this // odd case. My expectation is that USUALLY the newline will not arrive // until I am safely within the wait() so there will not be any undue // delay. try { wait(200); } catch (InterruptedException e) {} } String d = inputData; // NB the newline is implicit inputData = null; return d; } synchronized void setInputData(String s) { inputData = s; state = OUTPUTMODE; notify(); } HashMap actionHash = new HashMap(); Action getAction(String name) { return (Action)actionHash.get(name); } Action oldcopy, copy; Action oldcut, cut; Action olddefaultKeyTyped, defaultKeyTyped; Action olddeleteNextChar, deleteNextChar; Action olddeletePrevChar, deletePrevChar; Action oldinsertBreak, insertBreak; Action oldinsertContent, insertContent; Action oldinsertTab, insertTab; Action oldpaste, paste; Action historyUp; Action historyDown; Action insertPasted; Vector history = new Vector(10, 10); int historyIndex; InputPane myself; InputPane() { myself = this; doc = getStyledDocument(); setPreferredSize(new Dimension(800, 200)); setEditable(true); guessFont(); setupFonts(fontSize, fontWeight); caret = (DefaultCaret)getCaret(); requestFocus(); setBackground(new Color(yellowColor)); // a pale yellow setForeground(new Color(blackColor)); // very dark blue! // While updating the contents of the document I will often want to // set attributes, specifically ones that are to do with the colour of // the text inserted. Here are some attribute values ready to help me. redText = new SimpleAttributeSet(); yellowText= new SimpleAttributeSet(); blueText = new SimpleAttributeSet(); greenText = new SimpleAttributeSet(); blackText = new SimpleAttributeSet(); StyleConstants.setForeground(redText, new Color(redColor)); StyleConstants.setForeground(yellowText, new Color(yellowColor)); StyleConstants.setForeground(blueText, new Color(blueColor)); StyleConstants.setForeground(greenText, new Color(greenColor)); StyleConstants.setForeground(blackText, new Color(blackColor)); Action [] a = getActions(); for (int i=0; i<a.length; i++) actionHash.put(a[i].getValue(Action.NAME), a[i]); oldcopy = getAction(StyledEditorKit.copyAction); oldcut = getAction(StyledEditorKit.cutAction); olddefaultKeyTyped = getAction(StyledEditorKit.defaultKeyTypedAction); olddeleteNextChar = getAction(StyledEditorKit.deleteNextCharAction); olddeletePrevChar = getAction(StyledEditorKit.deletePrevCharAction); oldinsertBreak = getAction(StyledEditorKit.insertBreakAction); oldinsertContent = getAction(StyledEditorKit.insertContentAction); oldinsertTab = getAction(StyledEditorKit.insertTabAction); oldpaste = getAction(StyledEditorKit.pasteAction); copy = new CopyAction(); cut = new CutAction(); defaultKeyTyped = new DefaultKeyTypedAction(); deleteNextChar = new DeleteNextCharAction(); deletePrevChar = new DeletePrevCharAction(); insertBreak = new InsertBreakAction(); insertContent = new InsertContentAction(); insertTab = new InsertTabAction(); paste = new PasteAction(); historyUp = new HistoryUpAction(); historyDown = new HistoryDownAction(); insertPasted = new InsertPastedAction(); Keymap map = getKeymap(); map.setDefaultAction(new DefaultKeyTypedAction()); map.addActionForKeyStroke( // ^X => CUT KeyStroke.getKeyStroke(KeyEvent.VK_X, Event.CTRL_MASK), cut); map.addActionForKeyStroke( // ^V => PASTE KeyStroke.getKeyStroke(KeyEvent.VK_V, Event.CTRL_MASK), paste); map.addActionForKeyStroke( // ^C => COPY KeyStroke.getKeyStroke(KeyEvent.VK_C, Event.CTRL_MASK), copy); map.addActionForKeyStroke( // backspace => deletePrev KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE, 0), deletePrevChar); map.addActionForKeyStroke( // delete => deleteNext KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0), deleteNextChar); map.addActionForKeyStroke( // enter => accept the line KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), insertBreak); map.addActionForKeyStroke( // tab => tab KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0), insertTab); // The default actions for the up and down keys are // up/down move up and down one line // shift+up/down ditto but extending a selection // ctrl+up/down up & down one paragraph // ctrl+shift select up and sown by the paragraph // I modify this so that ctrl+up/down gives a behaviour rather like // that of "doskey" or the command history scheme present in some // Unix shells. It replaces the whole of the current input line // with one of the previous input lines (which I therefore need to // store away) map.addActionForKeyStroke( // ctrl-UP KeyStroke.getKeyStroke(KeyEvent.VK_UP, Event.CTRL_MASK), historyUp); map.addActionForKeyStroke( // ctrl-DOWN KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, Event.CTRL_MASK), historyDown); setKeymap(map); requestFocus(); replaceSelection("\n" + // getPreferredScrollableViewportSize() + "\n" + getBounds() + "\n" + getWidth() + "\n" ); } void guessFont() { fontSize = 16; fontWeight = Font.PLAIN; } void setupFonts(int size, int weight) { fontSize = size; fontWeight = weight; // I will prepare seven fonts. The first is a fixed pitch one used for // simple text output. The remaining seven (and I may decide later on that // I need yet more) are Roman & Italic styles in three sizes: the intent is // that they are for body, subscript and sub-subscripts. fixedFont = new Font(monospacedFontName, fontWeight, fontSize); setFont(fixedFont); metrics = getFontMetrics(fixedFont); // Now the others... I am somewhat uncertain about the scaling // rules that I should apply here, but I am confident that if the // main font is really tiny already I should be cautious about // reducing too abruptly. Tune this for the best visual effect! font = new Font(fontName, fontWeight, fontSize); italicFont = new Font(fontName, fontWeight+Font.ITALIC, fontSize); int smallSize = (fontSize <= 10) ? fontSize-1 : (fontSize <= 14) ? fontSize-2 : ((7*fontSize+9)/10); smallFont = new Font(fontName, fontWeight, smallSize); smallItalicFont = new Font(fontName, fontWeight+Font.ITALIC, smallSize); int tinySize = (smallSize <= 10) ? smallSize-1 : (smallSize <= 14) ? smallSize-2 : ((7*smallSize+9)/10); tinyFont = new Font(fontName, fontWeight, tinySize); tinyItalicFont = new Font(fontName, fontWeight+Font.ITALIC, tinySize); } // I want to arrange that input up to and including the most // recent prompt string is read-only. To achieve this I arrange // to reset dot and mark to be beyond there whenever an interesting // event is triggered. I allow them to roam so I can COPY from earlier // material. int adjustSelection() { int dot = caret.getDot(); int mark = caret.getMark(); int x = afterPrompt; if (dot < afterPrompt) { if (mark < afterPrompt) { caret.setDot(afterPrompt); dot = mark = afterPrompt; } else { caret.setDot(mark); caret.moveDot(afterPrompt); dot = afterPrompt; } } else if (mark < afterPrompt) { caret.setDot(afterPrompt); caret.moveDot(dot); mark = afterPrompt; } if (mark < dot) return mark; else return dot; } String clipboardSubstitute = null; class CopyAction extends AbstractAction { CopyAction() { super(StyledEditorKit.copyAction); } public void actionPerformed(ActionEvent e) { // I possibly want to customise this to transfer some style information // to the clipboard as well as plain text. Eg I probably want to do special // things with prompt strings. But also note that I want COPY (and so also // CUT) to be executed instantly and not deferred waiting for input. Actually // that means that CUT will not do much cutting in such circumstances. // int mark = caret.getMark(); int dot = caret.getDot(); if (mark == dot) return; else if (mark > dot) { int w = mark; mark = dot; dot = w; } // now (mark < dot) defines the region of my selection StringBuffer s = new StringBuffer(); // This really feels pretty gross. I look at each character and discard it // if it is the colour that I use for prompts! Doing it one character at a // time makes COPY a slow process, but I expect it only gets used on smallish // selections! The reasoning behind this is that I may want to re-input // previous segments of stuff, and if the prompts get picked up by "COPY" // and put back by "PASTE" that will mess me up. // // Maybe a yet better variation will be to map things so that the // text on the clipboard reads // on screen on clipboard // \ \\ // { \{ // } \} // prompt {\p prompt} // where this leaves capability for other bits of clipboard syntax // involving {} to denote other effects I may later introduce. // // Anyway that is not yet done and needs more thought! for (int i=mark; i<dot; i++) { try { Element ch = doc.getCharacterElement(i); AttributeSet a = ch.getAttributes(); Color col = doc.getForeground(a); if (new Color(redColor).equals(col)) continue; String chs = doc.getText(i, 1); s.append(chs); } catch (BadLocationException e1) {} } try { Clipboard cb = getToolkit().getSystemClipboard(); StringSelection ss = new StringSelection(s.toString()); cb.setContents(ss, ss); } catch (SecurityException e1) { clipboardSubstitute = s.toString(); } } } class CutAction extends AbstractAction { CutAction() { super(StyledEditorKit.cutAction); } public void actionPerformed(ActionEvent e) { // I want to copy it ALL to the clipboard (and I use the current // version of that action so I can deal with style-specific stuff // sometime when I feel keen). But then I delete just that bit // which is within the current input line. copy.actionPerformed(e); if (state != INPUTMODE) return; adjustSelection(); replaceSelection(""); } } class DefaultKeyTypedAction extends AbstractAction { DefaultKeyTypedAction() { super(StyledEditorKit.defaultKeyTypedAction); } public void actionPerformed(ActionEvent e) { int c = (int)e.getActionCommand().charAt(0); // I will be rather cautious about what characters I insert into my text. // In particular and perhaps unreasonably I will NOT allow characters with // code 0x100 or upwards. I also forbid control characters and deletes. if ((c & 0x7f) < 0x20 || (c & 0x7f) == 0x7f || c >= 0x100 || (e.getModifiers() & (Event.CTRL_MASK|Event.ALT_MASK|Event.META_MASK)) != 0) return; if (state != INPUTMODE) { deferAction(this, e); return; } // NB the "dot" returned here is the lower of dot/mark and so it is the // position to insert after I have done a replaceSelection(""); int dot = adjustSelection(); // I replace the selection with an empty string and THEN insert my // new character because that lets me control the attributes (in my case // colour) of inserted text. replaceSelection(""); try { doc.insertString(dot, e.getActionCommand(), blueText); caret.setDot(dot+1); } catch (BadLocationException e1) {} } } int adjustForDelete(boolean left) { // When I hit either delete-forwards or delete-backwards I will behave // as follows: // If either dot or mark line within the current input line // I shrink the selection to be the intersection of the current one // and the input line. If both dot and mark are outside the // current line then delete-forwards moves them to its start and // delete backwards moves them both to its end. // If there is a non-empty selection left I delete that material and // that is all I do (whichever direction the delete was in) // Finally I have a caret location within the input line and if // there is a character to the relevant size of it I delete that. int dot = caret.getDot(); int mark = caret.getMark(); int x = afterPrompt; if (dot < afterPrompt) { if (mark < afterPrompt) { if (left) { dot = mark = doc.getLength(); caret.setDot(dot); } else { dot = mark = afterPrompt; caret.setDot(afterPrompt); } } else { caret.setDot(mark); caret.moveDot(afterPrompt); dot = afterPrompt; } } else if (mark < afterPrompt) { caret.setDot(afterPrompt); caret.moveDot(dot); mark = afterPrompt; } // now the selection is fully after the prompt location if (mark == dot) return dot; replaceSelection(""); return -1; } class DeleteNextCharAction extends AbstractAction { DeleteNextCharAction() { super(StyledEditorKit.deleteNextCharAction); } public void actionPerformed(ActionEvent e) { if (state != INPUTMODE) { deferAction(this, e); return; } int dot = adjustForDelete(false); if (dot < 0) return; // done if (dot == doc.getLength()) return; caret.setDot(dot); caret.moveDot(dot+1); replaceSelection(""); } } class DeletePrevCharAction extends AbstractAction { DeletePrevCharAction() { super(StyledEditorKit.deletePrevCharAction); } public void actionPerformed(ActionEvent e) { if (state != INPUTMODE) { deferAction(this, e); return; } int dot = adjustForDelete(true); if (dot <= afterPrompt) return; caret.setDot(dot); caret.moveDot(dot-1); replaceSelection(""); } } class InsertBreakAction extends AbstractAction { InsertBreakAction() { super(StyledEditorKit.insertBreakAction); } public void actionPerformed(ActionEvent e) { if (state != INPUTMODE) { deferAction(this, e); return; } int l = doc.getLength(); try { String s = getText(afterPrompt, l - afterPrompt); history.add(s); // remember it! historyIndex = history.size(); // now I will put a newline on the screen. caret.setDot(l); // so it tends to remain at the end... doc.insertString(l, "\n", blueText); // I should do the setInputData LAST here since it flips my global // state from INPUTMODE to OUTPUTMODE and also lets the client thread // have my data and potentially start generating new output based on it. // Actually I might worry a bit about what happens if another mouse or // keyboard event is triggered while I am busy around here... setInputData(s); } catch (BadLocationException e1) {} } } // The next action is ONLY used as a pending action, so it can only // ever be triggered when I am in input mode. class InsertPastedAction extends AbstractAction { InsertPastedAction() { super("insert-pasted-text"); } public void actionPerformed(ActionEvent e) { int dot = adjustSelection(); replaceSelection(""); try { String s = e.getActionCommand(); doc.insertString(dot, s, blueText); caret.setDot(dot+s.length()); } catch (BadLocationException e1) {} if (e.getModifiers() != 0) return; insertBreak.actionPerformed(e); } } class InsertContentAction extends AbstractAction { InsertContentAction() { super(StyledEditorKit.insertContentAction); } public void actionPerformed(ActionEvent e) { System.out.println("insertContent " + safe(e.getActionCommand()) + " " + safe(e.paramString())); // oldinsertContent.actionPerformed(e); } } class InsertTabAction extends AbstractAction { InsertTabAction() { super(StyledEditorKit.insertTabAction); } public void actionPerformed(ActionEvent e) { if (state != INPUTMODE) { deferAction(this, e); return; } int dot = adjustSelection(); replaceSelection(""); try { do { doc.insertString(dot++, " ", blueText); } while ((dot-afterPrompt)%8 != 0); } catch (BadLocationException e1) {} } } class PasteAction extends AbstractAction { PasteAction() { super(StyledEditorKit.pasteAction); } public void actionPerformed(ActionEvent e) { // Actually if I go PASTE when not in input mode I think I still need to // grab the stuff off the clipboard instantly. It is inserting it into // the buffer that gets delayed. Clipboard cb; Transferable tr; String s = null; try { cb = getToolkit().getSystemClipboard(); tr = cb.getContents(this); s = (String)tr.getTransferData(DataFlavor.stringFlavor); } catch (Exception e1) { s = clipboardSubstitute; } if (s == null || s.length() == 0) return; // No more to do! int p = 0, q; ActionEvent e2; String s1; while ((q = s.indexOf('\n', p)) >= 0) { s1 = s.substring(p, q); // up to but not including newline e2 = new ActionEvent(this, 0, s1); deferAction(insertPasted, e2); p = q+1; } s1 = s.substring(p, s.length()); if (s1.length() != 0) { e2 = new ActionEvent(this, 0, s1, 1); // 1 marks "no newline"! deferAction(insertPasted, e2); } if (state != INPUTMODE) return; performPendedActions(); } } class HistoryUpAction extends AbstractAction { HistoryUpAction() { super("history-up"); } public void actionPerformed(ActionEvent e) { if (state != INPUTMODE) { deferAction(this, e); return; } if (historyIndex > 0) { historyIndex--; String s = ""; try { s = (String)history.get(historyIndex); } catch (ArrayIndexOutOfBoundsException e1) {}; caret.setDot(afterPrompt); caret.moveDot(doc.getLength()); replaceSelection(""); try { doc.insertString(afterPrompt, s, blueText); caret.setDot(doc.getLength()); } catch (BadLocationException e1) {} } } } class HistoryDownAction extends AbstractAction { HistoryDownAction() { super("history-down"); } public void actionPerformed(ActionEvent e) { if (state != INPUTMODE) { deferAction(this, e); return; } int n = history.size(); if (historyIndex < n) { historyIndex++; String s = ""; try { if (historyIndex < n) s = (String)history.get(historyIndex); } catch (ArrayIndexOutOfBoundsException e1) {}; caret.setDot(afterPrompt); caret.moveDot(doc.getLength()); replaceSelection(""); try { doc.insertString(afterPrompt, s, blueText); caret.setDot(doc.getLength()); } catch (BadLocationException e1) {} } } } String safe(String a) { if (a == null) return "<null>"; String r = ""; for (int i=0; i<a.length(); i++) { char c = a.charAt(i); int ic = (int)c; if (32 <= ic && ic < 127) r = r + c; else r = r + "\\x" + Integer.toHexString(ic); } return r; } } // end of InputPane.java