package com.robonobo.gui.panels;
import static com.robonobo.gui.GuiUtil.*;
import static javax.swing.SwingUtilities.*;
import info.clearthought.layout.TableLayout;
import java.awt.Point;
import java.awt.event.*;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;
import javax.swing.*;
import javax.swing.border.Border;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.robonobo.common.concurrent.CatchingRunnable;
import com.robonobo.common.exceptions.SeekInnerCalmException;
import com.robonobo.core.api.model.Comment;
import com.robonobo.core.api.model.User;
import com.robonobo.core.metadata.CommentCallback;
import com.robonobo.core.metadata.UserCallback;
import com.robonobo.gui.RoboColor;
import com.robonobo.gui.RoboFont;
import com.robonobo.gui.components.base.*;
import com.robonobo.gui.frames.RobonoboFrame;
import com.robonobo.gui.sheets.ConfirmSheet;
@SuppressWarnings("serial")
public abstract class CommentsTabPanel extends JPanel {
static DateFormat df = new SimpleDateFormat("dd MMM yyyy HH:mm");
Set<Long> gotComments = new HashSet<Long>();
Map<Long, CommentPanel> pnlsById = new HashMap<Long, CommentPanel>();
List<CommentPanel> topLvlCps = new ArrayList<CommentPanel>();
JPanel cmtListPnl;
JScrollPane cmtListScrollPane;
NewCommentForm newCmtForm;
RButton newCmtBtn;
JPanel newCmtBtnPnl;
Log log = LogFactory.getLog(getClass());
RobonoboFrame frame;
public CommentsTabPanel(RobonoboFrame frame) {
this.frame = frame;
double[][] cellSizen = { { 5, TableLayout.FILL, 0 }, { 5, TableLayout.FILL, 0 } };
setLayout(new TableLayout(cellSizen));
cmtListPnl = new JPanel();
cmtListPnl.setOpaque(false);
BoxLayout bl = new BoxLayout(cmtListPnl, BoxLayout.Y_AXIS);
cmtListPnl.setLayout(bl);
cmtListPnl.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 10));
cmtListScrollPane = new JScrollPane(cmtListPnl);
cmtListScrollPane.setOpaque(false);
cmtListScrollPane.setBackground(RoboColor.MID_GRAY);
add(cmtListScrollPane, "1,1");
newCmtBtn = new RGlassButton("Post New Comment...");
newCmtBtn.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
CatchingRunnable hideNewCmtForm = new CatchingRunnable() {
public void doRun() throws Exception {
newCmtForm = null;
layoutComments();
newCmtBtn.setEnabled(true);
}
};
newCmtForm = new NewCommentForm(hideNewCmtForm, -1);
layoutComments();
newCmtBtn.setEnabled(false);
newCmtForm.textArea.requestFocusInWindow();
invokeLater(new CatchingRunnable() {
public void doRun() throws Exception {
// Scroll to this panel
int offset = 0;
for (CommentPanel cp : topLvlCps) {
offset += cp.getHeight();
}
cmtListScrollPane.getViewport().setViewPosition(new Point(0, offset));
}
});
}
});
// Ah, the joys of swing layout
newCmtBtnPnl = new JPanel();
double[][] ncbpCells = { { newCmtBtn.getPreferredSize().getWidth(), TableLayout.FILL }, { newCmtBtn.getPreferredSize().getHeight() } };
newCmtBtnPnl.setLayout(new TableLayout(ncbpCells));
newCmtBtnPnl.add(newCmtBtn, "0,0");
layoutComments();
}
private void layoutComments() {
runOnUiThread(new CatchingRunnable() {
public void doRun() throws Exception {
cmtListPnl.removeAll();
for (CommentPanel cp : topLvlCps) {
cmtListPnl.add(cp);
}
if (newCmtForm != null)
cmtListPnl.add(newCmtForm);
cmtListPnl.add(newCmtBtnPnl);
cmtListScrollPane.revalidate();
}
});
}
protected abstract boolean canRemoveComment(Comment c);
protected abstract void newComment(long parentCmtId, String text, CommentCallback cb);
public void addComments(Collection<Comment> comments) {
// Fetch all the userids before showing any of the comments as we need to ensure the comments are loaded in order so that parent comments are always there
Set<Long> uids = new HashSet<Long>();
for (Comment cmt : comments) {
uids.add(cmt.getUserId());
}
AddCommentCallback cb = new AddCommentCallback(uids, comments);
for (Long uid : uids) {
frame.ctrl.getOrFetchUser(uid, cb);
}
}
/** The height offset from the top of the comment list to just below this comment */
private int offsetToComment(CommentPanel cp) {
int offset = 0;
if (cp.c.getParentId() < 0) {
// Top level comment - offset is total height of all top-level cmts up to and including this one
for (CommentPanel tlcp : topLvlCps) {
offset += tlcp.getHeight();
if (tlcp == cp)
break;
}
} else {
// Nested comment - offset is total height of all siblings up to and including this cmt, plus
// 'internalOffset'
// (see below) of parent
CommentPanel parent = pnlsById.get(cp.c.getParentId());
for (CommentPanel sibling : parent.subPanels) {
offset += sibling.getHeight();
if (sibling == cp)
break;
}
offset += internalOffset(parent);
}
offset -= 10; // Otherwise it's a bit off
return offset;
}
/** The height of this comment, not including child comments, plus sibling comments above this one, plus the same
* calculation for parents */
private int internalOffset(CommentPanel cp) {
int result = cp.nameLbl.getHeight() + cp.dateLbl.getHeight() + cp.textLbl.getHeight() + cp.btnsPnl.getHeight() + 15;
long parentId = cp.c.getParentId();
if (parentId < 0) {
// This is a top-level comment
for (CommentPanel sibling : topLvlCps) {
if (sibling == cp)
break;
result += sibling.getHeight();
}
} else {
CommentPanel parent = pnlsById.get(parentId);
for (CommentPanel sibling : parent.subPanels) {
if (sibling == cp)
break;
result += sibling.getHeight();
}
result += internalOffset(parent);
}
return result;
}
public void clear() {
// Clears out list of comments (doesn't actually delete the comments themselves, just clears the display
gotComments.clear();
pnlsById.clear();
topLvlCps.clear();
layoutComments();
}
abstract class CommentRemover extends CatchingRunnable {
protected CommentPanel cp;
void doRemove(CommentPanel cp) {
this.cp = cp;
run();
}
}
class AddCommentCallback implements UserCallback {
Set<Long> usersToGet = new HashSet<Long>();
Collection<Comment> cl;
Map<Long, User> gotUsers = new HashMap<Long, User>();
public AddCommentCallback(Set<Long> usersToGet, Collection<Comment> cl) {
this.usersToGet.addAll(usersToGet);
this.cl = cl;
}
@Override
public void success(User gotUser) {
synchronized (this) {
long uid = gotUser.getUserId();
usersToGet.remove(uid);
gotUsers.put(uid, gotUser);
if(usersToGet.size() > 0)
return;
}
if(getWidth() == 0)
throw new SeekInnerCalmException();
runOnUiThread(new CatchingRunnable() {
public void doRun() throws Exception {
final int totalWidth = getWidth();
int topLvlWidth = totalWidth - 20;
int indent = 60;
for (Comment c : cl) {
if(pnlsById.containsKey(c.getCommentId()))
continue;
User u = gotUsers.get(c.getUserId());
boolean canRemove = canRemoveComment(c);
CommentRemover remover = null;
if (c.getParentId() <= 0) {
// Top-level comment
if (canRemove) {
remover = new CommentRemover() {
public void doRun() throws Exception {
topLvlCps.remove(cp);
pnlsById.remove(cp.c.getCommentId());
layoutComments();
}
};
}
CommentPanel pnl = new CommentPanel(c, u, topLvlWidth, 0, remover);
topLvlCps.add(pnl);
pnlsById.put(c.getCommentId(), pnl);
layoutComments();
} else if (pnlsById.containsKey(c.getParentId())) {
// Sub-comment to existing comment
final CommentPanel parent = pnlsById.get(c.getParentId());
int indentLvl = parent.indentLvl + 1;
if (canRemove) {
remover = new CommentRemover() {
public void doRun() throws Exception {
parent.removeSubPanel(cp);
}
};
}
CommentPanel pnl = new CommentPanel(c, u, (topLvlWidth - (indentLvl * indent)), indentLvl, remover);
pnlsById.put(c.getCommentId(), pnl);
parent.addSubPanel(pnl);
layoutComments();
} else {
// Oops
log.error("Cannot add comment " + c.getCommentId() + " - no parent comment " + c.getParentId());
}
}
}
});
}
@Override
public void error(long userId, Exception e) {
log.error("Error fetching user " + userId + " for comments");
}
}
class CommentPanel extends JPanel {
Comment c;
User u;
List<CommentPanel> subPanels = new ArrayList<CommentPanel>();
NewCommentForm newCmtForm;
RLabel nameLbl;
RLabel dateLbl;
JPanel extendoPanel;
LineBreakTextPanel textLbl;
JPanel btnsPnl;
int indentLvl;
public CommentPanel(Comment c, User u, int totalWidth, int indentLvl, final CommentRemover remover) {
this.c = c;
this.u = u;
this.indentLvl = indentLvl;
int textWidth = totalWidth - 80;
double[][] cellSizen = { { 0, 50, 10, TableLayout.FILL, 0 }, { 0, 50, TableLayout.FILL, 0 } };
setLayout(new TableLayout(cellSizen));
add(new JLabel(imgIconFromUrl(u.getImgUrl())), "1,1");
extendoPanel = new JPanel();
BoxLayout epl = new BoxLayout(extendoPanel, BoxLayout.Y_AXIS);
extendoPanel.setLayout(epl);
Border botLine = BorderFactory.createMatteBorder(0, 0, 1, 0, RoboColor.DARKISH_GRAY);
Border margin = BorderFactory.createEmptyBorder(0, 0, 5, 0);
extendoPanel.setBorder(BorderFactory.createCompoundBorder(margin, botLine));
nameLbl = new RLabel16B(u.getFriendlyName());
nameLbl.setAlignmentX(LEFT_ALIGNMENT);
dateLbl = new RLabel11(df.format(c.getDate()));
dateLbl.setAlignmentX(LEFT_ALIGNMENT);
textLbl = new LineBreakTextPanel(c.getText(), RoboFont.getFont(13, false), textWidth);
textLbl.setAlignmentX(LEFT_ALIGNMENT);
add(extendoPanel, "3,1,3,2");
btnsPnl = new JPanel();
btnsPnl.setAlignmentX(LEFT_ALIGNMENT);
double[][] btnsCellSizen = { { 80, 10, 100, TableLayout.FILL }, { 5, 30, 5 } };
btnsPnl.setLayout(new TableLayout(btnsCellSizen));
RButton replyBtn = new RGlassButton("Reply");
replyBtn.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
CatchingRunnable hideNewCmt = new CatchingRunnable() {
public void doRun() throws Exception {
newCmtForm = null;
relayoutPanel();
}
};
final int scrollOffset = offsetToComment(CommentPanel.this);
newCmtForm = new NewCommentForm(hideNewCmt, CommentPanel.this.c.getCommentId());
relayoutPanel();
newCmtForm.textArea.requestFocusInWindow();
invokeLater(new CatchingRunnable() {
public void doRun() throws Exception {
// Scroll to this form
cmtListScrollPane.getViewport().setViewPosition(new Point(0, scrollOffset));
}
});
}
});
btnsPnl.add(replyBtn, "0,1");
if (remover != null) {
RButton deleteBtn = new RRedGlassButton("Delete");
final CatchingRunnable doRemove = new CatchingRunnable() {
public void doRun() throws Exception {
remover.doRemove(CommentPanel.this);
frame.ctrl.deleteComment(CommentPanel.this.c, new CommentCallback() {
public void success(Comment c) {
// Do nothing
}
public void error(long commentId, Exception ex) {
log.error("Error deleting comment", ex);
}
});
}
};
deleteBtn.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
frame.showSheet(new ConfirmSheet(frame, "Delete comment?", "Are you sure you want to delete this comment and all replies?", "Delete", doRemove));
}
});
btnsPnl.add(deleteBtn, "2,1");
}
relayoutPanel();
}
public void addSubPanel(CommentPanel sp) {
subPanels.add(sp);
relayoutPanel();
}
public void removeSubPanel(CommentPanel cp) {
subPanels.remove(cp);
relayoutPanel();
}
private void relayoutPanel() {
runOnUiThread(new CatchingRunnable() {
public void doRun() throws Exception {
extendoPanel.removeAll();
extendoPanel.add(nameLbl);
extendoPanel.add(dateLbl);
extendoPanel.add(Box.createVerticalStrut(5));
extendoPanel.add(textLbl);
extendoPanel.add(Box.createVerticalStrut(5));
extendoPanel.add(btnsPnl);
extendoPanel.add(Box.createVerticalStrut(5));
for (JPanel sp : subPanels) {
sp.setAlignmentX(LEFT_ALIGNMENT);
extendoPanel.add(sp);
}
if (newCmtForm != null) {
newCmtForm.setAlignmentX(LEFT_ALIGNMENT);
extendoPanel.add(newCmtForm);
}
extendoPanel.revalidate();
}
});
}
}
class NewCommentForm extends JPanel {
RTextArea textArea;
RButton postBtn;
public NewCommentForm(final Runnable remover, final long parentCmtId) {
double[][] cellSizen = { { 0, 50, 10, TableLayout.FILL, 100, 10, 100, 10 }, { 0, 25, 0, 60, 10, 30, 10 } };
setLayout(new TableLayout(cellSizen));
User me = frame.ctrl.getMyUser();
add(new JLabel(imgIconFromUrl(me.getImgUrl())), "1,1,1,3,LEFT,TOP");
String lblText = (parentCmtId < 0) ? "Post New Comment" : "Post Reply";
add(new RLabel14B(lblText), "3,1,LEFT,TOP");
textArea = new RTextArea(3, 50);
textArea.addKeyListener(new KeyAdapter() {
public void keyReleased(KeyEvent e) {
postBtn.setEnabled(textArea.getText().length() > 0);
}
});
add(new JScrollPane(textArea), "3,3,6,3");
RButton cancelBtn = new RRedGlassButton("Cancel");
cancelBtn.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
remover.run();
}
});
add(cancelBtn, "4,5");
postBtn = new RGlassButton("Post");
postBtn.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
postBtn.setText("Posting...");
postBtn.setEnabled(false);
newComment(parentCmtId, textArea.getText(), new CommentCallback() {
public void success(Comment c) {
runOnUiThread(remover);
}
public void error(long commentId, Exception ex) {
// Should never happen
runOnUiThread(remover);
log.error("Error posting comment", ex);
}
});
}
});
postBtn.setEnabled(false);
add(postBtn, "6,5");
}
}
}