// Chat Touring // Copyright (c) 1996 by Paul Burchard // Distributed under the terms of the GNU Library General Public License //////////////////////////////////////////////////////////////// // Please do not use this code as an example of anything. // // It is a nest of workarounds for bugs in AWT and Netscape, // // for AWT's horrible design for event and layout management, // // for Netscape/Sun's severely broken thread implementation, // // and for Java's OOP-hostile network loading strategy. // //////////////////////////////////////////////////////////////// import java.applet.*; import java.awt.*; import java.net.*; import java.util.*; import QueryTransaction; public class ChaTour extends Panel implements Observer { public static final String initString = "Welcome to Chat Touring by Paul Burchard\n"; public static final String infoString = "Chat Touring (c) 1996 by Paul Burchard\nDistributed under the terms of the GNU Library General Public License"; public static final String logoString = "Chat Touring"; // Parameters int readLine = 0, lookLine = 0; int showLines = 10; int fontSize = 12; public static final int LINE_LENGTH = 80; public static final int LINE_WIDTH = 600; int pauseTime = 1; String nickName = null, guideName = null; String tourDest = "help.html"; String cgiSuffix = ".cgi"; URL cgiBase = null; public static final String PARAM_PREFIX = "ChaTour."; public static final String IO_PARAM = "i"; public static final String SERIAL_PARAM = "z"; public static final String READLINE_PARAM = "r"; public static final String LOOKLINE_PARAM = "l"; public static final String SHOWLINES_PARAM = "s"; public static final String FONTSIZE_PARAM = "f"; public static final String PAUSETIME_PARAM = "p"; public static final String NICKNAME_PARAM = "n"; public static final String GUIDENAME_PARAM = "g"; public static final String TOURDEST_PARAM = "t"; public static final String CGISUFFIX_PARAM = "c"; public static final String CGIBASE_PARAM = "b"; // State QueryTransaction queryChain, pollChain; URL chatURL; Hashtable chatParams; int serial = 0; String nextDest = null, nextFrame = null, foundDest = null; boolean allowTeleport = false, nickSet = false; int card; private static final int CHAT_CARD = 0; private static final int SETTINGS_CARD = 1; // GUI Applet app; boolean inlineApplet = false; Panel chatCard, settingsCard; TextField inputText; TextArea outputText; Label logoLabel, settingsLabel; Teleporter haltButton; Button okButton, helpButton, settingsButton, exitButton; TextField nickText, guideText, pauseTimeText, fontSizeText; Font mainFont, logoFont; public static final int NONE = 0; public static final int ENDL = 1; public static final int HFIL = 2; public static final int VFIL = 4; // Polling for new messages Thread pollThread; ChatPoller poller; public ChaTour(Applet main) { this(main, false); } public ChaTour(Applet main, boolean isInline) { // Set up GUI. app = main; pauseTime = (isInline ? 4:1); cgiBase = app.getDocumentBase(); readParameters(app); createComponents(isInline); // Prepare chat query. try { chatURL = new URL(cgiBase, "update" + cgiSuffix); } catch(MalformedURLException e) { throw(new RuntimeException(logoString + " configuration error: bad chat server URL")); } chatParams = new Hashtable(); } protected void readParameters(Applet main) { // Load and validate parameters. app = main; try { int newReadLine = Integer.valueOf(main.getParameter(PARAM_PREFIX + READLINE_PARAM)).intValue(); if(newReadLine >= 0) readLine = newReadLine; } catch(Exception e) {} try { int newLookLine = Integer.valueOf(main.getParameter(PARAM_PREFIX + LOOKLINE_PARAM)).intValue(); if(newLookLine >= 0) lookLine = newLookLine; } catch(Exception e) {} if(lookLine < readLine) lookLine = readLine; try { int newShowLines = Integer.valueOf(main.getParameter(PARAM_PREFIX + SHOWLINES_PARAM)).intValue(); if(newShowLines>=1 && newShowLines<=100) showLines = newShowLines; } catch(Exception e) {} fontSize = app.getFont().getSize(); try { int newFontSize = Integer.valueOf(main.getParameter(PARAM_PREFIX + FONTSIZE_PARAM)).intValue(); if(newFontSize>=6 && newFontSize<=24) fontSize = newFontSize; } catch(Exception e) {} try { int newPauseTime = Integer.valueOf(main.getParameter(PARAM_PREFIX + PAUSETIME_PARAM)).intValue(); if(newPauseTime >= 0) pauseTime = newPauseTime; } catch(Exception e) {} String newNickName = main.getParameter(PARAM_PREFIX + NICKNAME_PARAM); if(newNickName != null) { nickName = newNickName; nickSet = true; allowTeleport = true; } String newGuideName = main.getParameter(PARAM_PREFIX + GUIDENAME_PARAM); if(newGuideName != null) guideName = newGuideName; String newTourDest = main.getParameter(PARAM_PREFIX + TOURDEST_PARAM); if(newTourDest != null) tourDest = newTourDest; String newCgiSuffix = main.getParameter(PARAM_PREFIX + CGISUFFIX_PARAM); if(newCgiSuffix != null) cgiSuffix = newCgiSuffix; try { URL newCgiBase = new URL(main.getParameter(PARAM_PREFIX + CGIBASE_PARAM)); if(newCgiBase != null) cgiBase = newCgiBase; } catch(Exception e) {} } protected void createComponents(boolean isInline) { // Lay out GUI. inlineApplet = isInline; setLayout(new CardLayout()); add("chat_card", (chatCard = new Panel())); { chatCard.setLayout(new GridBagLayout()); addComponent(chatCard, (logoLabel = new Label(logoString, Label.LEFT)), "logo_label", 0, 0, (inlineApplet ? 8:7), 1, HFIL); addComponent(chatCard, (helpButton = new Button("Help")), "help_button", (inlineApplet ? 8:7), 0, 1, 1, NONE); addComponent(chatCard, (settingsButton = new Button("Settings")), "settings_button", (inlineApplet ? 9:8), 0, 1, 1, (inlineApplet ? ENDL:NONE)); if(!inlineApplet) addComponent(chatCard, (exitButton = new Button("Exit")), "exit_button", 9, 0, 1, 1, ENDL); addComponent(chatCard, (outputText = new TextArea(showLines, LINE_LENGTH)), "output_text", 0, 1, 10, showLines+1, VFIL|HFIL|ENDL); { outputText.setEditable(false); outputText.setText(initString); } addComponent(chatCard, (inputText = new TextField(LINE_LENGTH - 6)), "input_text", 0, showLines+2, 9, 1, HFIL); { inputText.setEditable(true); } addComponent(chatCard, (haltButton = new Teleporter(this, pauseTime*1000)), "halt_button", 9, showLines+2, 1, 1, ENDL); } add("settings_card", (settingsCard = new Panel())); { settingsCard.setLayout(new GridBagLayout()); addComponent(settingsCard, (settingsLabel = new Label(logoString + " Settings", Label.CENTER)), "settings_label", 0, 0, 2, 1, HFIL|VFIL|ENDL); addComponent(settingsCard, (new Label("Your Nickname: ", Label.RIGHT)), "nick_label", 0, 1, 1, 1, HFIL); addComponent(settingsCard, (nickText = new TextField((nickName!=null ? nickName : "anonymous"))), "nick_text", 1, 1, 1, 1, HFIL|ENDL); addComponent(settingsCard, (new Label("Tour Guide: ", Label.RIGHT)), "guide_label", 0, 2, 1, 1, HFIL); addComponent(settingsCard, (guideText = new TextField((guideName!=null ? guideName : ""))), "guide_text", 1, 2, 1, 1, HFIL|ENDL); addComponent(settingsCard, (new Label("Font Size: ", Label.RIGHT)), "font_label", 0, 3, 1, 1, HFIL); addComponent(settingsCard, (fontSizeText = new TextField(String.valueOf(fontSize))), "font_text", 1, 3, 1, 1, HFIL|ENDL); addComponent(settingsCard, (new Label("Teleport Delay (seconds): ", Label.RIGHT)), "pause_label", 0, 4, 1, 1, HFIL); addComponent(settingsCard, (pauseTimeText = new TextField(String.valueOf(pauseTime))), "pause_text", 1, 4, 1, 1, HFIL|ENDL); addComponent(settingsCard, (okButton = new Button("GO!")), "ok_button", 0, 5, 2, 1, ENDL); } // Show correct initial "screen". if(nickName == null) { ((CardLayout)getLayout()).show(this, "settings_card"); layout(); nickText.selectAll(); nickText.requestFocus(); card = SETTINGS_CARD; } else { ((CardLayout)getLayout()).show(this, "chat_card"); layout(); inputText.requestFocus(); card = CHAT_CARD; } // Adjust font size per request. try { setFontSize(fontSize); } catch(Exception e) {} repaint(); } public static void addComponent(Container cont, Component comp, String key, int gridx, int gridy, int gridw, int gridh, int flags) { // Convenience method to sanitize use GridBagLayout. // Container must use GridBagLayout. GridBagConstraints cnst = new GridBagConstraints(); cnst.weightx = cnst.weighty = 0.0; cnst.gridx = gridx; cnst.gridy = gridy; cnst.gridwidth = gridw; cnst.gridheight = gridh; if((flags & ENDL) != 0) cnst.gridwidth = GridBagConstraints.REMAINDER; if((flags & HFIL) != 0) { if((flags & VFIL) != 0) { cnst.fill = GridBagConstraints.BOTH; cnst.weighty = 1.0; } else cnst.fill = GridBagConstraints.HORIZONTAL; cnst.weightx = cnst.weighty = 1.0; } else if((flags & VFIL) != 0) { cnst.fill = GridBagConstraints.VERTICAL; cnst.weighty = 1.0; } ((GridBagLayout)cont.getLayout()).setConstraints(comp, cnst); cont.add(key, comp); } protected synchronized void setFontSize(int sz) { if(sz<6 || sz>24) return; //!!! Netscape 2.0 doesn't appear to honor inline applet resize requests, //!!! so for now we teleport when changing the font size. if(inlineApplet && sz!=fontSize) { int oldSize = fontSize; fontSize = sz; if(tourDest == null) nextDest = "help.html"; else nextDest = tourDest; nextFrame = null; teleport(); fontSize = oldSize; return; } //!!! Manually force everything to re-lay itself out. So much for OOP... fontSize = sz; mainFont = app.getFont(); Font newFont = new Font(mainFont.getName(), mainFont.getStyle(), fontSize); if(newFont != null) { mainFont = newFont; logoFont = new Font(mainFont.getName(), Font.BOLD, fontSize+4); resize(preferredSize()); if(logoFont == null) logoFont = mainFont; inputText.setFont(mainFont); outputText.setFont(mainFont); logoLabel.setFont(logoFont); logoLabel.layout(); settingsLabel.setFont(logoFont); settingsLabel.layout(); haltButton.setFont(mainFont); haltButton.layout(); okButton.setFont(logoFont); okButton.layout(); helpButton.setFont(mainFont); helpButton.layout(); settingsButton.setFont(mainFont); settingsButton.layout(); if(exitButton != null) { exitButton.setFont(mainFont); exitButton.layout(); } nickText.setFont(mainFont); guideText.setFont(mainFont); pauseTimeText.setFont(mainFont); fontSizeText.setFont(mainFont); chatCard.layout(); settingsCard.layout(); Window frame; if(!inlineApplet && (frame = getWindow())!=null) { frame.reshape(frame.location().x, frame.location().y, frame.preferredSize().width, frame.preferredSize().height); frame.pack(); } repaint(); } } public Window getWindow() { Container win; for(win=this; win.getParent()!=null && win.getParent()!=win; win=win.getParent()); if(win instanceof Window) return ((Window)win); else return null; } public synchronized Dimension preferredSize() { // This is how much we allot ourselves in HTML. return new Dimension(LINE_WIDTH, (showLines + 3)*((fontSize*5)/4) + 50); } public synchronized Dimension minimumSize() { return preferredSize(); } public boolean mouseDown(Event e, int x, int y) { if(e.target == haltButton) return haltClick(); else return super.mouseDown(e, x, y); } protected boolean haltClick() { // Don't follow teleport request. teleportCancel(); inputText.requestFocus(); return true; } public boolean action(Event e, Object arg) { if(e.target == inputText) return inputAction(); else if(e.target == settingsButton) return settingsAction(); else if(e.target == okButton) return okAction(); else if(e.target == helpButton) return helpAction(); else if(e.target == exitButton) return exitAction(); else return super.action(e, arg); } protected boolean inputAction() { // Send a line to server and request an update. chat(inputText.getText()); inputText.setText(""); return true; } protected boolean settingsAction() { // Switch to settings panel. pauseTimeText.setText(String.valueOf(pauseTime)); fontSizeText.setText(String.valueOf(fontSize)); ((CardLayout)getLayout()).show(this, "settings_card"); layout(); card = SETTINGS_CARD; return true; } protected boolean okAction() { // Validate new settings. boolean ok = true; int newFontSize, newPauseTime; String newGuideName, newNickName; if((newFontSize = validateIntegerField(fontSizeText, 6, fontSize, 24)) < 6) ok = false; if((newPauseTime = validateIntegerField(pauseTimeText, 0, pauseTime, -1)) < 0) ok = false; if((newGuideName = validateNameField(guideText, 0, 16, (guideName!=null ? guideName : null))) == null) ok = false; if(newGuideName!=null && newGuideName.length()==0) newGuideName = null; if((newNickName = validateNameField(nickText, 1, 16, (nickName!=null ? nickName : "anonymous"))) == null) ok = false; // If OK, commit to new settings. if(ok) { boolean nickChange = !newNickName.equals(nickName); if(nickSet && nickChange && pollThread!=null) chat(""); guideName = newGuideName; nickName = newNickName; haltButton.setDelay(1000*(pauseTime = newPauseTime)); // Attempt to set font size. //!!! Note: this operation may teleport in current implementation, so do this setting last. if(newFontSize != fontSize) { allowTeleport = true; try { setFontSize(newFontSize); } catch(Exception exc) {} } // Switch back to chat panel. if(!nickSet) { nickSet = true; allowTeleport = true; if(pollThread != null) chat(""); } ((CardLayout)getLayout()).show(this, "chat_card"); layout(); card = CHAT_CARD; inputText.requestFocus(); } return true; } protected boolean helpAction() { boolean ok = true; try { app.getAppletContext().showDocument(new URL(app.getDocumentBase(), "help.html"), "ChaTour.frame.help"); } catch(MalformedURLException e) { ok = false; } return ok; } protected boolean exitAction() { Window frame; if(!inlineApplet && (frame = getWindow())!=null) frame.dispose(); app.destroy(); return true; } protected int validateIntegerField(TextField field, int minValue, int defaultValue, int maxValue) { // If field is out of range, returns value < minValue, // resets the field text to defaultValue, and selects it. // The maxValue will be ignored if < minValue. int newValue = defaultValue; try { newValue = Integer.valueOf(field.getText()).intValue(); } catch(NumberFormatException e) { newValue = minValue - 1; } if(newValue=minValue && newValue>maxValue)) { field.setText(String.valueOf(defaultValue)); field.selectAll(); field.requestFocus(); newValue = minValue - 1; } return newValue; } protected String validateNameField(TextField field, int minLength, int maxLength, String defaultValue) { // If field gives invalid "name", this returns null, // resets the field text to defaultValue (or to "", if // defaultValue is null), and selects it. // The maxLength will be ignored if < minLength. String newValue = field.getText().trim(); newValue = newValue.substring(0, Math.min(maxLength, newValue.length())); char[] valBuf = newValue.toCharArray(); int n = newValue.length(); int chrs = 0; for(int i=0; i= minLength) { newValue = new String(valBuf); } else { field.setText(defaultValue!=null ? defaultValue : ""); field.selectAll(); field.requestFocus(); newValue = null; } return newValue; } public boolean postEvent(Event e) { boolean ok = super.postEvent(e); if(e.id == Event.MOUSE_ENTER) { if(card == CHAT_CARD) inputText.requestFocus(); else if(card == SETTINGS_CARD) { if(nickName==null && nickText.getSelectionStart()==0 && nickText.getSelectionEnd()>0) nickText.requestFocus(); } } return ok; } public void start() { pollThread = new Thread(poller = new ChatPoller(this, 5000)); pollThread.setPriority(Thread.NORM_PRIORITY+1); pollThread.start(); if(nickSet) { allowTeleport = true; chat(""); } } public void stop() { if(pollThread != null) { if(nickSet) { chat(""); allowTeleport = false; } pollThread.stop(); pollThread = null; } } public void chat() { chat((String)null); } private synchronized void chat(String input) { chatParams.put(READLINE_PARAM, String.valueOf(readLine)); chatParams.put(SERIAL_PARAM, String.valueOf(serial++)); chatParams.put(SHOWLINES_PARAM, String.valueOf(showLines)); chatParams.put(NICKNAME_PARAM, (nickName!=null ? nickName : "anonymous")); if(input != null) chatParams.put(IO_PARAM, input); else chatParams.remove(IO_PARAM); if(input != null) (queryChain = new QueryTransaction(queryChain, chatURL, chatParams)).start((Observer)this); else if(pollChain==null || pollChain.isComplete()) (pollChain = new QueryTransaction(null, chatURL, chatParams)).start((Observer)this); } synchronized void appendText(String s) { if(outputText == null) return; outputText.insertText(s, outputText.getText().length()); int newlen = outputText.getText().length(); // Force outputText to scroll to end by selecting there. outputText.select(newlen, newlen); } public void chatFilter(String output, int lineNum) { // Display wrapped text, and filter the text for URLs. // // Text filter assumes line format decoded by the Perl statement: // (time, nick, text) = m!^([0-9:]*)[ ]([^:]*):[ ](.*)$!; // // Line wrap assumes fixed-width font for now...not clear // how expensive stringWidth() is. // int lineLength = outputText.getColumns(); if(outputText.isShowing()) { int charWidth = outputText.getFontMetrics(outputText.getFont()).charWidth('x'); if(charWidth > 0) lineLength = LINE_WIDTH/charWidth; else lineLength = outputText.getColumns(); } int nickStart = 1 + output.indexOf(' '); if(nickStart < 1) return; int textStart = 2 + output.substring(nickStart).indexOf(':'); if(textStart < 2) return; else textStart += nickStart; String headerText = output.substring(0, textStart); boolean foundGuide = false; if(guideName!=null && headerText.substring(nickStart).startsWith(guideName+":")) foundGuide = true; StringTokenizer toks = new StringTokenizer(output.substring(textStart), " \n\t\r<>\"\'", true); StringBuffer textLine = new StringBuffer(); textLine.append(headerText); while(toks.hasMoreTokens()) { String t = toks.nextToken(); if(lineNum>=lookLine && (guideName==null || foundGuide) && (t.indexOf("://")>=0 || t.startsWith("news:") || t.startsWith("mailto:") || t.startsWith("URL:") || t.startsWith("url:"))) { String u = t; if(u.startsWith("URL:") || u.startsWith("url:")) { u = u.substring("URL:".length()); } try { URL url = new URL(u); } catch(MalformedURLException e) { u = null; } if(u != null) foundDest = u; } if((t.length() + textLine.length())>=lineLength && (t.length() + headerText.length()) 0) { if(readLine <= 0) readLine = startLine; if(lookLine <= 0) lookLine = startLine + numLines; while(startLine readLine) { //!!! Out-of-order server response. //!!! Discard and wait for earlier responses to catch up (this wastes bandwidth). } else if(startLine == readLine) { foundDest = null; do { try { textLine = textLine.substring(1+textLine.indexOf('/')); } catch(IndexOutOfBoundsException ee) {} textLine = QueryTransaction.urlDecodeText(textLine); chatFilter(textLine, readLine++); } while(output.hasMoreTokens() && (textLine = output.nextToken())!=null); if(lookLine < readLine) lookLine = readLine; if(foundDest != null) { teleportRequest(foundDest, "ChaTour.frame.tour"); foundDest = null; return; } // Poll fast for a while if we actually got something. if(poller != null) poller.resetPollInterval(); } } } } else if(arg instanceof Exception) { appendText("!!!ERROR: "+((Exception)arg).getMessage()+"\n"); } } } public synchronized void teleportRequest(String address, String frame) { if(allowTeleport && address!=null) { nextDest = address; nextFrame = frame; appendText(">>>TELEPORTING: \"" + address + "\"\n"); haltButton.setWarning(true); } } public synchronized void teleportCancel() { haltButton.setWarning(false); if(nextDest != null) { appendText("<< 60000) pollInterval = 60000; } } } class Teleporter extends Canvas implements Runnable { ChaTour chatter; long delayInterval, accumInterval; int state; static final long switchInterval = 650; // Don't make too small, or we will hang in repaint() due to Netscape threading bugs. int fontSize; public Teleporter(ChaTour callback, long msec) { chatter = callback; delayInterval = msec; state = 0; fontSize = 12; } public synchronized void setWarning(boolean yn) { if(!yn) { state = 0; } else if(state == 0) { state = 1; Thread waiter = new Thread(this); waiter.setPriority(Thread.MAX_PRIORITY); waiter.start(); } accumInterval = 0; } public synchronized void setFont(Font f) { fontSize = f.getSize(); resize(4*fontSize, 2*fontSize); repaint(); } public synchronized void setDelay(long msec) { delayInterval = msec; } public synchronized Dimension minimumSize() { return new Dimension(4*fontSize, 2*fontSize); } public synchronized void run() { for(accumInterval=0; accumInterval 0) { g.setColor(Color.red); g.fillOval(0, 0, h, h); } else { g.setColor(Color.red); g.fillOval(w - h, 0, h, h); } } }