/*
* 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.AbstractAction;
import javax.swing.Action;
import javax.swing.Box;
import javax.swing.Icon;
import javax.swing.JComponent;
import javax.swing.KeyStroke;
import javax.swing.ListSelectionModel;
import javax.swing.SwingUtilities;
import javax.swing.TransferHandler;
import javax.swing.text.AbstractDocument;
import javax.swing.text.BadLocationException;
import javax.swing.text.BoxView;
import javax.swing.text.ComponentView;
import javax.swing.text.Element;
import javax.swing.text.IconView;
import javax.swing.text.LabelView;
import javax.swing.text.ParagraphView;
import javax.swing.text.Style;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyledDocument;
import javax.swing.text.StyledEditorKit;
import javax.swing.text.ViewFactory;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.ComponentOrientation;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.nio.file.Path;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.alee.extended.panel.FlowPanel;
import com.alee.extended.panel.GroupPanel;
import com.alee.extended.panel.GroupingType;
import com.alee.laf.label.WebLabel;
import com.alee.laf.menu.WebMenuItem;
import com.alee.laf.menu.WebPopupMenu;
import com.alee.laf.panel.WebPanel;
import com.alee.laf.text.WebEditorPane;
import com.alee.laf.text.WebTextPane;
import com.alee.managers.tooltip.TooltipManager;
import org.apache.commons.lang.time.DateUtils;
import org.jivesoftware.smackx.chatstates.ChatState;
import org.kontalk.crypto.Coder;
import org.kontalk.misc.JID;
import org.kontalk.model.Contact;
import org.kontalk.model.chat.Chat;
import org.kontalk.model.message.InMessage;
import org.kontalk.model.message.KonMessage;
import org.kontalk.model.message.MessageContent.Attachment;
import org.kontalk.model.message.MessageContent.GroupCommand;
import org.kontalk.model.message.MessageContent.InAttachment;
import org.kontalk.model.message.OutMessage;
import org.kontalk.model.message.Transmission;
import org.kontalk.persistence.Config;
import org.kontalk.util.Tr;
import org.kontalk.view.ChatView.Background;
import org.kontalk.view.ComponentUtils.AttachmentPanel;
/**
* View all messages of one chat in a left/right MIM style list.
*
* @author Alexander Bikadorov {@literal <bikaejkb@mail.tu-berlin.de>}
*/
final class MessageList extends ListView<KonMessage> {
private static final Logger LOGGER = Logger.getLogger(MessageList.class.getName());
private static final Icon PENDING_ICON = Utils.getIcon("ic_msg_pending.png");
private static final Icon SENT_ICON = Utils.getIcon("ic_msg_sent.png");
private static final Icon DELIVERED_ICON = Utils.getIcon("ic_msg_delivered.png");
private static final Icon ERROR_ICON = Utils.getIcon("ic_msg_error.png");
private static final Icon WARNING_ICON = Utils.getIcon("ic_msg_warning.png");
private static final Icon CRYPT_ICON = Utils.getIcon("ic_msg_crypt.png");
private static final Icon UNENCRYPT_ICON = Utils.getIcon("ic_msg_unencrypt.png");
private static final Icon CRYPT_WARNING_ICON = Utils.getIcon("ic_msg_crypt_warning.png");
private static final WrapEditorKit FIX_WRAP_KIT = new WrapEditorKit();
private static final WebPopupMenu TEXT_COPY_MENU = Utils.createCopyMenu(false);
private final ChatView mChatView;
private final Chat mChat;
private Background mBackground = null;
MessageList(View view, ChatView chatView, Chat chat) {
// render and editor item are equal (but not the same!)
super(view,
new MessageListFlyWeightItem(view),
new MessageListFlyWeightItem(view),
// allow multiple selections for "copy" action
ListSelectionModel.MULTIPLE_INTERVAL_SELECTION,
true,
false);
mChatView = chatView;
mChat = chat;
//this.setEditable(false);
//this.setAutoscrolls(true);
this.setOpaque(false);
// hide grid
this.setShowGrid(false);
// copy values to clipboard using the in-build 'copy' action, invoked by custom right-click
// menu or default ctrl+c shortcut
this.setTransferHandler(new CopyTransferHandler(mView));
this.updateOnEDT(null);
}
Chat getChat() {
return mChat;
}
Optional<Background> getBG() {
return Optional.ofNullable(mBackground);
}
@Override
protected void updateOnEDT(Object arg) {
if (arg == null || arg == Chat.ViewChange.VIEW_SETTINGS) {
this.setBackground(mChat.getViewSettings());
if (mChatView.getCurrentChat().orElse(null) == mChat) {
//mChatView.mScrollPane.getViewport().repaint();
mChatView.repaint();
}
}
// check for new messages to add
if ((arg == null || arg == Chat.ViewChange.NEW_MESSAGE) &&
this.getModel().getRowCount() < mChat.getMessages().size()) {
this.insertMessages();
}
if ((arg == null || arg == Chat.ViewChange.READ) &&
!mChat.isRead() && mView.chatIsVisible(mChat)) {
mChat.setRead();
}
if (arg == Chat.ViewChange.MEMBER_STATE) {
// show/hide "is writing..." for last message
// or hide "is writing..." for -now- second to last message after new message was added
this.updateRowRendering(this.getRowCount() - 2, this.getRowCount() - 1);
this.scrollToRow(this.getRowCount() - 1);
this.repaint(); // swing...
}
}
private void insertMessages() {
boolean newAdded = this.sync(mChat.getMessages().getAll());
if (newAdded) {
//this.scrollToRow(this.getRowCount() -1);
mChatView.setScrollDown();
}
}
private void setBackground(Chat.ViewSettings s) {
// simply overwrite
mBackground = mChatView.createBGOrNull(s);
}
void updateMessageFontSize() {
mRenderItem.configUpdate();
mEditorItem.configUpdate();
this.updateRowRendering(0, this.getRowCount() - 1);
}
@Override
protected WebPopupMenu rightClickMenu(List<KonMessage> selectedValues) {
WebPopupMenu menu = new WebPopupMenu();
Action copyAction = new AbstractAction(Tr.tr("Copy")) {
@Override
public void actionPerformed(ActionEvent e) {
Action copy = MessageList.this.getActionMap().get("copy");
ActionEvent ae = new ActionEvent(MessageList.this, ActionEvent.ACTION_PERFORMED, "");
copy.actionPerformed(ae);
}
};
copyAction.putValue(Action.ACCELERATOR_KEY, KeyStroke.getKeyStroke("control C"));
menu.add(copyAction);
if (selectedValues.isEmpty()) {
LOGGER.warning("no values");
return menu;
}
if (selectedValues.size() > 1)
return menu;
final KonMessage m = selectedValues.get(0);
if (m instanceof InMessage) {
InMessage im = (InMessage) m;
if (m.isEncrypted()) {
WebMenuItem decryptMenuItem = new WebMenuItem(Tr.tr("Decrypt"));
decryptMenuItem.setToolTipText(Tr.tr("Retry decrypting message"));
decryptMenuItem.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent event) {
mView.getControl().decryptAgain(im);
}
});
menu.add(decryptMenuItem);
}
InAttachment att = m.getContent().getInAttachment().orElse(null);
if (att != null && att.getFilename().isEmpty()) {
WebMenuItem attMenuItem = new WebMenuItem(Tr.tr("Load"));
attMenuItem.setToolTipText(Tr.tr("Retry downloading attachment"));
attMenuItem.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent event) {
mView.getControl().downloadAgain(im);
}
});
menu.add(attMenuItem);
}
} else if (m instanceof OutMessage) {
if (m.getStatus() == KonMessage.Status.ERROR) {
WebMenuItem sendMenuItem = new WebMenuItem(Tr.tr("Retry"));
sendMenuItem.setToolTipText(Tr.tr("Retry sending message"));
sendMenuItem.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent event) {
mView.getControl().sendAgain((OutMessage) m);
}
});
menu.add(sendMenuItem);
}
}
return menu;
}
@Override
public int compare(KonMessage o1, KonMessage o2) {
int idComp = Integer.compare(o1.getID(), o2.getID());
int dateComp = o1.getDate().compareTo(o2.getDate());
return (idComp == 0 || dateComp == 0) ? idComp : dateComp;
}
/**
* Flyweight render item for message items.
* The content is added to a panel inside this panel.
* Above the content panel a centered date panel is drawn (if appropriate).
*/
private static class MessageListFlyWeightItem extends FlyweightItem<KonMessage> {
private final View mView;
private final WebPanel mDateMarginPanel;
private final WebPanel mDatePanel;
private final WebLabel mDateLabel;
private final WebPanel mFlowPanel;
private final WebPanel mPanel;
private final WebLabel mFromLabel;
private final WebTextPane mTextPane;
// container for status icons and date, with own tooltip
private final WebPanel mStatusPanel;
// TODO use WebImages
private final WebLabel mStatusIconLabel;
private final WebLabel mEncryptIconLabel;
private final WebLabel mTimeLabel;
private final WebPanel mWritingPanel;
private final WebLabel mWritingLabel;
private final AttachmentPanel mAttPanel;
private final LinkUtils.Linkifier mLinkifier;
private final Style mMeCommandStyle;
MessageListFlyWeightItem(View view) {
mView = view;
this.setOpaque(false);
mDatePanel = new WebPanel();
mDatePanel.setRound(View.ROUND);
mDatePanel.setWebColoredBackground(false);
mDatePanel.setBackground(View.BLUE);
mDatePanel.setBorderColor(View.BLUE);
mDateLabel = new WebLabel();
mDateLabel.setForeground(Color.WHITE);
mDatePanel.add(mDateLabel, BorderLayout.CENTER);
mDateMarginPanel = new GroupPanel(mDatePanel);
this.add(new GroupPanel(GroupingType.fillFirstAndLast,
Box.createGlue(), mDateMarginPanel, Box.createGlue())
.setMargin(0),
BorderLayout.NORTH);
// FlowLayout to toggle left/right position of panel (see below)
mFlowPanel = new FlowPanel(0);
//this.setBorder(new EmptyBorder(10, 10, 10, 10));
mFlowPanel.setBackground(View.BLUE); // seen when selected
mPanel = new WebPanel(true);
mPanel.setRound(View.ROUND);
mPanel.setWebColoredBackground(false);
mPanel.setMargin(View.MARGIN_TINY);
mFromLabel = new WebLabel();
mFromLabel.setFontSize(View.FONT_SIZE_SMALL);
mFromLabel.setForeground(Color.BLUE);
mFromLabel.setItalicFont();
mPanel.add(mFromLabel, BorderLayout.NORTH);
// text area
mTextPane = new WebTextPane();
mTextPane.setEditable(false);
mTextPane.setOpaque(false);
// sets default font
mTextPane.putClientProperty(WebEditorPane.HONOR_DISPLAY_PROPERTIES, true);
// for detecting link clicks
mTextPane.addMouseListener(LinkUtils.CLICK_LISTENER);
// for detecting motion
mTextPane.addMouseMotionListener(LinkUtils.MOTION_LISTENER);
// fix word wrap for long words
mTextPane.setEditorKit(FIX_WRAP_KIT);
// right click menu
mTextPane.setComponentPopupMenu(TEXT_COPY_MENU);
// text styling
mLinkifier = new LinkUtils.Linkifier(mTextPane.getStyledDocument());
mMeCommandStyle = mTextPane.addStyle(null, null);
StyleConstants.setForeground(mMeCommandStyle, View.GREEN);
// attachment
mAttPanel = new AttachmentPanel();
// layout: content panel
WebPanel mContentPanel = new WebPanel();
mContentPanel.setOpaque(false);
mContentPanel.setMargin(View.MARGIN_SMALL);
mContentPanel.add(mTextPane, BorderLayout.CENTER);
mContentPanel.add(mAttPanel, BorderLayout.SOUTH);
mPanel.add(mContentPanel, BorderLayout.CENTER);
// status panel...
mStatusPanel = new WebPanel();
mStatusPanel.setOpaque(false);
mStatusPanel.setLayout(new FlowLayout());
mStatusIconLabel = new WebLabel();
mStatusPanel.add(mStatusIconLabel);
mEncryptIconLabel = new WebLabel();
mStatusPanel.add(mEncryptIconLabel);
mTimeLabel = new WebLabel();
mTimeLabel.setForeground(Color.GRAY);
mStatusPanel.add(mTimeLabel);
WebPanel southPanel = new WebPanel();
southPanel.setOpaque(false);
southPanel.add(mStatusPanel, BorderLayout.EAST);
mPanel.add(southPanel, BorderLayout.SOUTH);
mFlowPanel.add(mPanel);
this.add(mFlowPanel, BorderLayout.CENTER);
mWritingPanel = new WebPanel();
mWritingPanel.setRound(View.ROUND);
mWritingPanel.setWebColoredBackground(false);
mWritingPanel.setBackground(Color.WHITE);
mWritingPanel.setBorderColor(Color.WHITE);
mWritingLabel = new WebLabel();
mWritingLabel.setForeground(View.DARK_RED);
mWritingPanel.add(mWritingLabel, BorderLayout.CENTER);
this.add(new GroupPanel(GroupingType.fillLast, mWritingPanel, Box.createGlue())
.setMargin(0),
BorderLayout.SOUTH);
// set font size
this.configUpdate();
}
@Override
protected void configUpdate() {
int textFontSize;
int timeFontSize;
switch(Config.getInstance().getInt(Config.VIEW_MESSAGE_FONT_SIZE)) {
case 1:
textFontSize = timeFontSize = View.FONT_SIZE_TINY; break;
case 2:
textFontSize = View.FONT_SIZE_NORMAL;
timeFontSize = View.FONT_SIZE_SMALL; break;
case 3:
textFontSize = View.FONT_SIZE_BIG;
timeFontSize = View.FONT_SIZE_NORMAL; break;
default:
textFontSize = View.FONT_SIZE_SMALL;
timeFontSize = View.FONT_SIZE_TINY;
}
mDateLabel.setFontSize(textFontSize);
mTextPane.setFontSize(textFontSize);
mTimeLabel.setFontSize(timeFontSize);
}
@Override
protected void render(KonMessage value, int listWidth, boolean isSelected, boolean isLast) {
KonMessage last = value.getPredecessor().orElse(null);
boolean showDateSeparator = last == null ||
!DateUtils.isSameDay(last.getDate(), value.getDate());
mDatePanel.setVisible(showDateSeparator); // otherwise visible on mouse over (?)
mDateMarginPanel.setMargin(showDateSeparator ? View.MARGIN_SMALL : 0);
boolean consecutive = last == null || last.getSender().equals(value.getSender());
mDatePanel.setMargin(showDateSeparator || !consecutive ? View.MARGIN_SMALL : 0);
// decoration consumes space, even if nothing is visible in panel
mDatePanel.setUndecorated(!showDateSeparator);
mDateLabel.setText(showDateSeparator ?
Utils.getDateSeparatorText(value.getDate()) : "");
// background (flow item panel)
mFlowPanel.setOpaque(isSelected);
boolean isOut = !value.isInMessage();
// toggle left/right position
mFlowPanel.setComponentOrientation(!isOut ?
ComponentOrientation.LEFT_TO_RIGHT : ComponentOrientation.RIGHT_TO_LEFT);
// background (message panel)
boolean hasGroupCommand = value.getContent().getGroupCommand().isPresent();
Color color =
hasGroupCommand ? View.LIGHT_GREEN :
value.isInMessage() ? Color.WHITE : View.LIGHT_BLUE;
mPanel.setBackground(color);
mPanel.setBorderColor(color);
// from label
mFromLabel.setVisible(value.isInMessage() && value.getChat().isGroupChat());
// icon
if (value.getCoderStatus().isSecure()) {
mEncryptIconLabel.setIcon(CRYPT_ICON);
} else {
mEncryptIconLabel.setIcon(UNENCRYPT_ICON);
}
// date label
mTimeLabel.setText(Utils.SHORT_DATE_FORMAT.format(value.isInMessage() ?
value.getServerDate().orElse(value.getDate()) :
value.getDate()));
// title
mFromLabel.setText(!value.getContent().getGroupCommand().isPresent() &&
value instanceof InMessage ?
" " + getFromString((InMessage) value) :
"");
// text in text area
String text = messageToString(value, mView, false);
if (value.getContent().getGroupCommand().isPresent()) {
mTextPane.setText(text);
mTextPane.setFontStyle(false, true);
} else {
mTextPane.setFontStyle(false, value.isEncrypted());
StyledDocument document = mTextPane.getStyledDocument();
try {
document.remove(0, document.getLength());
// output implementation of the "/me" command, XEP-0245
if (text.startsWith(View.THE_ME_COMMAND)) {
Contact sender = value.getSender().orElse(null);
// NOTE: not updated if sender name changes, people have to live with it
String meName = (sender == null ? Tr.tr("Me") : sender.getName()) + " ";
document.insertString(0, meName, mMeCommandStyle);
text = text.substring(View.THE_ME_COMMAND.length());
}
mLinkifier.linkify(text);
} catch (BadLocationException ex) {
LOGGER.log(Level.WARNING, "can't set styled document text", ex);
}
}
// hide area if there is no text
mTextPane.setVisible(!text.isEmpty());
// status
Date deliveredDate = null;
Set<Transmission> transmissions = value.getTransmissions();
if (transmissions.size() == 1)
deliveredDate = transmissions.iterator().next().getReceivedDate().orElse(null);
// status icon
Icon statusIcon = null;
if (isOut) {
if (deliveredDate != null) {
statusIcon = DELIVERED_ICON;
} else {
switch (value.getStatus()) {
case PENDING :
statusIcon = PENDING_ICON;
break;
case SENT :
statusIcon = SENT_ICON;
break;
case RECEIVED:
// legacy
statusIcon = DELIVERED_ICON;
break;
case ERROR:
statusIcon = ERROR_ICON;
break;
default:
LOGGER.warning("unknown message receipt status!?");
}
}
} else { // IN message
if (!value.getCoderStatus().getErrors().isEmpty()) {
statusIcon = WARNING_ICON;
}
}
mStatusIconLabel.setIcon(statusIcon);
// encryption icon
Coder.Encryption enc = value.getCoderStatus().getEncryption();
Coder.Signing sign = value.getCoderStatus().getSigning();
boolean noSecurity = enc == Coder.Encryption.NOT && sign == Coder.Signing.NOT;
boolean fullSecurity = enc == Coder.Encryption.DECRYPTED &&
((isOut && sign == Coder.Signing.SIGNED) ||
(!isOut && sign == Coder.Signing.VERIFIED));
mEncryptIconLabel.setIcon(
noSecurity ? UNENCRYPT_ICON :
fullSecurity ? CRYPT_ICON :
CRYPT_WARNING_ICON);
// tooltip
String html = "<html><body>" + /*"<h3>Header</h3>"+*/ "<br>";
if (isOut) {
String secStat = null;
Date statusDate;
if (deliveredDate != null) {
secStat = Tr.tr("Received:");
statusDate = deliveredDate;
} else {
statusDate = value.getServerDate().orElse(null);
switch (value.getStatus()) {
case PENDING:
break;
case SENT:
secStat = Tr.tr("Sent:");
break;
// legacy
case RECEIVED:
secStat = Tr.tr("Received:");
break;
case ERROR:
secStat = Tr.tr("Error report:");
break;
default:
LOGGER.warning("unexpected msg status: "+value.getStatus());
}
}
String status = statusDate != null ?
Utils.MID_DATE_FORMAT.format(statusDate) :
null;
String create = Utils.MID_DATE_FORMAT.format(value.getDate());
if (!create.equals(status))
html += Tr.tr("Created:")+ " " + create + "<br>";
if (status != null && secStat != null)
html += secStat + " " + status + "<br>";
} else { // IN message
Date receivedDate = value.getDate();
String rec = Utils.MID_DATE_FORMAT.format(receivedDate);
Date sentDate = value.getServerDate().orElse(null);
if (sentDate != null) {
String sent = Utils.MID_DATE_FORMAT.format(sentDate);
if (!sent.equals(rec))
html += Tr.tr("Sent:")+ " " + sent + "<br>";
}
html += Tr.tr("Received:")+ " " + rec + "<br>";
}
// usual states
String sec = noSecurity ? Tr.tr("Not encrypted") :
fullSecurity ? Tr.tr("Secure") :
null;
if (sec == null) {
// unusual states
String encryption = Tr.tr("Unknown");
switch (enc) {
case NOT: encryption = Tr.tr("Not encrypted"); break;
case ENCRYPTED: encryption = Tr.tr("Encrypted"); break;
case DECRYPTED: encryption = Tr.tr("Decrypted"); break;
}
String verification = Tr.tr("Unknown");
switch (sign) {
case NOT: verification = Tr.tr("Not signed"); break;
case SIGNED: verification = Tr.tr("Not verified"); break;
case VERIFIED: verification = Tr.tr("Verified"); break;
}
sec = encryption + " / " + verification;
}
html += Tr.tr("Encryption")+": " + sec + "<br>";
String errors = "";
for (Coder.Error error: value.getCoderStatus().getErrors()) {
errors += error.toString() + " <br> ";
}
if (!errors.isEmpty())
html += Tr.tr("Security errors")+": " + errors;
String serverErrText = value.getServerError().text;
if (!serverErrText.isEmpty())
html += Tr.tr("Server error")+": " + serverErrText + " <br> ";
// TODO temporary catching for tracing bug
try {
TooltipManager.setTooltip(mStatusPanel, html);
} catch (NullPointerException ex) {
LOGGER.log(Level.WARNING, "cant set tooltip", ex);
LOGGER.warning("statusPanel="+mStatusPanel+",html="+html);
LOGGER.warning("edt: "+SwingUtilities.isEventDispatchThread());
}
// attachment / image; NOTE: loading many (big) images is very slow
Attachment att = value.getContent().getAttachment().orElse(null);
mAttPanel.setVisible(att != null);
if (att != null) {
Path imagePath = value.getContent().getPreview()
.map(p -> p.getImagePath(value.getID())).orElse(null);
Path linkPath = att.getFilePath();
if (imagePath != null && !imagePath.toString().isEmpty())
mAttPanel.setAttachment(imagePath, linkPath);
else
mAttPanel.setAttachment(linkPath.getFileName().toString(), linkPath);
// status text
String statusText;
if (!linkPath.toString().isEmpty() && !att.isEncrypted()) {
// file should exist, no status needed
statusText = "";
} else {
statusText = Tr.tr("Attachment:") + " ";
if (att.isEncrypted()) {
statusText += Tr.tr("encrypted");
} else {
switch (att.getDownloadProgress()) {
case -1: statusText += Tr.tr("stalled"); break;
case 0:
case -2: statusText += Tr.tr("downloading…"); break;
case -3: statusText += Tr.tr("download failed"); break;
default: statusText += Tr.tr("loading…");
}
}
}
mAttPanel.setStatus(statusText);
}
// resetting size
mTextPane.setSize(Short.MAX_VALUE, Short.MAX_VALUE);
mTextPane.setPreferredSize(null);
// calculate preferred width
// NOTE: on the very first call the list width is zero (?)
int maxWidth = (int)(listWidth * 0.8);
mTextPane.setSize(Short.MAX_VALUE, Short.MAX_VALUE);
//mTextPane.setText(content); // already done
int prefWidth = mTextPane.getPreferredSize().width;
// calculate preferred height now with fixed width
int width = Math.min(prefWidth, maxWidth);
mTextPane.setSize(width, Short.MAX_VALUE);
int height = mTextPane.getPreferredSize().height;
Dimension prefSize = new Dimension(width, height);
mTextPane.setSize(prefSize);
// textArea does not need this but textPane does, and editorPane
// is again totally different; I love Swing
mTextPane.setPreferredSize(prefSize);
boolean showWriting = isLast
&& value.getChat().getAllMembers().stream()
.anyMatch(m -> m.getState() == ChatState.composing);
mWritingPanel.setMargin(showWriting ? View.MARGIN_SMALL : 0);
// decoration consumes space, even if nothing is visible in panel
mWritingPanel.setUndecorated(!showWriting);
mWritingLabel.setText(showWriting ? Tr.tr("is writing…") : "");
}
}
private static String getFromString(InMessage message) {
return Utils.displayName(message.getContact(), message.getJID(), View.MAX_NAME_IN_FROM_LABEL);
}
/**
* Fix for the infamous "Wrap long words" problem in Java 7+.
* Source: https://stackoverflow.com/a/13375811
*/
private static class WrapEditorKit extends StyledEditorKit {
final ViewFactory defaultFactory = new WrapColumnFactory();
@Override
public ViewFactory getViewFactory() {
return defaultFactory;
}
private static class WrapColumnFactory implements ViewFactory {
@Override
public javax.swing.text.View create(Element elem) {
String kind = elem.getName();
if (kind != null) {
switch (kind) {
case AbstractDocument.ContentElementName:
return new WrapLabelView(elem);
case AbstractDocument.ParagraphElementName:
return new ParagraphView(elem);
case AbstractDocument.SectionElementName:
return new BoxView(elem, javax.swing.text.View.Y_AXIS);
case StyleConstants.ComponentElementName:
return new ComponentView(elem);
case StyleConstants.IconElementName:
return new IconView(elem);
}
}
// default to text display
return new LabelView(elem);
}
}
private static class WrapLabelView extends LabelView {
public WrapLabelView(Element elem) {
super(elem);
}
@Override
public float getMinimumSpan(int axis) {
switch (axis) {
case javax.swing.text.View.X_AXIS:
return 0;
case javax.swing.text.View.Y_AXIS:
return super.getMinimumSpan(axis);
default:
throw new IllegalArgumentException("Invalid axis: " + axis);
}
}
}
}
// overwriting non-public BasicTableUI.TableTransferHandler for copy action
private static class CopyTransferHandler extends TransferHandler {
private final View mView;
private CopyTransferHandler(View view) {
mView = view;
}
protected Transferable createTransferable(JComponent c) {
if (!(c instanceof MessageList)) {
return null;
}
List<KonMessage> messages = ((MessageList) c).getSelectedValues();
if (messages.isEmpty()) {
return null;
}
StringBuilder plainBuf = new StringBuilder();
for (KonMessage m : messages) {
String val = messageToString(m, mView, true);
plainBuf.append(val).append("\n"); // NOTE: newline after last line
}
//return new BasicTransferable(plainBuf.toString(), htmlBuf.toString());
return new StringSelection(plainBuf.toString());
}
public int getSourceActions(JComponent c) {
return COPY;
}
}
private static String messageToString(KonMessage message, View view, boolean copy) {
String pre = "";
if (copy) {
String date = Utils.LONG_DATE_FORMAT.format(message.getDate());
String from = message instanceof InMessage ?
getFromString((InMessage) message) :
Tr.tr("me"); // TODO get my name
Attachment att = message.getContent().getAttachment().orElse(null);
String as = att == null ? "" : "[" + att.getFilename() + "] ";
pre = date + " - " + from + " : " + as;
}
String text = "";
GroupCommand com = message.getContent().getGroupCommand().orElse(null);
if (com != null) {
InMessage inMessage = message instanceof InMessage ?
(InMessage) message : null;
String somebody = inMessage != null ?
getFromString(inMessage) : Tr.tr("You");
switch (com.getOperation()) {
case CREATE:
text = String.format(Tr.tr("%1$s created this group"), somebody);
break;
case LEAVE:
text = String.format(Tr.tr("%1$s left this group"), somebody);
break;
case SET:
String subject = com.getSubject();
if (!subject.isEmpty()) {
text = String.format(Tr.tr("%1$s set the subject to \"%2$s\""), somebody, subject);
}
List<JID> added = com.getAdded();
if (!added.isEmpty()) {
text = String.format(Tr.tr("%1$s added %2$s"), somebody, view.names(added));
}
List<JID> removed = com.getRemoved();
if (!removed.isEmpty()) {
text = String.format(Tr.tr("%1$s removed %2$s"), somebody, view.names(removed));
}
if (text.isEmpty()) {
text = somebody + " did something wrong";
}
break;
}
if (copy)
text = "[" + text + "]";
} else {
text = message.isEncrypted() ?
Tr.tr("[encrypted]") :
// removing whitespace (Pidgin adds weird tab characters)
message.getContent().getText().trim();
}
return pre + text;
}
}