Java tutorial
/* * Kontalk Java client * Copyright (C) 2014 Kontalk Devteam <devteam@kontalk.org> * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.kontalk.view; import com.alee.extended.filechooser.WebFileChooserField; import com.alee.extended.filefilter.ImageFilesFilter; import com.alee.extended.statusbar.WebStatusBar; import com.alee.extended.statusbar.WebStatusLabel; import com.alee.laf.WebLookAndFeel; import com.alee.laf.button.WebButton; import com.alee.laf.label.WebLabel; import com.alee.laf.menu.WebMenuItem; import com.alee.laf.menu.WebPopupMenu; import com.alee.laf.optionpane.WebOptionPane; import com.alee.laf.panel.WebPanel; import com.alee.laf.rootpane.WebDialog; import com.alee.laf.text.WebPasswordField; import com.alee.laf.text.WebTextArea; import com.alee.laf.text.WebTextField; import com.alee.managers.hotkey.Hotkey; import com.alee.managers.hotkey.HotkeyData; import com.alee.managers.notification.NotificationManager; import com.alee.managers.tooltip.TooltipManager; import com.alee.managers.tooltip.TooltipWay; import java.awt.AWTException; import java.awt.Color; import java.awt.Image; import java.awt.SystemTray; import java.awt.Toolkit; import java.awt.TrayIcon; import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.StringSelection; import java.awt.event.*; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.net.URL; import java.security.cert.CertificateException; import java.util.Observable; import java.util.Observer; import java.util.Optional; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; import java.util.logging.Level; import java.util.logging.Logger; import javax.net.ssl.SSLHandshakeException; import javax.swing.Icon; import javax.swing.ImageIcon; import javax.swing.JDialog; import javax.swing.SwingUtilities; import javax.swing.ToolTipManager; import javax.swing.event.DocumentEvent; import com.alee.utils.swing.DocumentChangeListener; import java.awt.BorderLayout; import java.awt.Component; import java.awt.EventQueue; import org.apache.commons.lang.StringUtils; import org.bouncycastle.openpgp.PGPException; import org.jivesoftware.smack.SmackException; import org.jivesoftware.smack.SmackException.ConnectionException; import org.jivesoftware.smack.sasl.SASLErrorException; import org.jivesoftware.smackx.chatstates.ChatState; import org.jxmpp.util.XmppStringUtils; import org.kontalk.Kontalk; import org.kontalk.system.Config; import org.kontalk.misc.KonException; import org.kontalk.crypto.Coder; import org.kontalk.misc.ViewEvent; import org.kontalk.model.InMessage; import org.kontalk.model.KonMessage; import org.kontalk.model.KonThread; import org.kontalk.model.MessageList; import org.kontalk.model.ThreadList; import org.kontalk.model.User; import org.kontalk.model.UserList; import org.kontalk.system.Control; import org.kontalk.util.Tr; /** * Initialize and control the user interface. * * @author Alexander Bikadorov <abiku@cs.tu-berlin.de> */ public final class View implements Observer { private final static Logger LOGGER = Logger.getLogger(View.class.getName()); final static Color BLUE = new Color(130, 170, 240); //final static Color BLUE = new Color(0, 181, 233); final static Color LIGHT_BLUE = new Color(220, 220, 250); //final static Color LIGHT_BLUE = new Color(32, 210, 237); final static Color GREEN = new Color(83, 196, 46); private final Control mControl; private final UserListView mUserListView; private final ThreadListView mThreadListView; private final ThreadView mThreadView; private final WebTextArea mSendTextArea; private final WebButton mSendButton; private final WebStatusLabel mStatusBarLabel; private final MainFrame mMainFrame; private TrayIcon mTrayIcon; private View(Control control) { mControl = control; WebLookAndFeel.install(); ToolTipManager.sharedInstance().setInitialDelay(200); mUserListView = new UserListView(this, UserList.getInstance()); UserList.getInstance().addObserver(mUserListView); mThreadListView = new ThreadListView(this, ThreadList.getInstance()); ThreadList.getInstance().addObserver(mThreadListView); mThreadView = new ThreadView(this); ThreadList.getInstance().addObserver(mThreadView); // text field mSendTextArea = new WebTextArea(); EventQueue.invokeLater(new Runnable() { @Override public void run() { mSendTextArea.requestFocusInWindow(); } }); mSendTextArea.setMargin(5); mSendTextArea.setLineWrap(true); mSendTextArea.setWrapStyleWord(true); mSendTextArea.getDocument().addDocumentListener(new DocumentChangeListener() { @Override public void documentChanged(DocumentEvent e) { View.this.handleKeyTypeEvent(); } }); // send button mSendButton = new WebButton(Tr.tr("Send")); // for showing the hotkey tooltip TooltipManager.addTooltip(mSendButton, Tr.tr("Send Message")); mSendButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { Component focusOwner = mMainFrame.getFocusOwner(); if (focusOwner != mSendTextArea) return; View.this.callSendText(); } }); // status bar WebStatusBar statusBar = new WebStatusBar(); mStatusBarLabel = new WebStatusLabel(" "); statusBar.add(mStatusBarLabel); // main frame mMainFrame = new MainFrame(this, mUserListView, mThreadListView, mThreadView, mSendTextArea, mSendButton, statusBar); mMainFrame.setVisible(true); // tray this.setTray(); // hotkeys this.setHotkeys(); // notifier MessageList.getInstance().addObserver(new Notifier(this)); this.statusChanged(); } final void setTray() { if (!Config.getInstance().getBoolean(Config.MAIN_TRAY)) { this.removeTray(); return; } if (!SystemTray.isSupported()) { LOGGER.info("tray icon not supported"); return; } if (mTrayIcon != null) // already set return; // load image Image image = getImage("kontalk.png"); //image = image.getScaledInstance(22, 22, Image.SCALE_SMOOTH); // popup menu outside of frame, officially not supported final WebPopupMenu popup = new WebPopupMenu("Kontalk"); WebMenuItem quitItem = new WebMenuItem(Tr.tr("Quit")); quitItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent event) { View.this.callShutDown(); } }); popup.add(quitItem); // workaround: menu does not disappear when focus is lost final WebDialog hiddenDialog = new WebDialog(); hiddenDialog.setUndecorated(true); // create an action listener to listen for default action executed on the tray icon MouseListener listener = new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { // menu must be shown on mouse release //check(e); } @Override public void mouseReleased(MouseEvent e) { if (e.getButton() == MouseEvent.BUTTON1) mMainFrame.toggleState(); else check(e); } private void check(MouseEvent e) { // if (!e.isPopupTrigger()) // return; hiddenDialog.setVisible(true); // TODO ugly code popup.setLocation(e.getX() - 20, e.getY() - 40); popup.setInvoker(hiddenDialog); popup.setCornerWidth(0); popup.setVisible(true); } }; mTrayIcon = new TrayIcon(image, "Kontalk" /*, popup*/); mTrayIcon.setImageAutoSize(true); mTrayIcon.addMouseListener(listener); SystemTray tray = SystemTray.getSystemTray(); try { tray.add(mTrayIcon); } catch (AWTException ex) { LOGGER.log(Level.WARNING, "can't add tray icon", ex); } } void setHotkeys() { final boolean enterSends = Config.getInstance().getBoolean(Config.MAIN_ENTER_SENDS); for (KeyListener l : mSendTextArea.getKeyListeners()) mSendTextArea.removeKeyListener(l); mSendTextArea.addKeyListener(new KeyListener() { @Override public void keyPressed(KeyEvent e) { if (enterSends && e.getKeyCode() == KeyEvent.VK_ENTER && e.getModifiersEx() == KeyEvent.CTRL_DOWN_MASK) { e.consume(); mSendTextArea.append(System.getProperty("line.separator")); } if (enterSends && e.getKeyCode() == KeyEvent.VK_ENTER && e.getModifiers() == 0) { // only ignore e.consume(); } } @Override public void keyReleased(KeyEvent e) { } @Override public void keyTyped(KeyEvent e) { } }); mSendButton.removeHotkeys(); HotkeyData sendHotkey = enterSends ? Hotkey.ENTER : Hotkey.CTRL_ENTER; mSendButton.addHotkey(sendHotkey, TooltipWay.up); } /** * Setup view on startup after model was initialized. */ public void init() { SwingUtilities.invokeLater(new Runnable() { @Override public void run() { View.this.mThreadListView.selectLastThread(); if (ThreadList.getInstance().getAll().isEmpty()) mMainFrame.selectTab(MainFrame.Tab.USER); } }); } Control.Status getCurrentStatus() { return mControl.getCurrentStatus(); } void showConfig() { JDialog configFrame = new ConfigurationDialog(mMainFrame, this); configFrame.setVisible(true); } /* control to view */ @Override public void update(Observable o, final Object arg) { if (SwingUtilities.isEventDispatchThread()) { this.updateOnEDT(arg); return; } SwingUtilities.invokeLater(new Runnable() { @Override public void run() { View.this.updateOnEDT(arg); } }); } private void updateOnEDT(Object arg) { if (arg instanceof ViewEvent.StatusChanged) { this.statusChanged(); } else if (arg instanceof ViewEvent.PasswordSet) { this.showPasswordDialog(false); } else if (arg instanceof ViewEvent.MissingAccount) { ViewEvent.MissingAccount missAccount = (ViewEvent.MissingAccount) arg; this.showImportWizard(missAccount.connect); } else if (arg instanceof ViewEvent.Exception) { ViewEvent.Exception exception = (ViewEvent.Exception) arg; this.handleException(exception.exception); } else if (arg instanceof ViewEvent.SecurityError) { ViewEvent.SecurityError error = (ViewEvent.SecurityError) arg; this.handleSecurityErrors(error.message); } else { LOGGER.warning("unexpected argument"); } } private void statusChanged() { Control.Status status = mControl.getCurrentStatus(); switch (status) { case CONNECTING: mStatusBarLabel.setText(Tr.tr("Connecting...")); break; case CONNECTED: mThreadView.setColor(Color.white); mStatusBarLabel.setText(Tr.tr("Connected")); break; case DISCONNECTING: mStatusBarLabel.setText(Tr.tr("Disconnecting...")); break; case DISCONNECTED: mThreadView.setColor(Color.lightGray); mStatusBarLabel.setText(Tr.tr("Not connected")); //if (mTrayIcon != null) // trayIcon.setImage(updatedImage); break; case SHUTTING_DOWN: mMainFrame.save(); mThreadListView.save(); this.removeTray(); mMainFrame.setVisible(false); mMainFrame.dispose(); break; case FAILED: mStatusBarLabel.setText(Tr.tr("Connecting failed")); break; case ERROR: mThreadView.setColor(Color.lightGray); mStatusBarLabel.setText(Tr.tr("Connection error")); break; } mMainFrame.statusChanged(status); } void showPasswordDialog(boolean wasWrong) { WebPanel passPanel = new WebPanel(); WebLabel passLabel = new WebLabel(Tr.tr("Please enter your key password:")); passPanel.add(passLabel, BorderLayout.NORTH); final WebPasswordField passField = new WebPasswordField(); passPanel.add(passField, BorderLayout.CENTER); if (wasWrong) { WebLabel wrongLabel = new WebLabel(Tr.tr("Wrong password")); wrongLabel.setForeground(Color.RED); passPanel.add(wrongLabel, BorderLayout.SOUTH); } WebOptionPane passPane = new WebOptionPane(passPanel, WebOptionPane.QUESTION_MESSAGE, WebOptionPane.OK_CANCEL_OPTION); JDialog dialog = passPane.createDialog(mMainFrame, Tr.tr("Enter password")); dialog.setModal(true); dialog.addWindowFocusListener(new WindowAdapter() { @Override public void windowGainedFocus(WindowEvent e) { passField.requestFocusInWindow(); } }); // blocking dialog.setVisible(true); Object value = passPane.getValue(); if (value != null && value.equals(WebOptionPane.OK_OPTION)) mControl.connect(passField.getPassword()); } void showImportWizard(boolean connect) { WebDialog importFrame = new ImportDialog(this, connect); importFrame.setVisible(true); } private void handleException(KonException ex) { if (ex.getError() == KonException.Error.LOAD_KEY_DECRYPT) { this.showPasswordDialog(true); return; } String errorText = getErrorText(ex); WebOptionPane.showMessageDialog(mMainFrame, errorText, Tr.tr("Error"), WebOptionPane.ERROR_MESSAGE); } private void handleSecurityErrors(KonMessage message) { String errorText = "<html>"; boolean isOut = message.getDir() == KonMessage.Direction.OUT; errorText += isOut ? Tr.tr("Encryption error") : Tr.tr("Decryption error"); errorText += ":"; for (Coder.Error error : message.getCoderStatus().getErrors()) { errorText += "<br>"; switch (error) { case UNKNOWN_ERROR: errorText += Tr.tr("Unknown error"); break; case KEY_UNAVAILABLE: errorText += Tr.tr("Key for receiver not found."); break; case INVALID_PRIVATE_KEY: errorText += Tr.tr("This message was encrypted with an old or invalid key"); break; default: errorText += Tr.tr("Unusual coder error") + ": " + error.toString(); } } errorText += "</html>"; NotificationManager.showNotification(mThreadView, errorText); } private void removeTray() { if (mTrayIcon != null) { SystemTray tray = SystemTray.getSystemTray(); tray.remove(mTrayIcon); mTrayIcon = null; } } /* view to control */ void callShutDown() { mControl.shutDown(); } void callConnect() { mControl.connect(); } void callDisconnect() { mControl.disconnect(); } void callCreateNewThread(Set<User> user) { KonThread thread = mControl.createNewThread(user); this.showThread(thread); } void callCreateNewUser(String jid, String name, boolean encrypted) { mControl.createNewUser(jid, name, encrypted); } private void callSendText() { KonThread thread = mThreadListView.getSelectedValue(); if (thread == null) { // nothing selected return; } mControl.sendText(thread, mSendTextArea.getText()); mSendTextArea.setText(""); } void callSetUserBlocking(User user, boolean blocking) { mControl.sendUserBlocking(user, blocking); } void callDecrypt(InMessage message) { mControl.decryptAndDownload(message); } void callRequestKey(User user) { mControl.sendKeyRequest(user); } void callHandleOwnChatStateEvent(KonThread thread, ChatState state) { mControl.handleOwnChatStateEvent(thread, state); } void callSendStatusText() { mControl.sendStatusText(); } /* view internal */ void selectThreadByUser(User user) { if (user == null) return; KonThread thread = ThreadList.getInstance().get(user); this.showThread(thread); } private void showThread(KonThread thread) { mThreadListView.setSelectedItem(thread); mMainFrame.selectTab(MainFrame.Tab.THREADS); } void selectedThreadChanged(KonThread thread) { if (thread == null) return; mThreadView.showThread(thread); } private void handleKeyTypeEvent() { Optional<KonThread> optThread = mThreadView.getCurrentThread(); if (!optThread.isPresent()) return; mControl.handleOwnChatStateEvent(optThread.get(), ChatState.composing); } Optional<KonThread> getCurrentShownThread() { return mThreadView.getCurrentThread(); } boolean mainFrameIsFocused() { return mMainFrame.isFocused(); } void reloadThreadBG() { mThreadView.loadDefaultBG(); } static Icon getIcon(String fileName) { return new ImageIcon(getImage(fileName)); } static Image getImage(String fileName) { URL imageUrl = ClassLoader.getSystemResource(Kontalk.RES_PATH + fileName); if (imageUrl == null) { LOGGER.warning("can't find icon image resource"); return new BufferedImage(10, 10, BufferedImage.TYPE_INT_RGB); } return Toolkit.getDefaultToolkit().createImage(imageUrl); } static WebFileChooserField createImageChooser(boolean enabled, String path) { WebFileChooserField chooser = new WebFileChooserField(); chooser.setEnabled(enabled); chooser.getChooseButton().setEnabled(enabled); if (!path.isEmpty()) chooser.setSelectedFile(new File(path)); chooser.setMultiSelectionEnabled(false); chooser.setShowRemoveButton(true); chooser.getWebFileChooser().setFileFilter(new ImageFilesFilter()); File file = new File(path); if (file.exists()) { chooser.setSelectedFile(file); } if (file.getParentFile() != null && file.getParentFile().exists()) chooser.getWebFileChooser().setCurrentDirectory(file.getParentFile()); return chooser; } static WebTextField createTextField(final String text) { final WebTextField field = new WebTextField(text, false); field.setEditable(false); field.setBackground(null); field.addMouseListener(new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { check(e); } @Override public void mouseReleased(MouseEvent e) { check(e); } private void check(MouseEvent e) { if (e.isPopupTrigger()) { WebPopupMenu popupMenu = new WebPopupMenu(); popupMenu.add(View.createCopyMenuItem(field.getText(), "")); popupMenu.show(field, e.getX(), e.getY()); } } }); return field; } static WebMenuItem createCopyMenuItem(final String copyText, String toolTipText) { WebMenuItem item = new WebMenuItem(Tr.tr("Copy")); if (!toolTipText.isEmpty()) item.setToolTipText(toolTipText); item.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent event) { Clipboard clip = Toolkit.getDefaultToolkit().getSystemClipboard(); clip.setContents(new StringSelection(copyText), null); } }); return item; } static String getErrorText(KonException ex) { String eol = System.getProperty("line.separator"); String errorText = Tr.tr("Unknown error!?"); switch (ex.getError()) { case IMPORT_ARCHIVE: errorText = Tr.tr("Can't open key archive."); break; case IMPORT_READ_FILE: errorText = Tr.tr("Can't load keyfile(s) from archive."); break; case IMPORT_KEY: errorText = Tr.tr("Can't create personal key from key files.") + " "; if (ex.getExceptionClass().equals(IOException.class)) { errorText += eol + Tr.tr("Is the public key file valid?"); } if (ex.getExceptionClass().equals(CertificateException.class)) { // bridge errorText += eol + Tr.tr("Are all key files valid?"); } if (ex.getExceptionClass().equals(PGPException.class)) { errorText += eol + Tr.tr("Is the passphrase correct?"); } break; case CHANGE_PASSWORD: errorText = Tr.tr("Can't change password. Internal error(!?)"); break; case WRITE_FILE: errorText = Tr.tr("Can't write key files to configuration directory."); break; case READ_FILE: case LOAD_KEY: switch (ex.getError()) { case READ_FILE: errorText = Tr.tr("Can't read key files from configuration directory."); break; case LOAD_KEY: errorText = Tr.tr("Can't load key files from configuration directory."); break; } errorText += " " + Tr.tr("Please reimport your key."); break; case CLIENT_CONNECTION: errorText = Tr.tr("Can't create connection"); break; case CLIENT_CONNECT: errorText = Tr.tr("Can't connect to server."); if (ex.getExceptionClass().equals(ConnectionException.class)) { errorText += eol + Tr.tr("Is the server address correct?"); } if (ex.getExceptionClass().equals(SSLHandshakeException.class)) { errorText += eol + Tr.tr("The server rejects the key."); } if (ex.getExceptionClass().equals(SmackException.NoResponseException.class)) { errorText += eol + Tr.tr("The server does not respond."); } break; case CLIENT_LOGIN: errorText = Tr.tr("Can't login to server."); if (ex.getExceptionClass().equals(SASLErrorException.class)) { errorText += eol + Tr.tr( "The server rejects the account. Is the specified server correct and the account valid?"); } break; case CLIENT_ERROR: errorText = Tr.tr("Connection to server closed on error."); // TODO more details break; } return errorText; } static String shortenJID(String jid, int maxLength) { if (jid.length() > maxLength) { String local = XmppStringUtils.parseLocalpart(jid); local = StringUtils.abbreviate(local, (int) (maxLength * 0.4)); String domain = XmppStringUtils.parseDomain(jid); domain = StringUtils.abbreviate(domain, (int) (maxLength * 0.6)); jid = XmppStringUtils.completeJidFrom(local, domain); } return jid; } static String shortenUserName(String jid, int maxLength) { String local = XmppStringUtils.parseLocalpart(jid); local = StringUtils.abbreviate(local, maxLength); String domain = XmppStringUtils.parseDomain(jid); return XmppStringUtils.completeJidFrom(local, domain); } public static Optional<View> create(final Control control) { Optional<View> optView = invokeAndWait(new Callable<View>() { @Override public View call() throws Exception { return new View(control); } }); if (!optView.isPresent()) { LOGGER.log(Level.SEVERE, "can't start view"); return optView; } control.addObserver(optView.get()); return optView; } static <T> Optional<T> invokeAndWait(Callable<T> callable) { try { FutureTask<T> task = new FutureTask<>(callable); SwingUtilities.invokeLater(task); // blocking return Optional.of(task.get()); } catch (ExecutionException | InterruptedException ex) { LOGGER.log(Level.WARNING, "can't execute task", ex); } return Optional.empty(); } public static void showWrongJavaVersionDialog() { String jVersion = System.getProperty("java.version"); if (jVersion.length() >= 3) jVersion = jVersion.substring(2, 3); String errorText = Tr.tr("The installed Java version is too old") + ": " + jVersion; errorText += System.getProperty("line.separator"); errorText += Tr.tr("Please install Java 8."); WebOptionPane.showMessageDialog(null, errorText, Tr.tr("Unsupported Java Version"), WebOptionPane.ERROR_MESSAGE); } }