/* * Kontalk Java client * Copyright (C) 2016 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 javax.swing.JComponent; import javax.swing.SwingConstants; import javax.swing.SwingUtilities; import javax.swing.TransferHandler; import javax.swing.event.DocumentEvent; import javax.swing.text.AbstractDocument; import javax.swing.text.AttributeSet; import javax.swing.text.BadLocationException; import javax.swing.text.DocumentFilter; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.DataFlavor; import java.awt.datatransfer.UnsupportedFlavorException; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.io.File; import java.io.IOException; import java.util.List; import com.alee.extended.panel.WebOverlay; import com.alee.laf.label.WebLabel; import com.alee.laf.text.WebTextArea; import com.alee.utils.swing.DocumentChangeListener; import org.kontalk.util.EncodingUtils; import org.kontalk.util.Tr; /** * Text composing area for writing a message. * * @author Alexander Bikadorov {@literal <bikaejkb@mail.tu-berlin.de>} */ final class ComposingArea { private final ChatView mChatView; private final WebTextArea mTextArea; private final ComponentUtils.ScrollPane mScrollPane; private final WebOverlay mOverlay; private final WebLabel mOverlayLabel; private final FileDropHandler mDropHandler; ComposingArea(ChatView chatView) { mChatView = chatView; mTextArea = new WebTextArea(); mTextArea.setMargin(View.MARGIN_SMALL); mTextArea.setBorder(null); mTextArea.setLineWrap(true); mTextArea.setWrapStyleWord(true); mTextArea.setFontSize(View.FONT_SIZE_NORMAL); mTextArea.setInputPrompt(Tr.tr("Type to compose")); mTextArea.setInputPromptHorizontalPosition(SwingConstants.LEFT); mTextArea.setComponentPopupMenu(Utils.createCopyMenu(true)); mTextArea.getDocument().addDocumentListener(new DocumentChangeListener() { @Override public void documentChanged(DocumentEvent e) { mChatView.onKeyTypeEvent(e.getDocument().getLength() == 0); } }); mScrollPane = new ComponentUtils.ScrollPane(mTextArea, false); mScrollPane.setBorder(null); // text area overlay mOverlayLabel = new WebLabel().setBoldFont(); mOverlay = new WebOverlay(mScrollPane, mOverlayLabel, SwingConstants.CENTER, SwingConstants.CENTER); mOverlay.setUndecorated(false) .setMargin(View.MARGIN_SMALL) .setWebColoredBackground(false); // old transfer handler of text area is fallback for bottom panel mDropHandler = new FileDropHandler(mTextArea.getTransferHandler()); mTextArea.setTransferHandler(mDropHandler); // when text changed... mTextArea.getDocument().addDocumentListener(new DocumentChangeListener() { @Override public void documentChanged(DocumentEvent e) { // these are strange times SwingUtilities.invokeLater(new Runnable() { public void run() { ComposingArea.this.adjustSize(); } }); } }); ((AbstractDocument) mTextArea.getDocument()).setDocumentFilter(new DocumentFilter() { @Override public void replace(FilterBypass fb, int offset, int length, String string, AttributeSet attr) throws BadLocationException { // input implementation of the "/me" command, XEP-0245 if (length == 0 && offset == 0 && string.equals("/")) { fb.insertString(0, View.THE_ME_COMMAND, attr); return; } super.replace(fb, offset, length, string, attr); } }); // ...or window is resized mChatView.addComponentListener(new ComponentAdapter() { @Override public void componentResized(ComponentEvent e) { ComposingArea.this.adjustSize(); } }); } private void adjustSize() { int newHeight = mTextArea.getPreferredSize().height; int maxHeight = mChatView.getHeight() / 3; mScrollPane.setPreferredSize(new Dimension(mScrollPane.getWidth(), newHeight < maxHeight ? // grow newHeight + 1 : // +1 for border // fixed height maxHeight)); // swing does not figure this out itself mChatView.revalidate(); } FileDropHandler getDropHandler() { return mDropHandler; } void focus() { mTextArea.requestFocusInWindow(); } void setHotkeys(boolean enterSends) { for (KeyListener l : mTextArea.getKeyListeners()) mTextArea.removeKeyListener(l); mTextArea.addKeyListener(new KeyListener() { @Override public void keyPressed(KeyEvent e) { if (enterSends && e.getKeyCode() == KeyEvent.VK_ENTER && e.getModifiersEx() == KeyEvent.CTRL_DOWN_MASK) { e.consume(); mTextArea.append(EncodingUtils.EOL); } 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) { } }); } void setEnabled(boolean enabled, boolean isMember) { mTextArea.setEnabled(enabled); Color textBG = enabled ? Color.WHITE : Color.LIGHT_GRAY; mTextArea.setBackground(textBG); mOverlay.setBackground(textBG); mOverlayLabel.setText(isMember ? Tr.tr("You are not a member of this group") : ""); } String getText() { return mTextArea.getText(); } void reset() { mTextArea.setText(""); } boolean isFocused() { return SwingUtilities.getWindowAncestor(mChatView).getFocusOwner() == mTextArea; } Component getComponent() { return mOverlay; } final class FileDropHandler extends TransferHandler { private final TransferHandler mTextHandler; private boolean mDropEnabled = true; private FileDropHandler(TransferHandler textHandler) { mTextHandler = textHandler; } void setDropEnabled(boolean enabled) { mDropEnabled = enabled; } @Override public boolean canImport(TransferSupport support) { if (support.isDrop() && !mDropEnabled) return false; if (support.getComponent() == mTextArea && mTextHandler.canImport(support)) return true; for (DataFlavor flavor : support.getDataFlavors()) { if (flavor.isFlavorJavaFileListType()) { return true; } } return false; } @Override @SuppressWarnings("unchecked") public boolean importData(TransferSupport support) { if (!this.canImport(support)) return false; if (support.getComponent() == mTextArea && mTextHandler.importData(support)) { // dropping text on text area was handled return true; } List<File> files; try { files = (List<File>) support.getTransferable() .getTransferData(DataFlavor.javaFileListFlavor); } catch (UnsupportedFlavorException | IOException ex) { // should never happen (or JDK is buggy) return false; } for (File file : files) { if (Utils.isAllowedAttachmentFile(file)) { mChatView.sendFile(file); } } return !files.isEmpty(); } @Override public void exportToClipboard(JComponent comp, Clipboard clip, int action) throws IllegalStateException { mTextHandler.exportToClipboard(comp, clip, action); } } }