Vassal 3.3 (or 3.31?) improved chatter submission

Hi Folks,

I posted about this over in the Design forum for people to add to their modules, but I’m going to go out on a limb and suggest that it’s actually versatile, useful, and modular enough to be potentially included in Vassal 3.3 (or a followon 3.31), as replaces only a single class.

Demo module is here: vassalengine.org/mediawiki/i … tDemo.vmod

(But you can see it in a “real” module in Paths of Glory 9.7 and For the People 3.2)

It turns the chat/log window into a fully HTML-compliant HTMLEditorKit pane. This means that bold/italic/underline font styles are available through easy inline tags e.g. etc in all “report” log messages generated by a module. Full CSS inline styling is actually available too via tags, etc, which is how I made the little die-roll icons when you roll the dice.

The version in the demo module is of course a “custom class” dropin, but it would only need the most minor of adjustments to “become” the real Vassal chatter.

[attachment=0]htmlchat.png[/attachment]

I realize the whole Java version of VASSAL is due to “sunset”, but I figure we’re likely to be using it for at least a year if not several years while Vassal 4 comes fully up to speed (and then modules getting converted over is likely to be a “process” as well).

So my points in favor of inclusion are:
(1) Replaces ONLY the chatter class, so does not reach deeply into Vassal innards
(2) Chat features are used by MANY players, and “report” traits by MOST modules – this would be broadly useful.
(3) Gives even beginner module designers a way to make their modules stand out w/ bold & colored text. Tagging text is WAY easier than most module design.
(4) Builds on the universal standard of HTML – the window becomes way better and more useful.
(5) aaaaaaaaaaannnd, as a longtime UI nut in my day job, I’m strongly hoping to at least push Vassal 4 in the direction of having an HTML compliant chat/log window!

No pressure, I’m gonna put it in all MY modules anyway, but it does seem to me like something that might be generically useful to many module designers.

Best,

Brian

Realized belatedly I forgot to say – source java file is tucked into the demo module. Happy to provide by any other means of course.

/*
 * $Id: Chatter.java 8952 2013-11-27 23:26:33Z uckelman $
 *
 * Copyright (c) 2000-2003 by Rodney Kinney
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License (LGPL) as published by the Free Software Foundation.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, copies are available
 * at http://www.opensource.org.
 */
package HTMLChatter;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.KeyEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.IOException;
import java.net.URL;

import javax.swing.BoxLayout;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.JTextPane;
import javax.swing.KeyStroke;
import javax.swing.plaf.basic.BasicTextAreaUI;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.Element;
import javax.swing.text.JTextComponent;
import javax.swing.text.PlainView;
import javax.swing.text.Segment;
import javax.swing.text.TabExpander;
import javax.swing.text.Utilities;
import javax.swing.text.View;
import javax.swing.text.WrappedPlainView;
import javax.swing.text.html.HTMLDocument;
import javax.swing.text.html.*;
import javax.swing.text.*;

import VASSAL.build.Buildable;
import VASSAL.build.GameModule;
import VASSAL.build.module.GlobalOptions; //BR//
import VASSAL.command.Command;
import VASSAL.command.CommandEncoder;
import VASSAL.configure.ColorConfigurer;
import VASSAL.configure.FontConfigurer;
import VASSAL.i18n.Resources;
import VASSAL.preferences.Prefs;
import VASSAL.tools.ErrorDialog;
import VASSAL.tools.KeyStrokeSource;
import VASSAL.tools.ScrollPane;
import net.sf.wraplog.Logger;
import VASSAL.tools.ComponentSplitter;
import VASSAL.tools.ComponentSplitter.SplitPane;
import VASSAL.tools.DataArchive;

//BR// Drop-in replacement Vassal Chatter - provides full HTML support for the chat window. In addition to allowing multiple colors/fonts, and images,
//BR// allows module creators to send <b>bold</b> and <i>italic</i> tags in Reports, along with tags for images, text styling, etc.  

/**
 * The chat window component. Displays text messages and accepts input. Also
 * acts as a {@link CommandEncoder}, encoding/decoding commands that display
 * message in the text area
 */
public class HTMLChatter extends VASSAL.build.module.Chatter implements CommandEncoder, Buildable {
	private static final long serialVersionUID = 1L;

	protected JTextPane conversation;
	protected HTMLDocument doc;
	protected HTMLEditorKit kit;
	protected StyleSheet style;

	protected JTextField input;
	protected JScrollPane scroll = new ScrollPane(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
			JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
	protected JScrollPane scroll2 = new ScrollPane(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
			JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
	protected static final String MY_CHAT_COLOR    = "HTMLChatColor";          //$NON-NLS-1$ //BR// Different tags to "restart" w/ new default scheme
	protected static final String OTHER_CHAT_COLOR = "HTMLotherChatColor";     //$NON-NLS-1$
	protected static final String GAME_MSG1_COLOR  = "HTMLgameMessage1Color";  //$NON-NLS-1$
	protected static final String GAME_MSG2_COLOR  = "HTMLgameMessage2Color";  //$NON-NLS-1$ //BR//
	protected static final String GAME_MSG3_COLOR  = "HTMLgameMessage3Color";  //$NON-NLS-1$ //BR//
	protected static final String GAME_MSG4_COLOR  = "HTMLgameMessage4Color";  //$NON-NLS-1$ //BR//
	protected static final String GAME_MSG5_COLOR  = "HTMLgameMessage5Color";  //$NON-NLS-1$ //BR//
	protected static final String SYS_MSG_COLOR    = "HTMLsystemMessageColor"; //$NON-NLS-1$

	protected Font myFont;

	protected Color gameMsg, gameMsg2, gameMsg3, gameMsg4, gameMsg5;
	protected Color systemMsg, myChat, otherChat;


	public HTMLChatter() {		
		setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));

		//BR// Conversation is now a JTextPane w/ HTMLEditorKit to process HTML.
		conversation = new JTextPane();
		conversation.setPreferredSize(new Dimension(500, 120));
		conversation.setContentType("text/html");
		doc = (HTMLDocument) conversation.getDocument();
		kit = (HTMLEditorKit) conversation.getEditorKit();

		style = kit.getStyleSheet();
		myFont = new Font("SansSerif", Font.PLAIN, 12); //BR// Will be overridden by the font from Chat preferences

		for (int i = 0; i < 15; ++i) {
			try {
				kit.insertHTML(doc, doc.getLength(), "<br>", 0, 0, null);
			} catch (BadLocationException ble) {
				ErrorDialog.bug(ble);
			} catch (IOException ex) {
				ErrorDialog.bug(ex);
			}
		}

		conversation.setEditable(false);
		conversation.addComponentListener(new ComponentAdapter() {
			public void componentResized(ComponentEvent e) {
				scroll.getVerticalScrollBar().setValue(scroll.getVerticalScrollBar().getMaximum());
			}
		});

		input = new JTextField(60);
		input.setFocusTraversalKeysEnabled(false);
		input.addActionListener(new ActionListener() {
			public void actionPerformed(ActionEvent e) {
				send(formatChat(e.getActionCommand()));
				input.setText(""); //$NON-NLS-1$
			}
		});
		input.setMaximumSize(new Dimension(input.getMaximumSize().width, input.getPreferredSize().height));

		conversation.setPreferredSize(new Dimension(input.getMaximumSize().width, input.getPreferredSize().height * 11));

		scroll.setViewportView(conversation);
		scroll.getVerticalScrollBar().setUnitIncrement(input.getPreferredSize().height); // BR// Scroll this faster																									
		add(scroll);
		add(input);
		
		//BR// This turns off the default Vassal chatter window, since the chatter's
		//BR// *constructor* can't be overridden. Not needed if this were dropped in AS Vassal's chatter.
		super.conversation.setVisible(false);
		super.input.setVisible(false);
		super.scroll.setVisible(false);				
	}

	private String formatChat(String text) {
		final String id = GlobalOptions.getInstance().getPlayerId();
		return "&lt;" + (id.length() == 0 ? "(" + getAnonymousUserName() + ")" : id) + "&gt; - " + text; //$NON-NLS-1$ //$NON-NLS-2$ //BR//HTML-friendly angle brackets
	}

	public JTextField getInputField() {
		return input;
	}

	/**
	 * Display a message in the text area
	 */
	public void show(String s) {
		String style;

		//BR// Choose an appropriate style to display this message in
		s = s.trim();
		if (s.length() > 0) {
			if (s.startsWith("*")) {
				//BR// Here we just extend the convention of looking at first characters, this time to the second character.
				//BR// ! = msg2
				//BR// ? = msg3
				//BR// ~ = msg4
				//BR// These characters can be pre-pended to Report messages to produce the color changes. The characters themselves are removed before display.
				//BR// Reports can also include <b></b> tags for bold and <i></i> for italic.
				if (s.startsWith("* !") || s.startsWith("*!")) {
					style = "msg2";
					s = s.replaceFirst("!", "");
				} else if (s.startsWith("* ?") || s.startsWith("*?")) {
					style = "msg3";
					s = s.replaceFirst("\\?", "");
				} else if (s.startsWith("* ~") || s.startsWith("*~")) {
					style = "msg4";
					s = s.replaceFirst("~", "");
				} else if (s.startsWith("* `") || s.startsWith("*`")) {
					style = "msg5";
					s = s.replaceFirst("`", "");
				} else {
					style = "msg";
				}
			} else if (s.startsWith("-")) {
				style = "sys";
			} else {
				//BR// Ideally we'd actually be passing in a player-side or player-number so that we'd have the option of 
				//BR// having e.g. Union player in Blue, Confederate Player in Grey, and Referee in Green, etc. But that would
				//BR// involve intercepting stuff in other java files, and I'm trying for a one-class-replacement feature here.
				if (s.startsWith(formatChat("").trim())) { //$NON-NLS-1$
					style = "mychat";
				} else {
					style = "other";
				}
			}
		} else {
			style = "msg";
		}

		//BR// Systematically search through for html image tags. When we find one, try
		//BR// to match it with an image from our DataArchive, and substitute the correct
		//BR// fully qualified URL into the tag.
		
		//BR// There are without a doubt "more efficient ways to do this", but this loop
		//BR// miraculously ran perfectly the very first time I compiled it, so superstition...
		
		URL url;
		String keystring = "<img src=\"";
		String file, tag, replace;
		int base;
		while (s.toLowerCase().contains(keystring)) { // Find next key (to-lower so we're not case sensitive)
			base = s.toLowerCase().indexOf(keystring);
			file = s.substring(base + keystring.length(), s.length()).split("\"")[0]; // Pull the filename out from between the quotes
    	    tag  = s.substring(base, base + keystring.length()) + file + "\""; // Reconstruct the part of the tag we want to remove, leaving all attributes after the filename alone, and properly matching the upper/lowercase of the keystring						
			
			try {
				url = GameModule.getGameModule().getDataArchive().getURL("images/" + file);
				replace = "<img  src=\"" + url.toString() + "\""; // Fully qualified URL if we are successful. The extra
																	// space between IMG and SRC in the processed
																	// version ensures we don't re-find THIS tag as we
																	// iterate.
			} catch (IOException ex) {
				replace = "<img  src=\"" + file + "\""; // Or just leave in except alter just enough that we won't find
														// this tag again.
			}

			if (s.contains(tag)) {
				s = s.replaceFirst(tag, replace); // Swap in our new URL-laden tag for the old one.
			} else {
				break; // BR// If something went wrong in matching up the tag, don't loop forever
			}
		}
		
		//BR// Now we have to fix up any legacy angle brackets around the word <observer>
	    keystring = Resources.getString("PlayerRoster.observer");
	    replace   = keystring.replace("<","&lt;");
	    replace   = replace.replace(">","&gt;");
	    if (replace != keystring) {
	    	s = s.replace(keystring, replace);
	    }	   
	    
        //if (s.contains("/force chatter") && !s.equals("other")) {        	
        //  final ComponentSplitter splitter = new ComponentSplitter();        	
        //  ComponentSplitter.SplitPane pane;
        //  pane = (SplitPane) splitter.getSplitAncestor(GameModule.getGameModule().getControlPanel(), -1);
        //  pane.setDividerLocation(160);
        //  return;	      
        //}
	    
	    //BR// Insert a div of the correct style for our line of text. Module designer
		//BR//still free to insert <span> tags and <img> tags and the like in Report
		//BR//messages.
		try {
			kit.insertHTML(doc, doc.getLength(), "\n<div class=" + style + ">" + s + "</div>", 0, 0, null);
		} catch (BadLocationException ble) {
			ErrorDialog.bug(ble);
		} catch (IOException ex) {
			ErrorDialog.bug(ex);
		}
		conversation.update(conversation.getGraphics()); //BR// Force graphics to update
	}

	/** @deprecated use GlobalOptions.getPlayerId() */
	@Deprecated
	public void setHandle(String s) {
	}

	/** @deprecated use GlobalOptions.getPlayerId() */
	@Deprecated
	public String getHandle() {
		return GlobalOptions.getInstance().getPlayerId();
	}

	//BR// Adds or updates a CSS stylesheet entry. Styles in the color, font type, and font size.
	private void addStyle(String s, Font f, Color c, String font_weight, int size) {
		if ((style == null) || (c == null)) return;
		style.addRule(s + " {color:" + String.format("#%02x%02x%02x", c.getRed(), c.getGreen(), c.getBlue())
				        + "; font-family:" + f.getFamily() + "; font-size:" + (size > 0 ? size : f.getSize()) + "; " + ((font_weight != "") ? "font-weight:" + font_weight + "; " : "") + "}");
	}

	//BR// Build ourselves a CSS stylesheet from our preference font/color settings.
	private void makeStyleSheet(Font f) {
		if (style != null) {
			if (f == null) {
				if (myFont == null) {
					f = new Font("SansSerif", 0, 12);
					myFont = f;
				} else {
					f = myFont;
				}
			}
			addStyle("body",    f, gameMsg,   "",     0);
			addStyle("p",       f, gameMsg,   "",     0);
			addStyle(".msg",    f, gameMsg,   "",     0);
			addStyle(".msg2",   f, gameMsg2,  "",     0);
			addStyle(".msg3",   f, gameMsg3,  "",     0);
			addStyle(".msg4",   f, gameMsg4,  "",     0);
			addStyle(".msg5",   f, gameMsg5,  "",     0);
			addStyle(".mychat", f, myChat,    "bold", 0);
			addStyle(".other ", f, otherChat, "bold", 0);
			addStyle(".sys",    f, systemMsg, "",     0);
			
			//BR// A fun extension would be letting the module designer provide extra class styles. 
		}
	}

	/**
	 * Set the Font used by the text area
	 */
	public void setFont(Font f) {
		myFont = f;
		if (input != null) {
			if (input.getText().length() == 0) {
				input.setText("XXX"); //$NON-NLS-1$
				input.setFont(f);
				input.setText(""); //$NON-NLS-1$
			} else
				input.setFont(f);
		}
		if (conversation != null) {
			conversation.setFont(f);
		}
		makeStyleSheet(f); // BR// When font changes, rebuild our stylesheet
	}

	public void build(org.w3c.dom.Element e) {
	}

	public org.w3c.dom.Element getBuildElement(org.w3c.dom.Document doc) {
		return doc.createElement(getClass().getName());
	}

	/**
	 * Expects to be added to a GameModule. Adds itself to the controls window and
	 * registers itself as a {@link CommandEncoder}
	 */
	public void addTo(Buildable b) {
		GameModule mod = (GameModule) b;
		mod.setChatter(this);
		mod.addCommandEncoder(this);
		mod.addKeyStrokeSource(new KeyStrokeSource(this, WHEN_ANCESTOR_OF_FOCUSED_COMPONENT));

		final FontConfigurer chatFont = new FontConfigurer("ChatFont",
				Resources.getString("Chatter.chat_font_preference"));

		chatFont.addPropertyChangeListener(new PropertyChangeListener() {
			public void propertyChange(PropertyChangeEvent evt) {
				setFont((Font) evt.getNewValue());
			}
		});

		mod.getControlPanel().add(this, BorderLayout.CENTER);

		chatFont.fireUpdate();
		mod.getPrefs().addOption(Resources.getString("Chatter.chat_window"), chatFont); //$NON-NLS-1$

		// Bug 10179 - Do not re-read Chat colors each time the Chat Window is
		// repainted.
		final Prefs globalPrefs = Prefs.getGlobalPrefs();

		//
		// game message color
		//
		final ColorConfigurer gameMsgColor = new ColorConfigurer(GAME_MSG_COLOR,
				Resources.getString("Chatter.game_messages_preference") + "(#1 - standard messages):  ", Color.black);

		gameMsgColor.addPropertyChangeListener(new PropertyChangeListener() {
			public void propertyChange(PropertyChangeEvent e) {
				gameMsg = (Color) e.getNewValue();
				makeStyleSheet(null);
			}
		});

		globalPrefs.addOption(Resources.getString("Chatter.chat_window"), gameMsgColor);

		gameMsg = (Color) globalPrefs.getValue(GAME_MSG_COLOR);

		//BR//
		//BR// game message color #2 (messages starting with "!") 
		//BR//
		final ColorConfigurer gameMsg2Color = new ColorConfigurer(GAME_MSG2_COLOR,
				Resources.getString("Chatter.game_messages_preference") + "(#2 - first character \"!\"):  ", new Color(0, 153, 51));

		gameMsg2Color.addPropertyChangeListener(new PropertyChangeListener() {
			public void propertyChange(PropertyChangeEvent e) {
				gameMsg2 = (Color) e.getNewValue();
				makeStyleSheet(null);
			}
		});

		globalPrefs.addOption(Resources.getString("Chatter.chat_window"), gameMsg2Color);

		gameMsg2 = (Color) globalPrefs.getValue(GAME_MSG2_COLOR);

		//BR//
		//BR// game message color #3 (messages starting with "?")
		//BR//
		final ColorConfigurer gameMsg3Color = new ColorConfigurer(GAME_MSG3_COLOR,
				Resources.getString("Chatter.game_messages_preference") + "(#3 - first character \"?\"):  ", new Color(255, 102, 102));

		gameMsg3Color.addPropertyChangeListener(new PropertyChangeListener() {
			public void propertyChange(PropertyChangeEvent e) {
				gameMsg3 = (Color) e.getNewValue();
				makeStyleSheet(null);
			}
		});

		globalPrefs.addOption(Resources.getString("Chatter.chat_window"), gameMsg3Color);

		gameMsg3 = (Color) globalPrefs.getValue(GAME_MSG3_COLOR);
		
		
		//BR//
		//BR// game message color #4 (messages starting with "~")
		//BR//
		final ColorConfigurer gameMsg4Color = new ColorConfigurer(GAME_MSG4_COLOR,
				Resources.getString("Chatter.game_messages_preference") + "(#4 - first character \"~\"):  ", new Color(255, 0, 0));

		gameMsg4Color.addPropertyChangeListener(new PropertyChangeListener() {
			public void propertyChange(PropertyChangeEvent e) {
				gameMsg4 = (Color) e.getNewValue();
				makeStyleSheet(null);
			}
		});

		globalPrefs.addOption(Resources.getString("Chatter.chat_window"), gameMsg4Color);

		gameMsg4 = (Color) globalPrefs.getValue(GAME_MSG4_COLOR);
		
		
		//BR//
		//BR// game message color #4 (messages starting with "`")
		//BR//
		final ColorConfigurer gameMsg5Color = new ColorConfigurer(GAME_MSG5_COLOR,
				Resources.getString("Chatter.game_messages_preference") + "(#5 - first character \"`\"):  ", new Color(153, 0, 153));

		gameMsg5Color.addPropertyChangeListener(new PropertyChangeListener() {
			public void propertyChange(PropertyChangeEvent e) {
				gameMsg5 = (Color) e.getNewValue();
				makeStyleSheet(null);
			}
		});

		globalPrefs.addOption(Resources.getString("Chatter.chat_window"), gameMsg5Color);

		gameMsg5 = (Color) globalPrefs.getValue(GAME_MSG5_COLOR);



		//
		// system message color
		//
		final ColorConfigurer systemMsgColor = new ColorConfigurer(SYS_MSG_COLOR,
				Resources.getString("Chatter.system_message_preference"), new Color(160, 160, 160));

		systemMsgColor.addPropertyChangeListener(new PropertyChangeListener() {
			public void propertyChange(PropertyChangeEvent e) {
				systemMsg = (Color) e.getNewValue();
				makeStyleSheet(null);
			}
		});

		globalPrefs.addOption(Resources.getString("Chatter.chat_window"), systemMsgColor);

		systemMsg = (Color) globalPrefs.getValue(SYS_MSG_COLOR);


		//
		// my message color
		//
		final ColorConfigurer myChatColor = new ColorConfigurer( MY_CHAT_COLOR,
		Resources.getString("Chatter.my_text_preference"), new Color(9, 32, 229) );
		  
		myChatColor.addPropertyChangeListener(new PropertyChangeListener() { 
			public void propertyChange(PropertyChangeEvent e) { 
				myChat = (Color) e.getNewValue(); 
				makeStyleSheet(null); 
			} 
		});
		  
		globalPrefs.addOption( Resources.getString("Chatter.chat_window"),
		myChatColor );
		  
		myChat = (Color) globalPrefs.getValue(MY_CHAT_COLOR);		 

		//
		// other message color
		//
		
		final ColorConfigurer otherChatColor = new ColorConfigurer( OTHER_CHAT_COLOR,
		Resources.getString("Chatter.other_text_preference"), new Color (0,153,255) );
		  
		otherChatColor.addPropertyChangeListener(new PropertyChangeListener() {
			public void propertyChange(PropertyChangeEvent e) { 
				otherChat = (Color) e.getNewValue(); 
				makeStyleSheet(null); 
			} 
		});
		  
		globalPrefs.addOption( Resources.getString("Chatter.chat_window"), otherChatColor );		  
		otherChat = (Color) globalPrefs.getValue(OTHER_CHAT_COLOR);
		 

		makeStyleSheet(myFont);
	}

	public void add(Buildable b) {
	}

	public Command decode(String s) {
		if (s.startsWith("CHAT")) { //$NON-NLS-1$
			return new DisplayText(this, s.substring(4));
		} else {
			return null;
		}
	}

	public String encode(Command c) {
		if (c instanceof DisplayText) {
			return "CHAT" + ((DisplayText) c).msg; //$NON-NLS-1$
		} else {
			return null;
		}
	}

	/**
	 * Displays the message, Also logs and sends to the server a {@link Command}
	 * that displays this message
	 */
	public void send(String msg) {
		if (msg != null && msg.length() > 0) {
			show(msg);
			GameModule.getGameModule().sendAndLog(new DisplayText(this, msg));
		}
	}

	/**
	 * Classes other than the Chatter itself may forward KeyEvents to the Chatter by
	 * using this method
	 */
	public void keyCommand(KeyStroke e) {
		if ((e.getKeyCode() == 0 || e.getKeyCode() == KeyEvent.CHAR_UNDEFINED)
				&& !Character.isISOControl(e.getKeyChar())) {
			input.setText(input.getText() + e.getKeyChar());
		} else if (e.isOnKeyRelease()) {
			switch (e.getKeyCode()) {
			case KeyEvent.VK_ENTER:
				if (input.getText().length() > 0)
					send(formatChat(input.getText()));
				input.setText(""); //$NON-NLS-1$
				break;
			case KeyEvent.VK_BACK_SPACE:
			case KeyEvent.VK_DELETE:
				String s = input.getText();
				if (s.length() > 0)
					input.setText(s.substring(0, s.length() - 1));
				break;
			}
		}
	}

	/**
	 * This is a {@link Command} object that, when executed, displays a text message
	 * in the Chatter's text area
	 */
	public static class DisplayText extends Command {
		private String msg;
		private HTMLChatter c;

		public DisplayText(HTMLChatter c, String s) {
			this.c = c;
			msg = s;
			if (msg.startsWith("<>")) {
				msg = "&lt;(" + HTMLChatter.getAnonymousUserName() + ")&gt;" + s.substring(2); // BR// HTML-friendly
																								// angle brackets
			} else {
				msg = s;
			}
		}

		public void executeCommand() {
			c.show(msg);
		}

		public Command myUndoCommand() {
			return new DisplayText(c, Resources.getString("Chatter.undo_message", msg)); //$NON-NLS-1$
		}

		public String getMessage() {
			return msg;
		}

		public String getDetails() {
			return msg;
		}
	}

	public static void main(String[] args) {
		HTMLChatter chat = new HTMLChatter();
		JFrame f = new JFrame();
		f.add(chat);
		f.pack();
		f.setVisible(true);
	}
}

Thus spake Cattlesquat:

Hi Folks,

I posted about this over in the Design forum for people to add to their
modules, but I’m going to go out on a limb and suggest that it’s
actually versatile, useful, and modular enough to be potentially
included in Vassal 3.3 (or a followon 3.31), as replaces only a single
class.

Demo module is here: vassalengine.org/mediawiki/i
tDemo.vmod[1]

(But you can see it in a “real” module in Paths of Glory 9.7 and For the
People 3.2)

It turns the chat/log window into a fully HTML-compliant HTMLEditorKit
pane. This means that bold/italic/underline font styles are available
through easy inline tags e.g. etc in all “report” log
messages generated by a module. Full CSS inline styling is actually
available too via tags, etc, which is how I made the little
die-roll icons when you roll the dice.

I’ve had a chance to look at this now and I have some questions.

  • Are we introducing a sercurity issue by letting one user send another
    user arbitrary HTML + CSS? (My suspicion here is: Yes.)

  • Would markdown be more user-friendly? (My feeling here is that it’s
    far nicer to use when typing in a chat pane. Ask anyone who uses Slack,
    StackOverflow, or anything else which handles formatting using markdown
    if they’d rather use HTML and the are likely to look at you like you
    have two heads.)

  • How tolerant is this of text which looks like malformed HTML, but isn’t
    supposed to be HTML at all? E.g., I could imagine wanting to type less
    than or greater than symbols and not get mangled HTML, and not have to
    type ≶ instead.


J.

Thanks for having a look - my thoughts below:

It would be simple enough to strip any tags out of incoming chat, and I think you’re right it would be a good idea. Given we’re talking about HTML3.2 with no browser hyperlinks, it is probably more about “griefing” than actual security (people send me arbitrary HTML code all the time, haha), but nonetheless - should strip the tags. We’d just read all incoming chat lines and turn every < into a < (and I suppose every > into a > though I don’t think that’s actually necessary).

Because IMHO the key stuff w/ bold/italic/colors/images is letting the Report strings of the module have access to them (for a much more user-friendly report log experience), not making lots of stuff available to individual users on the chat line. I do think offering e.g. emojis and stuff would be a great stretch goal for a future version, but honestly that’s probably more appropriately a good Vassal 4 thing.

But bold/italic/multicolors (and dice images) for the Report Log I feel like is a huge leap forward for THAT part of the platform (I see changes to victory points in red, major events in bold or a special color, and so they stand out from all the generic stuff that spams up the report log and makes it easy to miss things).

There’s definitely nothing wrong with markdown for chat, I’m just not aware of a way we get it “effectively effortlessly” the way we can get the HTML with the HTMLEditorKit already built into Java – super-low-maintenance since it’s already supported.

So I’d think markdown might be something to look into for future versions, whether future versions of this or eventually Vassal 4.

The one thing HTML will still be super-useful for, though (so we might want to consider running both side by side in the future) is the ability to insert & style things like the little dice-rolling images right into the text lines – markdown I think lets you put images but doesn’t let you style (e.g. especially resize) them. Of course the “standard” way to deal with this when running markdown is to ALSO allow HTML, e.g. tags. So eventually moving toward some hybrid, probably, and/or making easy shortcuts (like I’m looking at right here in this message board editor) to quick-insert smilies and bold and stuff in user chat.

It would be a choice, one way or another – it would be straightforward, when reading chat text specifically to simply disallow tags (convert all the < and > symbols in chat lines to < and > and so “real” tags are only available via Report message strings and the like – that would of course eliminate the griefing/security issues at the same time).

The only remaining problem would be > and < symbols in Report lines. Nothing would “break” but they might not show up properly e.g. if somebody has a report message that says “Your combat strength is < than the defender’s strength”. A single > sign actually already shows up correctly, but a single < sign does not. And the rest of the text does show up in the chat line.

A couple easy parse fixes we could do include:
(1) a quick scan if there is a SINGLE < or > in the line and no balancing one, and change to e.g. < or > – should catch 99% of report strings out there in existing modules.
(2) We could allow e.g. “<<” to be parsed as a < sign
(3) OR… last thing I thought of but probably the best, we could go “opt-in” meaning that only report lines that begin with one of the special character markers would parse any HTML at all – so that should be fully reverse-compatible. The already-declared special color things (e.g. the !, ? and ~ color things) would already opt-in to HTML, and we’d introduce another one e.g. | to use the “standard game text” color but turn on HTML. So in other words anything sitting out in some earlier module that didn’t know about the amazing wonders of HTML chat would still “just work”.

I’d definitely be happy to make the updates to make this release-ready. I knew there’d be some extra stuff needed if this was going to really go “into VASSAL”, but I thought the current version made a good demo and a “custom class worthy” release level.

Let me know how to proceed,

Brian

Just checking in to see if you’d had time to process my thoughts. – Brian

Thus spake Cattlesquat:

Just checking in to see if you’d had time to process my thoughts. –
Brian

I haven’t been able to get back to it yet.

Where things stand for me at the moment I can either cycle through all the
things which are waiting on me replying that I haven’t gotten to them yet
and never accomplish anything, or I can ignore everything except what I’m
doing and get that done.

It’s nothing to do with what you’ve asked me in particular, just that it
hasn’t made it to the front of queue yet and the queue is long. I will
eventually get back to this.


J.

Not a problem. Been there. Just think of it as a ping to show I still care :slight_smile: