/*
Eteria IRC Client, an RFC 1459 compliant client program written in Java.
Copyright (C) 2000-2001 Javier Kohen <jkohen at tough.com>
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 2 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, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package ar.com.jkohen.awt;
import java.awt.*;
import java.awt.event.*;
import java.text.Collator;
import java.text.CollationKey;
import java.util.Enumeration;
import java.util.Vector;
import ar.com.jkohen.irc.Modes;
import ar.com.jkohen.irc.RFC1459;
import ar.com.jkohen.irc.User;
import ar.com.jkohen.util.CollatedHashtable;
public class NickList extends ScrollPane implements MouseListener, MouseMotionListener, ItemSelectable
{
public static final int SYMBOL_RENDERER = 0;
public static final int BULLET_RENDERER = 1;
public static final int SORT_ALPHA = 0;
public static final int SORT_ALPHA_FAVOR_MODE = 1;
private static final int PREF_MIN_WIDTH = 190;
private int item_renderer = BULLET_RENDERER;
private int sort_method = SORT_ALPHA;
private MouseListener mouseListener;
private ActionListener actionListener;
private ItemListener itemListener;
protected MyList screen_list;
private NickInfoPopup nick_pop;
private Point mouse_coords;
private CollatedHashtable items;
private Collator collator;
private Color textbg = SystemColor.text;
private Color textfg = SystemColor.textText;
private Color selbg = SystemColor.textHighlight;
private Color selfg = SystemColor.textHighlightText;
private String last_selected_nick;
// SORT_*_FAVOR_MODE masks. These are ordered by priority.
private static final int [] SORT_MASKS = { User.OWNER_MASK, User.ADMIN_MASK, User.OP_MASK, User.HALFOP_MASK, User.VOICE_MASK, User.NORMAL_MASK };
private static final String SORT_CHARS = "123456";
public NickList()
{
super(ScrollPane.SCROLLBARS_ALWAYS); // _AS_NEEDED is so buggy...
this.collator = RFC1459.getCollator();
this.items = new CollatedHashtable(collator);
Panel p = new Panel();
CardLayout layout = new CardLayout();
p.setLayout(layout);
this.screen_list = new MyList();
this.nick_pop = new NickInfoPopup();
p.add(screen_list, "list");
p.add(nick_pop, "popup");
add(p);
screen_list.addMouseListener(this);
screen_list.addMouseMotionListener(this);
}
public String [] getNicks()
{
String [] list = new String [items.size()];
int i = 0;
for (Enumeration e = items.keys(); e.hasMoreElements(); )
list[i++] = (String)e.nextElement();
return list;
}
/**
* Gets the nick name for the item that contains <code>p</code>.
*
* @param p the point to look for in the nicks list.
*/
public String getNickAt(Point p)
{
NickItem ni = getNickItemAt(p);
if (ni != null)
return ni.getNick();
else
return(null);
}
/**
* Gets the NickItem that contains <code>p</code>.
*
* @param p the point to look for in the NickList.
*/
protected NickItem getNickItemAt(Point p)
{
for (Enumeration e = items.elements(); e.hasMoreElements(); )
{
NickItem ni = (NickItem)e.nextElement();
Rectangle r = new Rectangle(ni.getBounds().x, ni.getBounds().y, screen_list.getBounds().width, ni.getBounds().height);
if (r.contains(p))
return ni;
}
return null;
}
public void loadList(User [] users)
{
items.clear();
for (int i = 0; i < users.length; i++)
add(users[i].getTag(), users[i].getModes());
// validate();
repaint();
}
/**
* Adds <code>nick</code> to the list with user mode <code>mode</code>.
*/
public void add(String nick, int mode)
{
NickItem ni = new NickItem(nick);
switch (item_renderer)
{
case BULLET_RENDERER:
ni.setBullet(true);
break;
case SYMBOL_RENDERER:
ni.setBullet(false);
break;
default:
throw new IllegalArgumentException("Invalid NickItem renderer selected");
}
ni.setForeground(getForeground());
ni.setBackground(getBackground());
ni.setSelectedForeground(selfg);
ni.setSelectedBackground(selbg);
ni.setModes(mode);
ni.setTextBackground(textbg);
if ((mode & User.OWNER_MASK) != 0)
ni.setTextForeground(User.COLOR_OWNER);
else if ((mode & User.ADMIN_MASK) != 0)
ni.setTextForeground(User.COLOR_ADMIN);
else if ((mode & User.OP_MASK) != 0)
ni.setTextForeground(User.COLOR_OP);
else if ((mode & User.HALFOP_MASK) != 0)
ni.setTextForeground(User.COLOR_HOP);
else if ((mode & User.VOICE_MASK) != 0)
ni.setTextForeground(User.COLOR_VOICE);
else
ni.setTextForeground(NickInfo.nickToColor(nick));
items.put(nick, ni);
}
protected void setItemSelected(NickItem ni, boolean state)
{
ni.setSelected(state);
int item_state = state ? ItemEvent.SELECTED : ItemEvent.DESELECTED;
processItemEvent(new ItemEvent(this, ItemEvent.ITEM_STATE_CHANGED, this, item_state));
screen_list.repaint();
}
protected void unselectAllItems()
{
for (Enumeration e = items.elements(); e.hasMoreElements(); )
{
NickItem ni = (NickItem) e.nextElement();
setItemSelected(ni, false);
}
}
public int getSortMethod()
{
return sort_method;
}
public void setSortMethod(int method)
{
this.sort_method = method;
repaint();
}
public String getSelectedItem()
{
return last_selected_nick;
}
public Object[] getSelectedObjects()
{
Vector temp_list = new Vector(items.size());
for (Enumeration e = items.elements(); e.hasMoreElements(); )
{
NickItem ni = (NickItem) e.nextElement();
if (ni.isSelected())
temp_list.addElement(ni.getNick());
}
Object[] objs = new String[temp_list.size()];
temp_list.copyInto(objs);
return objs;
}
public int getItemRenderer()
{
return item_renderer;
}
public void setItemRenderer(int item_renderer)
{
this.item_renderer = item_renderer;
}
public void repaint()
{
screen_list.setDirty();
screen_list.repaint();
validate();
}
public void setFont(Font f)
{
screen_list.setFont(f);
nick_pop.setFont(f);
repaint();
}
public Color getTextBackground()
{
return(textbg);
}
public void setTextBackground(Color c)
{
super.setBackground(c);
screen_list.setBackground(c);
this.textbg = c;
for (Enumeration e = items.elements(); e.hasMoreElements(); )
{
NickItem ni = (NickItem) e.nextElement();
ni.setTextBackground(c);
}
repaint();
}
public Color getTextForeground()
{
return textfg;
}
public void setTextForeground(Color c)
{
this.textfg = c;
for (Enumeration e = items.elements(); e.hasMoreElements(); )
{
NickItem ni = (NickItem) e.nextElement();
ni.setTextForeground(c);
}
}
public Color getSelectedBackground()
{
return selbg;
}
public void setSelectedBackground(Color c)
{
this.selbg = c;
for (Enumeration e = items.elements(); e.hasMoreElements(); )
{
NickItem ni = (NickItem) e.nextElement();
ni.setSelectedBackground(c);
}
}
public Color getSelectedForeground()
{
return selfg;
}
public void setSelectedForeground(Color c)
{
this.selfg = c;
for (Enumeration e = items.elements(); e.hasMoreElements(); )
{
NickItem ni = (NickItem) e.nextElement();
ni.setSelectedForeground(c);
}
}
public Dimension getPreferredSize()
{
Dimension d = super.getPreferredSize();
d.width = Math.min(d.width, PREF_MIN_WIDTH);
return(d);
}
/**
* Translate the event as if it were thrown by this object.
*/
protected MouseEvent translateListMouseEvent(MouseEvent ev)
{
return new MouseEvent(this, ev.getID(), ev.getWhen(), ev.getModifiers(), ev.getX(), ev.getY(), ev.getClickCount(), ev.isPopupTrigger());
}
public void addMouseListener(MouseListener l)
{
if (null != l)
mouseListener = AWTEventMulticaster.add(mouseListener, l);
}
public void removeMouseListener(MouseListener l)
{
if (null != l)
mouseListener = AWTEventMulticaster.remove(mouseListener, l);
}
public void addActionListener(ActionListener l)
{
if (null != l)
actionListener = AWTEventMulticaster.add(actionListener, l);
}
public void removeActionListener(ActionListener l)
{
if (null != l)
actionListener = AWTEventMulticaster.remove(actionListener, l);
}
protected void processActionEvent(ActionEvent e)
{
if (actionListener != null)
actionListener.actionPerformed(e);
}
public void addItemListener(ItemListener l)
{
if (null != l)
itemListener = AWTEventMulticaster.add(itemListener, l);
}
public void removeItemListener(ItemListener l)
{
if (null != l)
itemListener = AWTEventMulticaster.remove(itemListener, l);
}
protected void processItemEvent(ItemEvent e)
{
if (itemListener != null)
itemListener.itemStateChanged(e);
}
public void mouseClicked(MouseEvent ev)
{
Object source = ev.getSource();
if (source.equals(screen_list))
{
if (mouseListener != null)
mouseListener.mouseClicked(translateListMouseEvent(ev));
NickItem ni = getNickItemAt(ev.getPoint());
if (null == ni)
return;
int ev_modifiers = ev.getModifiers();
if ((ev_modifiers & MouseEvent.BUTTON1_MASK) != 0 || ev_modifiers == 0)
{
// WORKAROUND: #32 Netscape Navigator doesn't set it right for BUTTON1.
String nick = ni.getNick();
switch (ev.getClickCount())
{
case 1:
boolean previously_selected = ni.isSelected();
if (!previously_selected)
last_selected_nick = nick;
if (0 == (ev_modifiers & MouseEvent.CTRL_MASK))
unselectAllItems();
setItemSelected(ni, !previously_selected);
break;
case 2:
processActionEvent(new ActionEvent(NickList.this, ActionEvent.ACTION_PERFORMED, nick, ev.getModifiers()));
break;
}
}
}
}
public void mousePressed(MouseEvent ev)
{
Object source = ev.getSource();
if (source.equals(screen_list))
if (mouseListener != null)
mouseListener.mousePressed(translateListMouseEvent(ev));
}
public void mouseReleased(MouseEvent ev)
{
Object source = ev.getSource();
if (source.equals(screen_list))
if (mouseListener != null)
mouseListener.mouseReleased(translateListMouseEvent(ev));
}
public void mouseEntered(MouseEvent ev)
{
Object source = ev.getSource();
if (source.equals(this))
if (mouseListener != null)
mouseListener.mouseEntered(ev);
}
public void mouseExited(MouseEvent ev)
{
Object source = ev.getSource();
if (source.equals(this))
{
if (mouseListener != null)
mouseListener.mouseExited(ev);
}
else if (source.equals(screen_list))
{
mouse_coords = null;
repaint();
}
}
public void mouseMoved(MouseEvent ev)
{
Object source = ev.getSource();
if (source.equals(screen_list))
{
mouse_coords = ev.getPoint();
repaint();
}
}
public void mouseDragged(MouseEvent ev)
{
}
protected class MyList extends Canvas implements ComponentListener
{
private Image image;
private Graphics img_gfx;
private NickItem [] items; // This obscures items in NickList.
private Dimension items_size;
private boolean dirty_items;
MyList()
{
this.dirty_items = true;
NickList.this.addComponentListener(this);
}
public void setDirty()
{
this.dirty_items = true;
}
private void createImageBuffer(Dimension size)
{
image = createImage(size.width, size.height);
if (img_gfx != null)
img_gfx.dispose();
img_gfx = image.getGraphics();
}
public void update(Graphics g)
{
paint(g);
}
public void paint(Graphics g)
{
if (dirty_items)
{
setSize(getPreferredSize());
super.repaint();
return;
}
Dimension size = getSize();
if (null == img_gfx || size.width != image.getWidth(this) || size.height != image.getHeight(this))
createImageBuffer(size);
img_gfx.setColor(getBackground());
img_gfx.fillRect(0, 0, size.width, size.height);
img_gfx.setColor(getForeground());
for (int i = 0; i < items.length; i++)
{
Rectangle r = items[i].getBounds();
// Draw item.
Graphics ng = img_gfx.create(r.x, r.y, size.width, r.height);
items[i].paint(ng);
// items[i].setSelected(false);
if (ng != null)
ng.dispose();
}
paintRollover(img_gfx);
g.drawImage(image, 0, 0, this);
}
private void paintRollover(Graphics g)
{
nick_pop.setVisible(false);
if (mouse_coords != null)
{
NickItem ni = getNickItemAt(mouse_coords);
String n = null;
if (ni != null)
{
// ni.setSelected(true);
n = getNickAt(mouse_coords);
if (n != null)
{
nick_pop.setNick(n);
nick_pop.setVisible(true);
}
}
nick_pop.setLocation(mouse_coords);
}
nick_pop.paint(g);
}
public Dimension getMinimumSize()
{
return getPreferredSize();
}
public Dimension getPreferredSize()
{
if (dirty_items)
{
grabItems();
sortItems();
Graphics g = null != img_gfx ? img_gfx : getGraphics();
int y = 0;
int max_width = 0;
for (int i = 0; i < items.length; i++)
{
Dimension d = items[i].getSize(g);
items[i].setBounds(0, y, d.width, d.height);
max_width = Math.max(max_width, d.width);
y += d.height;
}
this.items_size = new Dimension(max_width, y);
}
return adjustToPane(items_size.width, items_size.height);
}
private Dimension adjustToPane(int w, int h)
{
Dimension vp = NickList.this.getViewportSize();
if (w < vp.width)
w = vp.width;
if (h < vp.height)
h = vp.height;
return new Dimension(w, h);
}
private void grabItems()
{
// FIXME: Implement synchronization.
// Can't store directly to ITEMS array because NickList.this.items size could change and break the array store.
Vector temp_list = new Vector(NickList.this.items.size());
for (Enumeration e = NickList.this.items.elements(); e.hasMoreElements(); )
{
NickItem ni = (NickItem) e.nextElement();
temp_list.addElement(ni);
}
this.items = new NickItem [temp_list.size()];
temp_list.copyInto(items);
dirty_items = false;
}
// Sorting algorithm from James Gosling
private void sortItems()
{
sort(items, 0, items.length - 1);
}
private void sort(NickItem n[], int i, int j)
{
int k = i;
int l = j;
if(k >= l)
return;
else
if(k == l - 1)
{
NickItem a = n[k];
NickItem b = n[l];
if (compare(a, b) < 0)
{
n[k] = b;
n[l] = a;
}
}
NickItem a = n[(k + l) / 2];
n[(k + l) / 2] = n[l];
n[l] = a;
while(k < l)
{
while(k < l && compare(a, n[k]) >= 0)
k++;
while(k < l && compare(a, n[l]) <= 0)
l--;
if(k < l)
{
NickItem ni = n[k];
n[k] = n[l];
n[l] = ni;
}
}
n[j] = n[l];
n[l] = a;
sort(n, i, k - 1);
sort(n, l + 1, j);
}
private int compare(NickItem i, NickItem j)
{
String n = i.getNick();
String m = j.getNick();
int o = i.getModes();
int p = j.getModes();
int comp = 0;
switch (sort_method)
{
default:
case SORT_ALPHA:
comp = collator.compare(i.getNick(), j.getNick());
break;
case SORT_ALPHA_FAVOR_MODE:
// Will prefix the nick with a num char according to its highest privilege.
int k = 0;
while ((k < SORT_MASKS.length) && ((o & SORT_MASKS[k]) == 0))
k++;
if (k >= SORT_MASKS.length)
k = SORT_MASKS.length - 1;
n = SORT_CHARS.charAt(k) + n;
k = 0;
while ((k < SORT_MASKS.length) && ((p & SORT_MASKS[k]) == 0))
k++;
if (k >= SORT_MASKS.length)
k = SORT_MASKS.length - 1;
m = SORT_CHARS.charAt(k) + m;
comp = collator.compare(n, m);
break;
}
return(comp);
}
public void componentResized(ComponentEvent e)
{
setSize(getPreferredSize());
}
public void componentMoved(ComponentEvent e)
{
}
public void componentShown(ComponentEvent e)
{
}
public void componentHidden(ComponentEvent e)
{
}
}
}