/* * Copyright (c) 2014 tabletoptool.com team. * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Public License v3.0 * which accompanies this distribution, and is available at * http://www.gnu.org/licenses/gpl.html * * Contributors: * rptools.com team - initial implementation * tabletoptool.com team - further development */ package com.t3.client.ui.commandpanel; 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.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Image; import java.awt.Insets; import java.awt.Rectangle; import java.awt.RenderingHints; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.image.BufferedImage; import java.io.IOException; import java.util.LinkedList; import java.util.List; import java.util.Observable; import java.util.Observer; import javax.swing.AbstractAction; import javax.swing.ActionMap; import javax.swing.BorderFactory; import javax.swing.ImageIcon; import javax.swing.InputMap; import javax.swing.JButton; import javax.swing.JColorChooser; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.JTextPane; import javax.swing.JToggleButton; import javax.swing.KeyStroke; import javax.swing.border.BevelBorder; import javax.swing.plaf.basic.BasicToggleButtonUI; import org.apache.log4j.Logger; import com.t3.AppEvent; import com.t3.AppEventListener; import com.t3.client.AppActions; import com.t3.client.AppPreferences; import com.t3.client.AppStyle; import com.t3.client.T3MacroContext; import com.t3.client.TabletopTool; import com.t3.client.ui.chat.ChatProcessor; import com.t3.client.ui.chat.SmileyChatTranslationRuleGroup; import com.t3.image.ImageUtil; import com.t3.model.ObservableList; import com.t3.model.Token; import com.t3.model.chat.PlayerSpeaker; import com.t3.model.chat.Speaker; import com.t3.model.chat.TextMessage; import com.t3.model.chat.TokenSpeaker; import com.t3.swing.SwingUtil; import com.t3.util.ImageManager; import com.t3.util.StringUtil; import com.t3.util.guidreference.NullHelper; import com.t3.util.guidreference.TokenReference; public class CommandPanel extends JPanel implements Observer { private static final long serialVersionUID = 8710948417044703674L; private final List<String> commandHistory = new LinkedList<String>(); private final static Logger log=Logger.getLogger(CommandPanel.class); private JLabel characterLabel; private JTextPane commandTextArea; private MessagePanel messagePanel; private int commandHistoryIndex; private TextColorWell textColorWell; private JToggleButton scrollLockButton; private JToggleButton chatNotifyButton; private AvatarPanel avatarPanel; private JPopupMenu emotePopup; private JButton emotePopupButton; private String typedCommandBuffer; // Chat timers // private long chatNotifyDuration; // Initialize it on first load // private Timer chatTimer; private ChatProcessor chatProcessor; private TokenReference impersonatedToken; public CommandPanel() { setLayout(new BorderLayout()); setBorder(BorderFactory.createLineBorder(Color.gray)); add(BorderLayout.SOUTH, createSouthPanel()); add(BorderLayout.CENTER, getMessagePanel()); initializeSmilies(); addFocusHotKey(); } public ChatProcessor getChatProcessor() { return chatProcessor; } private void initializeSmilies() { SmileyChatTranslationRuleGroup smileyRuleGroup = new SmileyChatTranslationRuleGroup(); emotePopup = smileyRuleGroup.getEmotePopup(); chatProcessor = new ChatProcessor(); chatProcessor.install(smileyRuleGroup); } /** * Whether the player is currently impersonating a token */ public boolean isImpersonating() { return impersonatedToken != null; } /** * The name currently in use; if the user is not impersonating a token, this will return the player's name. */ public Speaker getSpeaker() { if (impersonatedToken == null) return new PlayerSpeaker(TabletopTool.getPlayer()); else if(!impersonatedToken.isValid()) { this.setImpersonatedToken(null); return new PlayerSpeaker(TabletopTool.getPlayer()); } return new TokenSpeaker(impersonatedToken.getId()); } public Token getImpersonatedToken() { return NullHelper.value(impersonatedToken); } public void setImpersonatedToken(TokenReference token) { if (token != null) { impersonatedToken = token; avatarPanel.setImage(ImageManager.getImageAndWait(token.value().getImageAssetId())); setCharacterLabel("Speaking as: " + getSpeaker().toString()); } else { impersonatedToken = null; avatarPanel.setImage(null); setCharacterLabel(""); } } public JButton getEmotePopupButton() { if (emotePopupButton == null) { try { emotePopupButton = new JButton(new ImageIcon(ImageUtil.getImage("com/t3/client/image/smiley/emsmile.png"))); emotePopupButton.setMargin(new Insets(0, 0, 0, 0)); emotePopupButton.setContentAreaFilled(false); emotePopupButton.setBorderPainted(false); emotePopupButton.setFocusPainted(false); emotePopupButton.setOpaque(false); emotePopupButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { emotePopup.show(emotePopupButton, 0, 0); } }); } catch (IOException ioe) { ioe.printStackTrace(); } } return emotePopupButton; } public JToggleButton getScrollLockButton() { if (scrollLockButton == null) { scrollLockButton = new JToggleButton(); scrollLockButton.setIcon(new ImageIcon(AppStyle.chatScrollImage)); scrollLockButton.setSelectedIcon(new ImageIcon(AppStyle.chatScrollLockImage)); scrollLockButton.setToolTipText("Scroll lock"); scrollLockButton.setUI(new BasicToggleButtonUI()); scrollLockButton.setBorderPainted(false); scrollLockButton.setFocusPainted(false); scrollLockButton.setPreferredSize(new Dimension(16, 16)); } return scrollLockButton; } /** * Gets the button for sending or suppressing notification that a player is typing in the chat text area. * * @return the notification button */ public JToggleButton getNotifyButton() { if (chatNotifyButton == null) { chatNotifyButton = new JToggleButton(); chatNotifyButton.setIcon(new ImageIcon(AppStyle.showTypingNotification)); chatNotifyButton.setSelectedIcon(new ImageIcon(AppStyle.hideTypingNotification)); chatNotifyButton.setToolTipText("Show/hide typing notification"); chatNotifyButton.setUI(new BasicToggleButtonUI()); chatNotifyButton.setBorderPainted(false); chatNotifyButton.setFocusPainted(false); chatNotifyButton.setPreferredSize(new Dimension(16, 16)); chatNotifyButton.addItemListener(new ItemListener() { private ChatTypingListener ours = null; @Override public void itemStateChanged(ItemEvent e) { if (e.getStateChange() == ItemEvent.SELECTED) { if (ours != null) commandTextArea.removeKeyListener(ours); ours = null; // Go ahead and turn off the chat panel right away. TabletopTool.getFrame().getChatTypingPanel().setVisible(false); } else if (e.getStateChange() == ItemEvent.DESELECTED) { if (ours == null) ours = new ChatTypingListener(); commandTextArea.addKeyListener(ours); } } }); } return chatNotifyButton; } private JComponent createSouthPanel() { JPanel panel = new JPanel(new BorderLayout()); JPanel subPanel = new JPanel(new BorderLayout()); subPanel.add(BorderLayout.EAST, createTextPropertiesPanel()); subPanel.add(BorderLayout.NORTH, createCharacterLabel()); subPanel.add(BorderLayout.CENTER, createCommandPanel()); panel.add(BorderLayout.WEST, createAvatarPanel()); panel.add(BorderLayout.CENTER, subPanel); return panel; } private JComponent createAvatarPanel() { avatarPanel = new AvatarPanel(new Dimension(60, 60)); return avatarPanel; } private JComponent createCommandPanel() { JScrollPane pane = new JScrollPane(getCommandTextArea(), JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); return pane; } private JComponent createTextPropertiesPanel() { JPanel panel = new JPanel(new GridBagLayout()); panel.setBorder(BorderFactory.createEmptyBorder(0, 3, 0, 3)); GridBagConstraints constraints = new GridBagConstraints(); constraints.gridx = 0; constraints.gridy = 0; panel.add(getTextColorWell(), constraints); // constraints.gridy++; // panel.add(Box.createVerticalStrut(2), constraints); constraints.gridy++; panel.add(getScrollLockButton(), constraints); constraints.gridx = 1; constraints.gridy = 0; panel.add(getEmotePopupButton(), constraints); constraints.gridy = 1; panel.add(getNotifyButton(), constraints); return panel; } public String getMessageHistory() { return messagePanel.getMessagesText(); } public void setCharacterLabel(String label) { characterLabel.setText(label); } @Override public Dimension getPreferredSize() { return new Dimension(50, 50); } @Override public Dimension getMinimumSize() { return getPreferredSize(); } public JLabel createCharacterLabel() { characterLabel = new JLabel("", JLabel.LEFT); characterLabel.setText(""); characterLabel.setBorder(BorderFactory.createEmptyBorder(1, 5, 1, 5)); return characterLabel; } /** * Creates the label for live typing. * * @return */ public JTextPane getCommandTextArea() { if (commandTextArea == null) { commandTextArea = new JTextPane() { @Override protected void paintComponent(Graphics g) { super.paintComponent(g); Dimension size = getSize(); g.setColor(Color.gray); g.drawLine(0, 0, size.width, 0); } }; commandTextArea.setBorder(BorderFactory.createEmptyBorder(0, 5, 0, 5)); commandTextArea.setPreferredSize(new Dimension(50, 40)); // XXX should be resizable commandTextArea.setFont(new Font("sans-serif", 0, AppPreferences.getFontSize())); commandTextArea.addKeyListener(new ChatTypingListener()); SwingUtil.useAntiAliasing(commandTextArea); ActionMap actions = commandTextArea.getActionMap(); actions.put(AppActions.COMMIT_COMMAND_ID, AppActions.COMMIT_COMMAND); actions.put(AppActions.ENTER_COMMAND_ID, AppActions.ENTER_COMMAND); actions.put(AppActions.CANCEL_COMMAND_ID, AppActions.CANCEL_COMMAND); actions.put(AppActions.COMMAND_UP_ID, new CommandHistoryUpAction()); actions.put(AppActions.COMMAND_DOWN_ID, new CommandHistoryDownAction()); InputMap inputs = commandTextArea.getInputMap(); inputs.put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), AppActions.CANCEL_COMMAND_ID); inputs.put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), AppActions.COMMIT_COMMAND_ID); inputs.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0), AppActions.COMMAND_UP_ID); inputs.put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0), AppActions.COMMAND_DOWN_ID); // Resize on demand TabletopTool.getEventDispatcher().addListener(TabletopTool.PreferencesEvent.Changed, new AppEventListener() { @Override public void handleAppEvent(AppEvent event) { commandTextArea.setFont(commandTextArea.getFont().deriveFont((float) AppPreferences.getFontSize())); doLayout(); } }); } return commandTextArea; } /** * KeyListener for command area to handle live typing notification. Implements an idle timer that removes the typing * notification after the duration set in AppPreferences expires. */ private class ChatTypingListener extends KeyAdapter { @Override public void keyReleased(KeyEvent kre) { // Get the key released int key = kre.getKeyCode(); if (key == KeyEvent.VK_ENTER) { // User hit enter, reset the label and stop the timer TabletopTool.serverCommand().setLiveTypingLabel(TabletopTool.getPlayer().getName(), false); } else if (!chatNotifyButton.isSelected()) { TabletopTool.serverCommand().setLiveTypingLabel(TabletopTool.getPlayer().getName(), true); } } } /** * Execute the command in the command field. */ public void commitCommand() { commitCommand(null); } /** * Disables the chat notification toggle if the GM enforces notification * * @param boolean whether to disable the toggle */ public void disableNotifyButton(Boolean disable) { // Little clumsy, but when the menu item is _enabled_, the button should be _disabled_ if (!TabletopTool.getPlayer().isGM()) { chatNotifyButton.setSelected(false); chatNotifyButton.setEnabled(!disable); maybeAddTypingListener(); } } /** * Execute the command in the command field. * * @param macroContext * The context we are calling the macro in. */ public void commitCommand(T3MacroContext macroContext) { String text = commandTextArea.getText().trim(); // Command history // Don't store up a bunch of repeats if (commandHistory.size() == 0 || !text.equals(commandHistory.get(commandHistory.size() - 1))) { commandHistory.add(text); typedCommandBuffer = null; } commandHistoryIndex = commandHistory.size(); // Make sure they aren't trying to break out of the div // FIXME: as above, </{"div"}> can be used to get around this int divCount = StringUtil.countOccurances(text, "<div"); int closeDivCount = StringUtil.countOccurances(text, "</div>"); while (closeDivCount < divCount) { text += "</div>"; closeDivCount++; } if (closeDivCount > divCount) { TabletopTool.addServerMessage(TextMessage.me("You have too many </div>.")); commandTextArea.setText(""); return; } //try { ChatExecutor.executeChat(text,this.getSpeaker()); //FIXMESOON print the message as needed on different computers //MacroEngine.getInstance().evaluate(text, macroContext); /*} catch (MacroException e) { e.printStackTrace(); //FIXMESOON log error }*/ commandTextArea.setText(""); TabletopTool.serverCommand().setLiveTypingLabel(TabletopTool.getPlayer().getName(), false); } public void clearMessagePanel() { messagePanel.clearMessages(); } /** * Cancel the current command in the command field. */ public void cancelCommand() { commandTextArea.setText(""); validate(); TabletopTool.getFrame().hideCommandPanel(); } public void startMacro() { TabletopTool.getFrame().showCommandPanel(); commandTextArea.requestFocusInWindow(); commandTextArea.setText("/"); } public void startChat() { TabletopTool.getFrame().showCommandPanel(); commandTextArea.requestFocusInWindow(); } public TextColorWell getTextColorWell() { if (textColorWell == null) { textColorWell = new TextColorWell(); } return textColorWell; } private class CommandHistoryUpAction extends AbstractAction { @Override public void actionPerformed(ActionEvent e) { if (commandHistory.size() == 0) { return; } if (commandHistoryIndex == commandHistory.size()) { typedCommandBuffer = getCommandTextArea().getText(); } commandHistoryIndex--; if (commandHistoryIndex < 0) { commandHistoryIndex = 0; } commandTextArea.setText(commandHistory.get(commandHistoryIndex)); } } private class CommandHistoryDownAction extends AbstractAction { private static final long serialVersionUID = 7070274680351186504L; @Override public void actionPerformed(ActionEvent e) { if (commandHistory.size() == 0) { return; } commandHistoryIndex++; if (commandHistoryIndex == commandHistory.size()) { commandTextArea.setText(typedCommandBuffer != null ? typedCommandBuffer : ""); commandHistoryIndex = commandHistory.size(); } else if (commandHistoryIndex >= commandHistory.size()) { commandHistoryIndex--; } else { commandTextArea.setText(commandHistory.get(commandHistoryIndex)); } } } @Override public void requestFocus() { commandTextArea.requestFocus(); } private MessagePanel getMessagePanel() { if (messagePanel == null) { messagePanel = new MessagePanel(); // Update whenever the preferences change TabletopTool.getEventDispatcher().addListener(TabletopTool.PreferencesEvent.Changed, new AppEventListener() { @Override public void handleAppEvent(AppEvent event) { messagePanel.refreshRenderer(); } }); } return messagePanel; } public void addMessage(TextMessage message) { messagePanel.addMessage(message); } public void setTrustedMacroPrefixColors(Color foreground, Color background) { getMessagePanel().setTrustedMacroPrefixColors(foreground, background); } public static class TextColorWell extends JPanel { private static final long serialVersionUID = -9006587537198176935L; //Set the Color from the saved chat color from AppPreferences private Color color = AppPreferences.getChatColor(); public TextColorWell() { setMinimumSize(new Dimension(15, 15)); setPreferredSize(new Dimension(15, 15)); setBorder(BorderFactory.createBevelBorder(BevelBorder.LOWERED)); setToolTipText("Set the color of your speech text"); addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { Color newColor = JColorChooser.showDialog(TextColorWell.this, "Text Color", color); if (newColor != null) { setColor(newColor); } } }); } public void setColor(Color newColor) { color = newColor; repaint(); AppPreferences.setChatColor(color); //Set the Chat Color in AppPreferences } public Color getColor() { return color; } @Override protected void paintComponent(Graphics g) { g.setColor(color); g.fillRect(0, 0, getSize().width, getSize().height); } } private class AvatarPanel extends JComponent { private static final long serialVersionUID = -8027749503951260361L; private static final int PADDING = 5; private final Dimension preferredSize; private Image image; private Rectangle cancelBounds; public AvatarPanel(Dimension preferredSize) { this.preferredSize = preferredSize; setImage(null); addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if (cancelBounds != null && cancelBounds.contains(e.getPoint())) { JTextPane commandArea = getCommandTextArea(); commandArea.setText("/im"); TabletopTool.getFrame().getCommandPanel().commitCommand(); } } }); } public void setImage(Image image) { this.image = image; setPreferredSize(image != null ? preferredSize : new Dimension(0, 0)); invalidate(); repaint(); } @Override protected void paintComponent(Graphics g) { Dimension size = getSize(); g.setColor(getBackground()); g.fillRect(0, 0, size.width, size.height); cancelBounds = null; if (image == null) { return; } Dimension imgSize = new Dimension(image.getWidth(null), image.getHeight(null)); SwingUtil.constrainTo(imgSize, size.width - PADDING * 2, size.height - PADDING * 2); ((Graphics2D) g).setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); g.drawImage(image, (size.width - imgSize.width) / 2, (size.height - imgSize.height) / 2, imgSize.width, imgSize.height, this); // Cancel BufferedImage cancelButton = AppStyle.cancelButton; int x = size.width - cancelButton.getWidth(); int y = 2; g.drawImage(cancelButton, x, y, this); cancelBounds = new Rectangle(x, y, cancelButton.getWidth(), cancelButton.getHeight()); } } //// // OBSERVER @Override public void update(Observable o, Object arg) { ObservableList<TextMessage> textList = TabletopTool.getMessageList(); ObservableList.Event event = (ObservableList.Event) arg; switch (event) { case append: addMessage(textList.get(textList.size() - 1)); break; case add: case remove: //resetMessagePanel(); break; case clear: clearMessagePanel(); break; default: throw new IllegalArgumentException("Unknown event: " + event); } } private void addFocusHotKey() { KeyStroke ks = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0); Object actionObject = new Object(); getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(ks, actionObject); getActionMap().put(actionObject, new AbstractAction() { @Override public void actionPerformed(ActionEvent event) { requestFocus(); } }); } /** * Convenience method to run a command with ability to preserve the text in the commandTextArea. * * @param command * Command string * @param preserveOldText * true for preserving, false to remove the old text */ public void quickCommit(String command, boolean preserveOldText) { String oldText = commandTextArea.getText(); commandTextArea.setText(command); commitCommand(); if (preserveOldText) { commandTextArea.setText(oldText); } } public void quickCommit(String command) { quickCommit(command, true); } /* * Gets the chat notification duration. Method is unused at this time. * * @return time in milliseconds before chat notifications disappear * * public long getChatNotifyDuration(){ return chatNotifyDuration; } */ /** * If the GM enforces typing notification and no listener is present (because the client had notification off), a * new listener is added to the command text area */ private void maybeAddTypingListener() { if (commandTextArea.getListeners(ChatTypingListener.class).length == 0) { commandTextArea.addKeyListener(new ChatTypingListener()); } } }