/* * Kontalk Java client * Copyright (C) 2016 Kontalk Devteam <[email protected]> * * 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 javax.swing.AbstractButton; import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.DefaultListModel; import javax.swing.DefaultListSelectionModel; import javax.swing.Icon; import javax.swing.JFileChooser; import javax.swing.JLayeredPane; import javax.swing.JList; import javax.swing.JOptionPane; import javax.swing.JPopupMenu; import javax.swing.JRootPane; import javax.swing.ListCellRenderer; import javax.swing.ListSelectionModel; import javax.swing.ScrollPaneConstants; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.text.AttributeSet; import javax.swing.text.BadLocationException; import javax.swing.text.Document; import javax.swing.text.PlainDocument; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.FlowLayout; import java.awt.Window; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.WindowEvent; import java.awt.event.WindowStateListener; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.Arrays; import java.util.Comparator; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Optional; import java.util.logging.Level; import java.util.logging.Logger; import com.alee.extended.image.WebDecoratedImage; import com.alee.extended.image.WebImage; import com.alee.extended.label.WebLinkLabel; import com.alee.extended.layout.FormLayout; import com.alee.extended.panel.GroupPanel; import com.alee.extended.panel.GroupingType; import com.alee.laf.button.WebButton; import com.alee.laf.button.WebToggleButton; import com.alee.laf.checkbox.WebCheckBox; import com.alee.laf.filechooser.WebFileChooser; import com.alee.laf.label.WebLabel; import com.alee.laf.list.WebList; import com.alee.laf.menu.WebMenuItem; import com.alee.laf.menu.WebPopupMenu; import com.alee.laf.panel.WebPanel; import com.alee.laf.scroll.WebScrollPane; import com.alee.laf.separator.WebSeparator; import com.alee.laf.tabbedpane.WebTabbedPane; import com.alee.laf.text.WebPasswordField; import com.alee.laf.text.WebTextField; import com.alee.managers.popup.WebPopup; import com.alee.managers.tooltip.TooltipManager; import com.alee.utils.ImageUtils; import com.alee.utils.SwingUtils; import com.alee.utils.filefilter.ImageFilesFilter; import com.alee.utils.swing.DocumentChangeListener; import com.google.i18n.phonenumbers.PhoneNumberUtil; import org.kontalk.misc.JID; import org.kontalk.model.Contact; import org.kontalk.model.Model; import org.kontalk.model.chat.Chat; import org.kontalk.model.chat.GroupChat; import org.kontalk.model.chat.Member; import org.kontalk.persistence.Config; import org.kontalk.util.MediaUtils; import org.kontalk.util.Tr; import org.kontalk.util.XMPPUtils; import org.kontalk.view.AvatarLoader.AvatarImg; /** * Some own component classes used in view. * * @author Alexander Bikadorov {@literal <[email protected]>} */ final class ComponentUtils { private static final Logger LOGGER = Logger.getLogger(ComponentUtils.class.getName()); private ComponentUtils() {} static class ScrollPane extends WebScrollPane { ScrollPane(Component component) { this(component, true); } ScrollPane(Component component, boolean border) { super(component); if (!border) this.setBorder(BorderFactory.createMatteBorder(1, 1, 0, 1, Color.LIGHT_GRAY)); this.setHorizontalScrollBarPolicy( ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); this.getVerticalScrollBar().setUnitIncrement(25); } } /** A button that toggles showing a panel on click. */ static abstract class ToggleButton extends WebToggleButton { private ModalPopup mPopup; ToggleButton(Icon icon, String tooltip) { super(icon); TooltipManager.addTooltip(this, tooltip); this.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { //if (mPopup == null || !mPopup.isShowing()) ToggleButton.this.showPopupPanel(); } }); } private void showPopupPanel() { if (mPopup == null) mPopup = new ComponentUtils.ModalPopup(this); PopupPanel panel = this.getPanel().orElse(null); if (panel == null) return; mPopup.removeAll(); panel.onShow(); for (ComponentListener cl : panel.getComponentListeners()) panel.removeComponentListener(cl); panel.addComponentListener(new ComponentAdapter() { @Override public void componentHidden(ComponentEvent e) { mPopup.close(); } }); mPopup.add(panel); mPopup.showPopup(); } abstract Optional<PopupPanel> getPanel(); } /** A modal popup invoked by a toggle button. * Cannot be instantiated on UI start! */ private static class ModalPopup extends WebPopup { private final AbstractButton mInvoker; private final WebPanel layerPanel; ModalPopup(AbstractButton invokerButton) { mInvoker = invokerButton; layerPanel = new WebPanel(); layerPanel.setOpaque(false); JRootPane rootPane = SwingUtils.getRootPane(mInvoker); if (rootPane == null) { throw new IllegalStateException("not on UI start, dummkopf!"); } installPopupLayer(layerPanel, rootPane); layerPanel.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { ModalPopup.this.close(); } }); this.setRequestFocusOnShow(false); } void showPopup() { layerPanel.setVisible(true); this.showAsPopupMenu(mInvoker); } void close() { this.hidePopup(); mInvoker.setSelected(false); layerPanel.setVisible(false); } // taken from com.alee.managers.popup.PopupManager private static void installPopupLayer(final WebPanel popupLayer, JRootPane rootPane) { final JLayeredPane layeredPane = rootPane.getLayeredPane(); popupLayer.setBounds(0, 0, layeredPane.getWidth(), layeredPane.getHeight()); layeredPane.add(popupLayer, JLayeredPane.DEFAULT_LAYER); layeredPane.revalidate(); layeredPane.addComponentListener(new ComponentAdapter() { @Override public void componentResized(final ComponentEvent e) { popupLayer.setBounds(0, 0, layeredPane.getWidth(), layeredPane.getHeight()); popupLayer.revalidate(); } }); final Window window = SwingUtils.getWindowAncestor(rootPane); window.addWindowStateListener(new WindowStateListener() { @Override public void windowStateChanged(final WindowEvent e) { popupLayer.setBounds(0, 0, layeredPane.getWidth(), layeredPane.getHeight()); popupLayer.revalidate(); } }); } } /** Base class for panels shown in a modal popup invoked by a toggle button. * Popup is closed if panel visibility is set to false. */ static class PopupPanel extends WebPanel { void onShow() {} } static class AddContactPanel extends PopupPanel { private final View mView; private final WebTabbedPane mTabbedPane; private final WebTextField mNameField; private final WebTextField mJIDField; private final WebTextField mServerField; private final WebTextField mNumberField; private final WebTextField mPrefixField; private final WebCheckBox mEncryptionBox; private final WebButton mSaveButton; AddContactPanel(View view) { mView = view; GroupPanel groupPanel = new GroupPanel(View.GAP_BIG, false); groupPanel.setMargin(View.MARGIN_BIG); groupPanel.add(new WebLabel(Tr.tr("Add Contact")).setBoldFont()); groupPanel.add(new WebSeparator(true, true)); // editable fields mNameField = new WebTextField(20); addListener(this, mNameField); groupPanel.add(new GroupPanel(View.GAP_DEFAULT, new WebLabel(Tr.tr("Name:")), mNameField)); mSaveButton = new WebButton(Tr.tr("Create")); mTabbedPane = new WebTabbedPane(); mTabbedPane.addChangeListener(new ChangeListener() { @Override public void stateChanged(ChangeEvent e) { AddContactPanel.this.checkSaveButton(); } }); WebPanel kontalkPanel = new WebPanel(new FormLayout(false, false, View.GAP_DEFAULT, View.GAP_DEFAULT)); kontalkPanel.setMargin(View.MARGIN_BIG); kontalkPanel.add(new WebLabel("Country code:")); PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance(); int countryCode = phoneUtil.getCountryCodeForRegion( Locale.getDefault().getCountry()); String prefix = "+"+Integer.toString(countryCode); mPrefixField = new WebTextField(prefix, 3); mPrefixField.setInputPrompt(prefix); addListener(this, mPrefixField); kontalkPanel.add(mPrefixField); kontalkPanel.add(new WebLabel("Number:")); mNumberField = new WebTextField(12); mNumberField.setInputPrompt("0123-9876543"); addListener(this, mNumberField); kontalkPanel.add(mNumberField); kontalkPanel.add(new WebLabel("Server:")); String serverText = Config.getInstance().getString(Config.SERV_HOST); mServerField = new WebTextField(serverText, 16); mServerField.setInputPrompt(serverText); addListener(this, mServerField); kontalkPanel.add(mServerField); mTabbedPane.addTab(Tr.tr("Kontalk Contact"), kontalkPanel); WebPanel jabberPanel = new WebPanel(new FormLayout(false, false, View.GAP_DEFAULT, View.GAP_DEFAULT)); jabberPanel.setMargin(View.MARGIN_BIG); jabberPanel.add(new WebLabel("Jabber ID:")); mJIDField = new WebTextField(20); mJIDField.setInputPrompt(Tr.tr("username")+"@jabber-server.com"); addListener(this, mJIDField); jabberPanel.add(mJIDField); mTabbedPane.addTab(Tr.tr("Jabber Contact"), jabberPanel); groupPanel.add(mTabbedPane); groupPanel.add(new WebSeparator(true, true)); mEncryptionBox = new WebCheckBox(Tr.tr("Encryption")); mEncryptionBox.setAnimated(false); mEncryptionBox.setSelected(true); groupPanel.add(mEncryptionBox); groupPanel.add(new WebSeparator(true, true)); this.add(groupPanel, BorderLayout.CENTER); mSaveButton.setEnabled(false); mSaveButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { AddContactPanel.this.saveContact(); AddContactPanel.this.setVisible(false); } }); GroupPanel buttonPanel = new GroupPanel(mSaveButton); buttonPanel.setLayout(new FlowLayout(FlowLayout.TRAILING)); this.add(buttonPanel, BorderLayout.SOUTH); } private void checkSaveButton() { mSaveButton.setEnabled(this.inputToJID().isValid()); } private void saveContact() { JID jid = this.inputToJID(); if (!jid.isValid()) // this shouldnt happen return; mView.getControl().createContact(jid, mNameField.getText(), mEncryptionBox.isSelected()); // reset fields mNameField.setText(""); mJIDField.setText(""); mNumberField.setText(""); } private JID inputToJID() { return mTabbedPane.getSelectedIndex() == 0 ? JID.bare(XMPPUtils.phoneNumberToKontalkLocal( mPrefixField.getText()+mNumberField.getText()), mServerField.getText()) : JID.bare(mJIDField.getText()); } private static void addListener(final AddContactPanel panel, WebTextField field) { field.getDocument().addDocumentListener(new DocumentChangeListener() { @Override public void documentChanged(DocumentEvent e) { panel.checkSaveButton(); } }); } } static class AddGroupChatPanel extends PopupPanel { private final View mView; private final Model mModel; private final WebTextField mSubjectField; private final ParticipantsList mList; private final WebButton mCreateButton; AddGroupChatPanel(View view, Model model) { mView = view; mModel = model; GroupPanel groupPanel = new GroupPanel(View.GAP_BIG, false); groupPanel.setMargin(View.MARGIN_BIG); groupPanel.add(new WebLabel(Tr.tr("Create Group")).setBoldFont()); groupPanel.add(new WebSeparator(true, true)); // editable fields mSubjectField = new WebTextField(20); mSubjectField.setDocument(new TextLimitDocument(View.MAX_SUBJ_LENGTH)); mSubjectField.getDocument().addDocumentListener(new DocumentChangeListener() { @Override public void documentChanged(DocumentEvent e) { AddGroupChatPanel.this.checkSaveButton(); } }); groupPanel.add(new GroupPanel(View.GAP_DEFAULT, new WebLabel(Tr.tr("Subject:")+" "), mSubjectField)); groupPanel.add(new WebLabel(Tr.tr("Select participants:")+" ")); mList = new ParticipantsList(); mList.setVisibleRowCount(6); mList.addListSelectionListener(new ListSelectionListener() { @Override public void valueChanged(ListSelectionEvent e) { AddGroupChatPanel.this.checkSaveButton(); } }); groupPanel.add(new ScrollPane(mList).setPreferredWidth(160)); this.add(groupPanel, BorderLayout.CENTER); mCreateButton = new WebButton(Tr.tr("Create")); mCreateButton.setEnabled(false); mCreateButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { AddGroupChatPanel.this.createGroup(); AddGroupChatPanel.this.setVisible(false); } }); GroupPanel buttonPanel = new GroupPanel(mCreateButton); buttonPanel.setLayout(new FlowLayout(FlowLayout.TRAILING)); this.add(buttonPanel, BorderLayout.SOUTH); } private void checkSaveButton() { mCreateButton.setEnabled(!mSubjectField.getText().isEmpty() && !mList.getSelectedContacts().isEmpty()); } private void createGroup() { GroupChat newChat = mView.getControl().createGroupChat( mList.getSelectedContacts(), mSubjectField.getText()).orElse(null); if (newChat != null) mView.showChat(newChat); mSubjectField.setText(""); } @Override protected void onShow() { List<Contact> contacts = new LinkedList<>(); for (Contact c : Utils.allContacts(mModel.contacts(), false)) { if (c.isKontalkUser() && !c.isMe()) contacts.add(c); } contacts.sort(new Comparator<Contact>() { @Override public int compare(Contact c1, Contact c2) { return Utils.compareContacts(c1, c2); } }); mList.setContacts(contacts); } } // Note: https://github.com/mgarin/weblaf/issues/153 static class ParticipantsList extends WebList { private final DefaultListModel<Contact> mModel; @SuppressWarnings("unchecked") ParticipantsList() { mModel = new DefaultListModel<>(); this.setModel(mModel); this.setFixedCellHeight(25); this.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); this.setSelectionModel(new DefaultListSelectionModel() { @Override public void setSelectionInterval(int index0, int index1) { if(super.isSelectedIndex(index0)) { super.removeSelectionInterval(index0, index1); } else { super.addSelectionInterval(index0, index1); } } }); this.setCellRenderer(new CellRenderer()); } void setContacts(List<Contact> contacts) { mModel.clear(); for (Contact contact : contacts) mModel.addElement(contact); } @SuppressWarnings("unchecked") List<Contact> getSelectedContacts() { return this.getSelectedValuesList(); } private class CellRenderer extends WebLabel implements ListCellRenderer<Contact> { @Override public Component getListCellRendererComponent(JList list, Contact contact, int index, boolean isSelected, boolean cellHasFocus) { this.setText(" " + Utils.displayName(contact)); this.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0,Color.LIGHT_GRAY)); return this; } } } // NOTE: https://github.com/mgarin/weblaf/issues/153 static class MemberList extends WebList { private final DefaultListModel<Member> mModel; @SuppressWarnings("unchecked") MemberList(boolean selectable) { mModel = new DefaultListModel<>(); this.setModel(mModel); this.setFixedCellHeight(25); this.setEnabled(selectable); if (selectable) { this.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); this.setSelectionModel(new DefaultListSelectionModel() { @Override public void setSelectionInterval(int index0, int index1) { if(super.isSelectedIndex(index0)) { super.removeSelectionInterval(index0, index1); } else { super.addSelectionInterval(index0, index1); } } }); } this.setCellRenderer(new CellRenderer()); } void setMembers(List<Member> members) { mModel.clear(); for (Member member : members) mModel.addElement(member); } private class CellRenderer extends WebPanel implements ListCellRenderer<Member> { private final WebLabel mNameLabel; private final WebLabel mRoleLabel; CellRenderer() { mNameLabel = new WebLabel(); mRoleLabel = new WebLabel(); mRoleLabel.setForeground(View.DARK_GREEN); this.setMargin(View.MARGIN_DEFAULT); this.setBorder(BorderFactory.createMatteBorder(0, 0, 1, 0, Color.LIGHT_GRAY)); this.add(new GroupPanel(GroupingType.fillMiddle, View.GAP_DEFAULT, mNameLabel, Box.createGlue(), mRoleLabel), BorderLayout.CENTER); } @Override public Component getListCellRendererComponent(JList list, Member member, int index, boolean isSelected, boolean cellHasFocus) { mNameLabel.setText( Utils.displayName(member.getContact(), View.MAX_NAME_IN_GROUP_LENGTH)); mRoleLabel.setText(Utils.role(member.getRole())); return this; } } } abstract static class PassPanel extends WebPanel { private final boolean mPassSet; private final WebCheckBox mSetPass; private final WebPasswordField mOldPassField; private final WebLabel mWrongPassLabel; private final WebPasswordField mNewPassField; private final WebPasswordField mConfirmPassField; PassPanel(boolean passSet) { mPassSet = passSet; GroupPanel groupPanel = new GroupPanel(View.GAP_DEFAULT, false); groupPanel.setMargin(View.MARGIN_SMALL); DocumentListener docListener = new DocumentListener() { @Override public void insertUpdate(DocumentEvent e) { PassPanel.this.checkDoneButton(); } @Override public void removeUpdate(DocumentEvent e) { PassPanel.this.checkDoneButton(); } @Override public void changedUpdate(DocumentEvent e) { PassPanel.this.checkDoneButton(); } }; mOldPassField = new WebPasswordField(30); mWrongPassLabel = new WebLabel(Tr.tr("Wrong password")); if (mPassSet) { groupPanel.add(new WebLabel(Tr.tr("Current password:"))); mOldPassField.getDocument().addDocumentListener(docListener); groupPanel.add(mOldPassField); mWrongPassLabel.setBoldFont(); mWrongPassLabel.setForeground(Color.RED); mWrongPassLabel.setVisible(false); groupPanel.add(mWrongPassLabel); groupPanel.add(new WebSeparator()); } mSetPass = new WebCheckBox(Tr.tr("Set key password")); String setPassText = Tr.tr("If not set, key is saved unprotected!"); TooltipManager.addTooltip(mSetPass, setPassText); groupPanel.add(new GroupPanel(mSetPass, new WebSeparator())); mSetPass.addItemListener(new ItemListener() { @Override public void itemStateChanged(ItemEvent e) { boolean selected = e.getStateChange() == ItemEvent.SELECTED; mNewPassField.setEnabled(selected); mConfirmPassField.setEnabled(selected); PassPanel.this.checkDoneButton(); } }); mNewPassField = new WebPasswordField(30); mNewPassField.setInputPrompt(Tr.tr("Enter new password")); mNewPassField.setEnabled(false); mNewPassField.setHideInputPromptOnFocus(false); mNewPassField.getDocument().addDocumentListener(docListener); groupPanel.add(mNewPassField); mConfirmPassField = new WebPasswordField(30); mConfirmPassField.setInputPrompt(Tr.tr("Confirm password")); mConfirmPassField.setEnabled(false); mConfirmPassField.setHideInputPromptOnFocus(false); mConfirmPassField.getDocument().addDocumentListener(docListener); groupPanel.add(mConfirmPassField); this.checkDoneButton(); this.add(groupPanel); } private void checkDoneButton() { if (mPassSet && mOldPassField.getPassword().length < 1) { this.onInvalidInput(); return; } if (!mSetPass.isSelected()) { this.onValidInput(); return; } char[] newPass = mNewPassField.getPassword(); if (newPass.length > 0 && Arrays.equals(newPass, mConfirmPassField.getPassword())) { this.onValidInput(); } else { this.onInvalidInput(); } } char[] getOldPassword() { return mOldPassField.getPassword(); } Optional<char[]> getNewPassword() { if (!mSetPass.isSelected()) return Optional.of(new char[0]); char[] newPass = mNewPassField.getPassword(); // better check again if (!Arrays.equals(newPass, mConfirmPassField.getPassword())) return Optional.empty(); return Optional.of(newPass); } void showWrongPassword() { mWrongPassLabel.setVisible(true); } abstract void onValidInput(); abstract void onInvalidInput(); } static class LabelTextField extends WebTextField { LabelTextField(int columns, Component focusGainer) { this("", null, false, columns, focusGainer); } LabelTextField(int maxTextLength, int columns, final Component focusGainer) { this("", maxTextLength, true, columns, focusGainer); } LabelTextField(String text, int maxTextLength, boolean editable, int columns, final Component focusGainer) { this(text, new TextLimitDocument(maxTextLength), editable, columns, focusGainer); } LabelTextField(String text, Document doc, boolean editable, int columns, final Component focusGainer) { super(doc, text, columns); this.setEditable(editable); this.setFocusable(true); if (editable) this.setTrailingComponent(new WebImage(Utils.getIcon("ic_ui_edit.png"))); else this.setBackground(null); // edit mode needs more height than label mode this.setMinimumHeight(30); this.setHideInputPromptOnFocus(false); this.addFocusListener(new FocusListener() { @Override public void focusGained(FocusEvent e) { LabelTextField.this.switchToEditMode(); } @Override public void focusLost(FocusEvent e) { LabelTextField.this.onFocusLost(); LabelTextField.this.switchToLabelMode(); } }); this.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { focusGainer.requestFocus(); } }); this.switchToLabelMode(); } private void switchToEditMode() { String text = this.editText(); this.setInputPrompt(text); this.setText(text); this.setCaretPosition(0); // "scroll" back this.setDrawBorder(true); Optional.ofNullable(this.getTrailingComponent()).ifPresent(c -> c.setVisible(false)); } private void switchToLabelMode() { this.setText(this.labelText()); this.setCaretPosition(0); // "scroll" back // layout problem here this.setDrawBorder(false); Optional.ofNullable(this.getTrailingComponent()).ifPresent(c -> c.setVisible(true)); } String labelText() { return this.getText(); } String editText() { return this.getText(); } void onFocusLost() {} } static class AttachmentPanel extends GroupPanel { private final WebLabel mStatus; private final WebLinkLabel mAttLabel; private final WebFileChooser mFileChooser; private File mFile = null; AttachmentPanel() { super(View.GAP_SMALL, false); this.add(mStatus = new WebLabel().setItalicFont()); this.add(mAttLabel = new WebLinkLabel()); mFileChooser = new WebFileChooser() { @Override public void approveSelection(){ File f = getSelectedFile(); if (f.exists()) { int option = JOptionPane.showConfirmDialog(this, String.format( Tr.tr("The file \"%s\" already exists, overwrite?"), f.getName()), Tr.tr("Overwrite File?"), JOptionPane.OK_CANCEL_OPTION); if (option != JOptionPane.YES_OPTION) return; // doing nothing means not approve } super.approveSelection(); } }; } /** Set image preview. */ void setAttachment(Path imagePath, Path linkPath) { this.setAttachment("", imagePath, linkPath); } /** Set link text. */ void setAttachment(String text, Path linkPath) { this.setAttachment(text, null, linkPath); } private void setAttachment(String text, Path imagePath, Path linkPath) { mFile = linkPath.toFile(); mAttLabel.setIcon(imagePath == null ? null : // file should be present and should be an image, show it ImageLoader.imageIcon(imagePath)); mAttLabel.setLink(text, Utils.createLinkRunnable(linkPath)); } @Override public JPopupMenu getComponentPopupMenu() { WebPopupMenu menu = new WebPopupMenu(); if (mFile == null) return null; // should never happen WebMenuItem saveMenuItem = new WebMenuItem(Tr.tr("Save File As…")); saveMenuItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent event) { if (mFile == null) return; // should never happen File suggestedFile = new File( mFileChooser.getCurrentDirectory(), mFile.getName()); mFileChooser.setSelectedFile(suggestedFile); // fix WebLaf bug mFileChooser.getFileChooserPanel().setSelectedFiles(new File[]{suggestedFile}); int option = mFileChooser.showSaveDialog(AttachmentPanel.this); if (option == JFileChooser.APPROVE_OPTION) { try { Files.copy(mFile.toPath(), mFileChooser.getSelectedFile().toPath(), StandardCopyOption.REPLACE_EXISTING); } catch (IOException ex) { LOGGER.log(Level.WARNING, "can't copy file", ex); } } } }); if (!mFile.exists()) { saveMenuItem.setEnabled(false); saveMenuItem.setToolTipText(Tr.tr("File does not exist")); } menu.add(saveMenuItem); return menu; } void setStatus(String text) { mStatus.setText(text); mAttLabel.setEnabled(text.isEmpty()); } } static class EncryptionPanel extends WebPanel { private static final Icon ICON_SECURE = Utils.getIcon("ic_ui_secure.png"); private static final Icon ICON_INSECURE = Utils.getIcon("ic_ui_insecure.png"); private final WebLabel mEncryptionIcon; private final WebLabel mEncryptionWarningIcon; EncryptionPanel() { mEncryptionIcon = new WebLabel(); mEncryptionWarningIcon = new WebLabel(Utils.getIcon("ic_ui_warning.png")); this.add(mEncryptionIcon, BorderLayout.WEST); this.add(mEncryptionWarningIcon, BorderLayout.EAST); } void setStatus(boolean isEncrypted, boolean canEncrypt) { mEncryptionIcon.setIcon(isEncrypted ? ICON_SECURE : ICON_INSECURE); mEncryptionWarningIcon.setVisible(isEncrypted != canEncrypt); String text = "<html>" + (isEncrypted ? Tr.tr("Encryption enabled") : Tr.tr("Encryption disabled")); if (isEncrypted && !canEncrypt) { text += "<br>" + Tr.tr("The contact's public key is missing"); } else if (!isEncrypted && canEncrypt) { text += "<br>" + Tr.tr("Encryption can be activated"); } TooltipManager.setTooltip(this, text + "</html>"); } } // Source: http://www.rgagnon.com/javadetails/java-0198.html static class TextLimitDocument extends PlainDocument { private final int mLimit; TextLimitDocument(int limit) { this.mLimit = limit; } @Override public void insertString( int offset, String str, AttributeSet attr) throws BadLocationException { if (str == null) return; if ((this.getLength() + str.length()) <= mLimit) { super.insertString(offset, str, attr); } } } // NOTE: no option to adjust image to component size like for WebImage, // -> component size depends on image size static class AvatarImage extends WebDecoratedImage { final int mSize; AvatarImage(int size) { mSize = size; this.setRound(0); } void setAvatarImage(Contact c) { this.setAvatarImg(AvatarLoader.load(c, mSize)); } void setAvatarImage(Chat c) { this.setAvatarImg(AvatarLoader.load(c, mSize)); } void setAvatarImg(AvatarImg avatarImg) { this.setDrawGlassLayer(avatarImg.isFallback); this.setImage(avatarImg.image); } } static abstract class EditableAvatarImage extends AvatarImage { private final WebFileChooser mImgChooser; private BufferedImage mImage = null; private boolean mImageChanged = false; EditableAvatarImage(int size) { this(size, true, Optional.empty()); } EditableAvatarImage(int size, boolean enabled, Optional<BufferedImage> image) { super(size); mImgChooser = new WebFileChooser(); mImgChooser.setFileFilter(new ImageFilesFilter()); mImage = image.orElse(null); this.setImageOrDefault(mImage); this.setGrayscale(!enabled); this.setEnabled(enabled); this.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() && enabled) { EditableAvatarImage.this.showPopupMenu(e); } } @Override public void mouseClicked(MouseEvent e) { if (e.getButton() == MouseEvent.BUTTON1 && enabled) { EditableAvatarImage.this.chooseImage(); } } }); TooltipManager.setTooltip(this, this.tooltipText()); } private void changeImage(BufferedImage image) { mImage = image; mImageChanged = true; this.setImageOrDefault(image); this.onImageChange(Optional.ofNullable(image)); TooltipManager.setTooltip(this, this.tooltipText()); } private void setImageOrDefault(BufferedImage image) { if (image == null) { this.setAvatarImg(this.defaultImage()); return; } this.setDrawGlassLayer(false); this.setImage(image); } void onImageChange(Optional<BufferedImage> optImage) {} boolean imageChanged() { return mImageChanged; } Optional<BufferedImage> getAvatarImage() { return Optional.ofNullable(mImage); } abstract AvatarLoader.AvatarImg defaultImage(); abstract boolean canRemove(); String tooltipText() { return this.canRemove() ? Tr.tr("Right click to unset") : Tr.tr("Click to choose image"); } void update() { AvatarImg img = this.defaultImage(); mImage = img.image; this.setDrawGlassLayer(img.isFallback); mImageChanged = false; this.setImage(mImage); } private void chooseImage() { int state = mImgChooser.showOpenDialog(this); if (state != WebFileChooser.APPROVE_OPTION) return; File imgFile = mImgChooser.getSelectedFile(); if (!imgFile.isFile()) return; BufferedImage img = MediaUtils.readImage(imgFile).orElse(null); if (img == null) return; this.changeImage(ImageUtils.createPreviewImage(img, mSize)); } private void showPopupMenu(MouseEvent e) { WebPopupMenu menu = new WebPopupMenu(); WebMenuItem removeItem = new WebMenuItem(Tr.tr("Remove")); removeItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent event) { EditableAvatarImage.this.changeImage(null); } }); removeItem.setEnabled(EditableAvatarImage.this.canRemove()); menu.add(removeItem); menu.show(this, e.getX(), e.getY()); } } }