package chatty.gui.components;
import chatty.Helper;
import chatty.gui.components.menus.ContextMenuListener;
import chatty.gui.components.menus.StreamInfosContextMenu;
import chatty.util.DateTime;
import chatty.util.api.StreamInfo;
import java.awt.Color;
import java.awt.Component;
import java.awt.Font;
import java.awt.Rectangle;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import javax.swing.BorderFactory;
import javax.swing.DefaultListCellRenderer;
import javax.swing.JList;
import javax.swing.JPopupMenu;
import javax.swing.JTextArea;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.border.Border;
import javax.swing.border.TitledBorder;
/**
*
* @author tduva
*/
public class LiveStreamsList extends JList<StreamInfo> {
private static final int UPDATE_TIMER_DELAY = 5;
private static final int CHECK_DELAY = 20;
private static final int REPAINT_DELAY = 60;
private final SortedListModel<StreamInfo> data;
private final List<ContextMenuListener> contextMenuListeners;
private final LiveStreamListener liveStreamListener;
/**
* How long after the last stream status change, that it uses the TITLE_NEW
* border.
*/
private static final int STREAMINFO_NEW_TIME = 180;
private ListDataChangedListener listDataChangedListener;
private JPopupMenu lastContextMenu;
private long lastChecked = 0;
private long lastRepainted = 0;
public LiveStreamsList(LiveStreamListener liveStreamListener) {
data = new SortedListModel<>();
setModel(data);
setCellRenderer(new MyCellRenderer());
contextMenuListeners = new ArrayList<>();
this.liveStreamListener = liveStreamListener;
addListeners();
new UpdateTimer();
}
public void addContextMenuListener(ContextMenuListener listener) {
if (listener != null) {
contextMenuListeners.add(listener);
}
}
public void addStreams(List<StreamInfo> infos) {
for (StreamInfo info : infos) {
addStream(info);
}
}
public void setComparator(Comparator<StreamInfo> comparator) {
data.setComparator(comparator);
}
/**
* Adds or removes and readds a stream.
*
* @param info
*/
public void addStream(StreamInfo info) {
if (info.isValid() && info.getOnline()) {
if (data.contains(info)) {
data.remove(info);
}
data.add(info);
itemAdded(info);
} else if (data.contains(info)) {
data.remove(info);
itemRemoved(info);
}
listDataChanged();
}
/**
* Adds the listener to notify about list data changes.
*
* @param listener
*/
public void addListDataChangedListener(ListDataChangedListener listener) {
this.listDataChangedListener = listener;
}
/**
* Checks all added streams and removes invalid ones.
*/
private void checkStreams() {
if ((System.currentTimeMillis() - lastChecked) / 1000 < CHECK_DELAY) {
return;
}
lastChecked = System.currentTimeMillis();
Set<StreamInfo> remove = new HashSet<>();
for (StreamInfo info : data) {
if (!info.isValid() || !info.getOnline()) {
remove.add(info);
}
}
// Remove invalid items
for (StreamInfo info : remove) {
data.remove(info);
itemRemoved(info);
}
// Update and inform only if items were actually removed
if (remove.isEmpty()) {
listDataChanged();
}
}
/**
* Clears the selection if dialog is not active.
*/
private void checkToClearSelection() {
if (!isFocusOwner() &&
(lastContextMenu == null || !lastContextMenu.isVisible())) {
clearSelection();
}
}
/**
* Call to all the regular update stuff.
*/
private void update() {
checkToClearSelection();
checkStreams();
checkToRepaint();
}
/**
* Repaints the list on a set delay to update colors.
*/
private void checkToRepaint() {
long timePassed = (System.currentTimeMillis() - lastRepainted) / 1000;
if (timePassed > REPAINT_DELAY) {
repaint();
lastRepainted = System.currentTimeMillis();
}
}
/**
* Inform the listener that the list data has changed (new items, removed
* items, updated new streams count).
*/
private void listDataChanged() {
if (listDataChangedListener != null) {
listDataChangedListener.listDataChanged();
}
}
private void itemRemoved(StreamInfo item) {
if (listDataChangedListener != null) {
listDataChangedListener.itemRemoved(item);
}
}
private void itemAdded(StreamInfo item) {
if (listDataChangedListener != null) {
listDataChangedListener.itemAdded(item);
}
}
/**
* Open context menu for this user, if the event points at one.
*
* @param e
*/
private void openContextMenu(MouseEvent e) {
if (e.isPopupTrigger()) {
selectClicked(e, false);
List<StreamInfo> selected = getSelectedValuesList();
StreamInfosContextMenu m = new StreamInfosContextMenu(selected, true);
for (ContextMenuListener cml : contextMenuListeners) {
m.addContextMenuListener(cml);
}
lastContextMenu = m;
m.show(this, e.getX(), e.getY());
}
}
/**
* Adds selection of the clicked element, or removes selection if no
* element was clicked.
*
* @param e
* @param onlyOutside
*/
private void selectClicked(MouseEvent e, boolean onlyOutside) {
int index = locationToIndex(e.getPoint());
Rectangle bounds = getCellBounds(index, index);
if (bounds != null && bounds.contains(e.getPoint())) {
if (!onlyOutside) {
if (isSelectedIndex(index)) {
addSelectionInterval(index, index);
} else {
setSelectedIndex(index);
}
}
} else {
clearSelection();
}
}
private void addListeners() {
ComponentListener l = new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent e) {
// Trick from kleopatra:
// http://stackoverflow.com/questions/7306295/swing-jlist-with-multiline-text-and-dynamic-height
// next line possible if list is of type JXList
// list.invalidateCellSizeCache();
// for core: force cache invalidation by temporarily setting fixed height
setFixedCellHeight(10);
setFixedCellHeight(-1);
}
};
addComponentListener(l);
addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
selectClicked(e, true);
openContextMenu(e);
}
@Override
public void mouseReleased(MouseEvent e) {
openContextMenu(e);
}
@Override
public void mouseClicked(MouseEvent e) {
if (e.getClickCount() == 2) {
StreamInfo info = getSelectedValue();
if (info != null && liveStreamListener != null) {
liveStreamListener.liveStreamClicked(info);
}
}
}
});
}
/**
* To prevent horizontal scrolling and allow for tracking of the viewport
* width.
*
* @return
*/
@Override
public boolean getScrollableTracksViewportWidth() {
return true;
}
/**
* Custom renderer to use a text area and borders etc.
*/
private static class MyCellRenderer extends DefaultListCellRenderer {
private static final Border PADDING =
BorderFactory.createEmptyBorder(2, 3, 1, 3);
private static final Border MARGIN =
BorderFactory.createEmptyBorder(4, 3, 1, 3);
private static final Border TITLE =
BorderFactory.createMatteBorder(1, 0, 0, 0, Color.LIGHT_GRAY);
private static final Border TITLE_SELECTED =
BorderFactory.createMatteBorder(1, 0, 0, 0, new Color(165,165,165));
private static final Border TITLE_NEW =
BorderFactory.createMatteBorder(1, 0, 0, 0, Color.BLACK);
private final JTextArea area;
public MyCellRenderer() {
area = new JTextArea();
area.setLineWrap(true);
area.setWrapStyleWord(true);
}
@Override
public Component getListCellRendererComponent(JList list, Object value,
int index, boolean isSelected, boolean cellHasFocus) {
if (value == null) {
area.setText(null);
return area;
}
//System.out.println("Getting rubberstamp for "+value);
StreamInfo info = (StreamInfo)value;
// Make Text
String text = info.getTitle();
if (!info.getGame().isEmpty()) {
text += "\n("+info.getGame()+")";
}
area.setText(text);
// Adjust size
int width = list.getWidth();
if (width > 0) {
area.setSize(width, Short.MAX_VALUE);
}
// Add Borders
String title = String.format("%s (%s | %s)",
info.getCapitalizedName(),
Helper.formatViewerCount(info.getViewers()),
DateTime.agoUptimeCompact(info.getTimeStartedWithPicnic()));
Border titleBaseBorder = isSelected ? TITLE_SELECTED : TITLE;
if (info.getStatusChangeTimeAgo() < STREAMINFO_NEW_TIME) {
titleBaseBorder = TITLE_NEW;
}
Border titleBorder = BorderFactory.createTitledBorder(titleBaseBorder,
title, TitledBorder.CENTER, TitledBorder.TOP, null, null);
Border innerBorder = BorderFactory.createCompoundBorder(titleBorder, PADDING);
Border border = BorderFactory.createCompoundBorder(MARGIN, innerBorder);
area.setBorder(border);
// Selected Color
if (isSelected) {
area.setBackground(list.getSelectionBackground());
area.setForeground(list.getSelectionForeground());
} else {
area.setBackground(list.getBackground());
area.setForeground(list.getForeground());
}
return area;
}
}
/**
* Periodically check what of the list should be updated. This is used for
* clearing focus, removing old elements etc.
*/
private class UpdateTimer extends Timer {
public UpdateTimer() {
TimerTask task = new TimerTask() {
@Override
public void run() {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
update();
}
});
}
};
this.schedule(task, UPDATE_TIMER_DELAY*1000, UPDATE_TIMER_DELAY*1000);
}
}
public interface ListDataChangedListener {
void listDataChanged();
void itemRemoved(StreamInfo item);
void itemAdded(StreamInfo item);
}
}