// License: WTFPL. For details, see LICENSE file. package geochat; import static org.openstreetmap.josm.tools.I18n.tr; import static org.openstreetmap.josm.tools.I18n.trn; import java.awt.AlphaComposite; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Composite; import java.awt.Dimension; import java.awt.Font; import java.awt.FontMetrics; import java.awt.Graphics2D; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Point; import java.awt.RenderingHints; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.List; import java.util.Map; import java.util.TreeMap; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComponent; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JTabbedPane; import javax.swing.JTextField; import javax.swing.SwingConstants; import org.openstreetmap.josm.Main; import org.openstreetmap.josm.data.Bounds; import org.openstreetmap.josm.data.coor.LatLon; import org.openstreetmap.josm.gui.JosmUserIdentityManager; import org.openstreetmap.josm.gui.MapView; import org.openstreetmap.josm.gui.Notification; import org.openstreetmap.josm.gui.dialogs.ToggleDialog; import org.openstreetmap.josm.gui.layer.MapViewPaintable; import org.openstreetmap.josm.gui.util.GuiHelper; import org.openstreetmap.josm.tools.GBC; /** * Chat Panel. Contains of one public chat pane and multiple private ones. * * @author zverik */ public class GeoChatPanel extends ToggleDialog implements ChatServerConnectionListener, MapViewPaintable { private JTextField input; private JTabbedPane tabs; private JComponent noData; private JPanel loginPanel; private JPanel gcPanel; private ChatServerConnection connection; // those fields should be visible to popup menu actions Map<String, LatLon> users; ChatPaneManager chatPanes; boolean userLayerActive; public GeoChatPanel() { super(tr("GeoChat"), "geochat", tr("Open GeoChat panel"), null, 200, true); noData = new JLabel(tr("Zoom in to see messages"), SwingConstants.CENTER); tabs = new JTabbedPane(); tabs.addMouseListener(new GeoChatPopupAdapter(this)); chatPanes = new ChatPaneManager(this, tabs); input = new JPanelTextField() { @Override protected void processEnter(String text) { connection.postMessage(text, chatPanes.getRecipient()); } @Override protected String autoComplete(String word, boolean atStart) { return autoCompleteUser(word, atStart); } }; String defaultUserName = constructUserName(); loginPanel = createLoginPanel(defaultUserName); gcPanel = new JPanel(new BorderLayout()); gcPanel.add(loginPanel, BorderLayout.CENTER); createLayout(gcPanel, false, null); users = new TreeMap<>(); // Start threads connection = ChatServerConnection.getInstance(); connection.addListener(this); boolean autoLogin = Main.pref.get("geochat.username", null) == null ? false : Main.pref.getBoolean("geochat.autologin", true); connection.autoLoginWithDelay(autoLogin ? defaultUserName : null); updateTitleAlarm(); } private String constructUserName() { String userName = Main.pref.get("geochat.username", null); // so the default is null if (userName == null) userName = JosmUserIdentityManager.getInstance().getUserName(); if (userName == null) userName = ""; if (userName.contains("@")) userName = userName.substring(0, userName.indexOf('@')); userName = userName.replace(' ', '_'); return userName; } private JPanel createLoginPanel(String defaultUserName) { final JTextField nameField = new JPanelTextField() { @Override protected void processEnter(String text) { connection.login(text); } }; nameField.setText(defaultUserName); JButton loginButton = new JButton(tr("Login")); loginButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { connection.login(nameField.getText()); } }); nameField.setPreferredSize(new Dimension(nameField.getPreferredSize().width, loginButton.getPreferredSize().height)); final JCheckBox autoLoginBox = new JCheckBox(tr("Enable autologin"), Main.pref.getBoolean("geochat.autologin", true)); autoLoginBox.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { Main.pref.put("geochat.autologin", autoLoginBox.isSelected()); } }); JPanel panel = new JPanel(new GridBagLayout()); panel.add(nameField, GBC.std().fill(GridBagConstraints.HORIZONTAL).insets(15, 0, 5, 0)); panel.add(loginButton, GBC.eol().fill(GridBagConstraints.NONE).insets(0, 0, 15, 0)); panel.add(autoLoginBox, GBC.std().insets(15, 0, 15, 0)); return panel; } protected void logout() { connection.logout(); } @Override public void destroy() { try { if (Main.pref.getBoolean("geochat.logout.on.close", true)) { connection.removeListener(this); connection.bruteLogout(); } } catch (IOException e) { Main.warn("Failed to logout from geochat server: " + e.getMessage()); } super.destroy(); } private String autoCompleteUser(String word, boolean atStart) { String result = null; boolean singleUser = true; for (String user : users.keySet()) { if (user.startsWith(word)) { if (result == null) result = user; else { singleUser = false; int i = word.length(); while (i < result.length() && i < user.length() && result.charAt(i) == user.charAt(i)) { i++; } if (i < result.length()) result = result.substring(0, i); } } } return result == null ? null : !singleUser ? result : atStart ? result + ": " : result + " "; } /** * This is implementation of a "temporary layer". It paints circles * for all users nearby. */ @Override public void paint(Graphics2D g, MapView mv, Bounds bbox) { Graphics2D g2d = (Graphics2D) g.create(); g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); Composite ac04 = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.4f); Composite ac10 = g2d.getComposite(); Font font = g2d.getFont().deriveFont(Font.BOLD, g2d.getFont().getSize2D() + 2.0f); g2d.setFont(font); FontMetrics fm = g2d.getFontMetrics(); for (String user : users.keySet()) { int stringWidth = fm.stringWidth(user); int radius = stringWidth / 2 + 10; Point p = mv.getPoint(users.get(user)); g2d.setComposite(ac04); g2d.setColor(Color.white); g2d.fillOval(p.x - radius, p.y - radius, radius * 2 + 1, radius * 2 + 1); g2d.setComposite(ac10); g2d.setColor(Color.black); g2d.drawString(user, p.x - stringWidth / 2, p.y + fm.getDescent()); } } /* ================== Notifications in the title ======================= */ /** * Display number of users and notifications in the panel title. */ protected void updateTitleAlarm() { int alarmLevel = connection.isLoggedIn() ? chatPanes.getNotifyLevel() : 0; if (!isDialogInCollapsedView() && alarmLevel > 1) alarmLevel = 1; String comment; if (connection.isLoggedIn()) { comment = trn("{0} user", "{0} users", users.size() + 1, users.size() + 1); } else { comment = tr("not logged in"); } String title = tr("GeoChat"); if (comment != null) title = title + " (" + comment + ")"; final String alarm = (alarmLevel <= 0 ? "" : alarmLevel == 1 ? "* " : "!!! ") + title; GuiHelper.runInEDT(new Runnable() { @Override public void run() { setTitle(alarm); } }); } /** * Track panel collapse events. */ @Override protected void setIsCollapsed(boolean val) { super.setIsCollapsed(val); chatPanes.setCollapsed(val); updateTitleAlarm(); } /* ============ ChatServerConnectionListener methods ============= */ @Override public void loggedIn(String userName) { Main.pref.put("geochat.username", userName); if (gcPanel.getComponentCount() == 1) { GuiHelper.runInEDTAndWait(new Runnable() { @Override public void run() { gcPanel.remove(0); gcPanel.add(tabs, BorderLayout.CENTER); gcPanel.add(input, BorderLayout.SOUTH); } }); } updateTitleAlarm(); } @Override public void notLoggedIn(final String reason) { if (reason != null) { GuiHelper.runInEDT(new Runnable() { @Override public void run() { new Notification(tr("Failed to log in to GeoChat:") + "\n" + reason).show(); } }); } else { // regular logout if (gcPanel.getComponentCount() > 1) { gcPanel.removeAll(); gcPanel.add(loginPanel, BorderLayout.CENTER); } } updateTitleAlarm(); } @Override public void messageSendFailed(final String reason) { GuiHelper.runInEDT(new Runnable() { @Override public void run() { new Notification(tr("Failed to send message:") + "\n" + reason).show(); } }); } @Override public void statusChanged(boolean active) { // only the public tab, because private chats don't rely on coordinates tabs.setComponentAt(0, active ? chatPanes.getPublicChatComponent() : noData); repaint(); } @Override public void updateUsers(Map<String, LatLon> newUsers) { for (String uname : this.users.keySet()) { if (!newUsers.containsKey(uname)) chatPanes.addLineToPublic(tr("User {0} has left", uname), ChatPaneManager.MESSAGE_TYPE_INFORMATION); } for (String uname : newUsers.keySet()) { if (!this.users.containsKey(uname)) chatPanes.addLineToPublic(tr("User {0} is mapping nearby", uname), ChatPaneManager.MESSAGE_TYPE_INFORMATION); } this.users = newUsers; updateTitleAlarm(); if (userLayerActive && Main.map.mapView != null) Main.map.mapView.repaint(); } private final SimpleDateFormat TIME_FORMAT = new SimpleDateFormat("HH:mm"); private void formatMessage(StringBuilder sb, ChatMessage msg) { sb.append("\n"); sb.append('[').append(TIME_FORMAT.format(msg.getTime())).append("] "); sb.append(msg.getAuthor()).append(": ").append(msg.getMessage()); } @Override public void receivedMessages(boolean replace, List<ChatMessage> messages) { if (replace) chatPanes.clearPublicChatPane(); if (!messages.isEmpty()) { int alarm = 0; StringBuilder sb = new StringBuilder(); for (ChatMessage msg : messages) { boolean important = msg.isIncoming() && containsName(msg.getMessage()); if (msg.isIncoming() && alarm < 2) { alarm = important ? 2 : 1; } if (important) { // add buffer, then add current line with italic, then clear buffer chatPanes.addLineToPublic(sb.toString()); sb.setLength(0); formatMessage(sb, msg); chatPanes.addLineToPublic(sb.toString(), ChatPaneManager.MESSAGE_TYPE_ATTENTION); sb.setLength(0); } else formatMessage(sb, msg); } chatPanes.addLineToPublic(sb.toString()); if (alarm > 0) chatPanes.notify(null, alarm); } if (replace) showNearbyUsers(); } private void showNearbyUsers() { if (!users.isEmpty()) { StringBuilder sb = new StringBuilder(tr("Users mapping nearby:")); boolean first = true; for (String user : users.keySet()) { sb.append(first ? " " : ", "); sb.append(user); } chatPanes.addLineToPublic(sb.toString(), ChatPaneManager.MESSAGE_TYPE_INFORMATION); } } private boolean containsName(String message) { String userName = connection.getUserName(); int length = userName.length(); int i = message.indexOf(userName); while (i >= 0) { if ((i == 0 || !Character.isJavaIdentifierPart(message.charAt(i - 1))) && (i + length >= message.length() || !Character.isJavaIdentifierPart(message.charAt(i + length)))) return true; i = message.indexOf(userName, i + 1); } return false; } @Override public void receivedPrivateMessages(boolean replace, List<ChatMessage> messages) { if (replace) chatPanes.closePrivateChatPanes(); for (ChatMessage msg : messages) { StringBuilder sb = new StringBuilder(); formatMessage(sb, msg); chatPanes.addLineToChatPane(msg.isIncoming() ? msg.getAuthor() : msg.getRecipient(), sb.toString()); if (msg.isIncoming()) chatPanes.notify(msg.getAuthor(), 2); } } }