package chatty.gui;
import chatty.Helper;
import chatty.Helper.IntegerPair;
import chatty.gui.components.Channel;
import chatty.gui.components.ChannelDialog;
import chatty.gui.components.tabs.Tabs;
import chatty.gui.components.menus.ContextMenuListener;
import chatty.gui.components.menus.TabContextMenu;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.Window;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.util.*;
import javax.swing.JDialog;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
/**
* Managing the Channel objects in the main window and popouts, providing a
* default channel while no other is added.
*
* @author tduva
*/
public class Channels {
private final MainGui gui;
private final WindowListener windowListener;
private ChangeListener changeListener;
/**
* Saves all added channels by name.
*/
private final HashMap<String,Channel> channels = new HashMap<>();
/**
* Saves which channels are in a popout (and which dialog it is).
*/
private final Map<Channel, JDialog> dialogs = new LinkedHashMap<>();
/**
* Saves attributes of closed popout dialogs.
*/
private final List<LocationAndSize> dialogsAttributes = new ArrayList<>();
private final Tabs tabs;
private Channel defaultChannel;
private final StyleManager styleManager;
private final ContextMenuListener contextMenuListener;
private final MouseClickedListener mouseClickedListener = new MyMouseClickedListener();
private Channel.OnceOffEditListener onceOffEditListener;
/**
* Default width of the userlist, given to Channel objects when created.
*/
private int defaultUserlistWidth = 140;
private int minUserlistWidth = 0;
private boolean defaultUserlistVisibleState = true;
private boolean chatScrollbarAlaways;
private Channel lastActiveChannel = null;
private boolean savePopoutAttributes;
private boolean closeLastChannelPopout;
/**
* Save channels whose state is new highlighted messages, so the color
* doesn't get overwritten by new messages.
*/
private final Set<Channel> highlighted = new HashSet<>();
public Channels(MainGui gui, StyleManager styleManager,
ContextMenuListener contextMenuListener) {
windowListener = new MyWindowListener();
tabs = new Tabs();
tabs.setPopupMenu(new TabContextMenu(contextMenuListener));
this.styleManager = styleManager;
this.contextMenuListener = contextMenuListener;
this.gui = gui;
tabs.addChangeListener(new TabChangeListener());
tabs.setMouseWheelScrollingEnabled(gui.getSettings().getBoolean("tabsMwheelScrolling"));
tabs.setMouseWheelScrollingAnywhereEnabled(gui.getSettings().getBoolean("tabsMwheelScrollingAnywhere"));
tabs.setTabPlacement(gui.getSettings().getString("tabsPlacement"));
tabs.setTabLayoutPolicy(gui.getSettings().getString("tabsLayout"));
gui.addWindowListener(windowListener);
//tabs.setOpaque(false);
//tabs.setBackground(new Color(0,0,0,0));
addDefaultChannel();
}
public void setOnceOffEditListener(Channel.OnceOffEditListener listener) {
this.onceOffEditListener = listener;
if (defaultChannel != null) {
defaultChannel.setOnceOffEditListener(listener);
}
}
public void setChangeListener(ChangeListener listener) {
changeListener = listener;
}
private void channelChanged() {
if (changeListener != null) {
changeListener.stateChanged(new ChangeEvent(this));
}
}
/**
* Set channel to show a new highlight messages has arrived, changes color
* of the tab. ONly if not currently active tab.
*
* @param channel
*/
public void setChannelHighlighted(Channel channel) {
if (getActiveTab() != channel) {
tabs.setForegroundForComponent(channel, MainGui.COLOR_NEW_HIGHLIGHTED_MESSAGE);
highlighted.add(channel);
}
}
/**
* Set channel to show a new message arrived, changes color of the tab.
* Only if not currently active tab.
*
* @param channel
*/
public void setChannelNewMessage(Channel channel) {
if (getActiveTab() != channel && !highlighted.contains(channel)) {
tabs.setForegroundForComponent(channel, MainGui.COLOR_NEW_MESSAGE);
}
}
/**
* Reset state (color, title suffixes) to default.
*
* @param channel
*/
public void resetChannelTab(Channel channel) {
tabs.setForegroundForComponent(channel, null);
tabs.setTitleForComponent(channel, channel.getName());
highlighted.remove(channel);
}
/**
* Set channel to new status available, adds a suffix to indicate that.
* Only if not currently the active tab.
*
* @param channel
*/
public void setChannelNewStatus(Channel channel) {
if (getActiveTab() != channel) {
tabs.setTitleForComponent(channel, channel.getName()+"*");
}
}
/**
* This is the channel when no channel has been added yet.
*/
private void addDefaultChannel() {
defaultChannel = createChannel("", Channel.Type.NONE);
tabs.addTab(defaultChannel);
}
private Channel createChannel(String name, Channel.Type type) {
Channel channel = new Channel(name,type,gui,styleManager, contextMenuListener);
channel.setUserlistWidth(defaultUserlistWidth, minUserlistWidth);
channel.setMouseClickedListener(mouseClickedListener);
channel.setScrollbarAlways(chatScrollbarAlaways);
channel.setUserlistEnabled(defaultUserlistVisibleState);
channel.setOnceOffEditListener(onceOffEditListener);
if (type == Channel.Type.SPECIAL || type == Channel.Type.WHISPER) {
channel.setUserlistEnabled(false);
}
return channel;
}
public Component getComponent() {
return tabs;
}
public Channel get(String key) {
return channels.get(key);
}
public Collection<Channel> channels() {
return channels.values();
}
public int getChannelCount() {
return channels.size();
}
/**
* Check if the given channel is added.
*
* @param channel
* @return
*/
public boolean isChannel(String channel) {
if (channel == null) {
return false;
}
return channels.get(channel) != null;
}
public Channel getChannel(String channel) {
return getChannel(channel, getTypeFromChannelName(channel));
}
public Channel.Type getTypeFromChannelName(String name) {
if (name.startsWith("#")) {
return Channel.Type.CHANNEL;
} else if (name.startsWith("$")) {
return Channel.Type.WHISPER;
} else if (name.startsWith("*")) {
return Channel.Type.SPECIAL;
}
return Channel.Type.NONE;
}
/**
* Gets the Channel object for the given channel name. If none exists, the
* channel is automatically added.
*
* @param channel
* @param type
* @return
*/
public Channel getChannel(String channel, Channel.Type type) {
Channel panel = channels.get(channel);
if (panel == null) {
panel = addChannel(channel, type);
}
return panel;
}
public Channel getChannelOfType(String channel, Channel.Type type) {
Channel panel = channels.get(channel);
if (panel != null && panel.getType() == type) {
return panel;
}
if (panel == null) {
return addChannel(channel, type);
}
return null;
}
public String getChannelNameFromPanel(Channel panel) {
for (String key : channels.keySet()) {
if (channels.get(key) == panel) {
return key;
}
}
return null;
}
public Channel getChannelFromWindow(Object dialog) {
for (Channel channel : dialogs.keySet()) {
if (dialogs.get(channel) == dialog) {
return channel;
}
}
if (dialog == gui) {
return getActiveTab();
}
return null;
}
/**
* Adds a channel with the given name. If the default channel is still there
* it is used for this channel and renamed.
*
* @param channelName
* @param type
* @return
*/
public Channel addChannel(String channelName, Channel.Type type) {
if (channels.get(channelName) != null) {
return null;
}
Channel panel;
if (defaultChannel != null) {
// Reuse default channel
panel = defaultChannel;
defaultChannel = null;
panel.setName(channelName);
panel.setType(type);
channelChanged();
}
else {
// No default channel, so create a new one
panel = createChannel(channelName, type);
tabs.addTab(panel);
if (type != Channel.Type.WHISPER) {
tabs.setSelectedComponent(panel);
}
}
channels.put(channelName, panel);
return panel;
}
public void removeChannel(final String channelName) {
Channel channel = channels.get(channelName);
if (channel == null) {
return;
}
channels.remove(channelName);
closePopout(channel);
tabs.removeTab(channel);
channel.cleanUp();
if (tabs.getTabCount() == 0) {
if (dialogs.isEmpty() || !closeLastChannelPopout) {
addDefaultChannel();
} else {
closePopout(dialogs.keySet().iterator().next());
}
lastActiveChannel = null;
channelChanged();
gui.updateState();
}
}
/**
* Popout the given channel if it isn't already and if there is actually
* more than one tab.
*
* @param channel The {@code Channel} to popout
*/
public void popout(final Channel channel) {
if (channel == null) {
return;
}
if (dialogs.containsKey(channel)) {
return;
}
if (tabs.getTabCount() < 2) {
return;
}
tabs.removeTab(channel);
// Create and configure new dialog for the popout
final JDialog newDialog = new ChannelDialog(gui, channel);
newDialog.setLocationRelativeTo(gui);
newDialog.addWindowListener(windowListener);
gui.popoutCreated(newDialog);
// Restore attributes if available
if (!dialogsAttributes.isEmpty()) {
LocationAndSize attr = dialogsAttributes.remove(0);
if (GuiUtil.isPointOnScreen(attr.location, 5)) {
newDialog.setLocation(attr.location);
}
newDialog.setSize(attr.size);
}
dialogs.put(channel, newDialog);
// Making it visible directly apparently makes it not properly detect
// it as active window
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
newDialog.setVisible(true);
}
});
gui.updateState(true);
}
public void popoutActiveChannel() {
if (getActiveChannel() != null) {
popout(getActiveChannel());
}
}
public void setSavePopoutAttributes(boolean save) {
savePopoutAttributes = save;
if (!save) {
dialogsAttributes.clear();
}
}
/**
* Returns a list of Strings that contain the location/size of open dialogs
* and of closed dialogs that weren't reused.
*
* Format: "x,y;width,height"
*
* @return
*/
public List getPopoutAttributes() {
List<String> attributes = new ArrayList<>();
for (JDialog dialog : dialogs.values()) {
attributes.add(dialog.getX()+","+dialog.getY()
+";"+dialog.getWidth()+","+dialog.getHeight());
}
for (LocationAndSize attr : dialogsAttributes) {
attributes.add(attr.location.x + "," + attr.location.y
+ ";" + attr.size.width + "," + attr.size.height);
}
return attributes;
}
/**
* Sets the attributes for popouts that will be opened, as a List of Strings
* that have to be parsed into {@literal LocationAndSize} objects.
*
* @param attributes
*/
public void setPopoutAttributes(List<String> attributes) {
dialogsAttributes.clear();
for (String attr : attributes) {
if (attr == null) {
continue;
}
String[] split = attr.split(";");
if (split.length != 2) {
continue;
}
IntegerPair location = Helper.getNumbersFromString(split[0]);
IntegerPair size = Helper.getNumbersFromString(split[1]);
if (location != null && size != null) {
dialogsAttributes.add(
new LocationAndSize(
new Point(location.a, location.b),
new Dimension(size.a, size.b)));
}
}
}
public void setCloseLastChannelPopout(boolean close) {
closeLastChannelPopout = close;
}
/**
* Once the popout dialog was closed (either by the user or by the program)
* add the channel to the tabs again and update the GUI.
*
* @param channel
*/
private void popoutDisposed(Channel channel) {
if (channel == null) {
return;
}
dialogs.remove(channel);
if (defaultChannel != null) {
tabs.removeTab(defaultChannel);
defaultChannel = null;
}
tabs.addTab(channel);
tabs.setSelectedComponent(channel);
gui.updateState(true);
}
/**
* Close the popout for the given channel (if it exists) and move the
* channel back to the main window.
*
* @param channel
*/
public void closePopout(Channel channel) {
if (channel == null) {
return;
}
if (!dialogs.containsKey(channel)) {
return;
}
JDialog dialog = dialogs.remove(channel);
dialog.dispose();
popoutDisposed(channel);
}
/**
* Return the currently active Channel, which means the one that has focus,
* either because it is a popout dialog that has focus, or the currently
* selected tab if the focus is on the main window.
*
* <p>
* If the focus is not on a window containing a Channel, then the current
* tab of the main window is returned.
* </p>
*
* @return The Channel object which is currently selected.
*/
public Channel getActiveChannel() {
for (Channel channel : dialogs.keySet()) {
if (dialogs.get(channel).isActive()) {
return channel;
}
}
return getActiveTab();
}
/**
* Returns the Channel that was last active. If the focus is on a Window
* that contains a Channel, it should be the same as
* {@link getActiveChannel()}, otherwise it is the Channel that was active
* before a Window without a Channel was focused (e.g. an info dialog).
*
* @return
*/
public Channel getLastActiveChannel() {
if (lastActiveChannel == null) {
return getActiveTab();
}
return lastActiveChannel;
}
/**
* Returns channel of the active tab in the main window (as opposed to the
* active channel, which might also be in a popout).
*
* @return
*/
public Channel getActiveTab() {
Component c = tabs.getSelectedComponent();
if (c instanceof Channel) {
return (Channel) c;
}
return null;
}
/**
* Returns a map of all channels and their respective dialog.
*
* @return The {@literal Map} with {@literal Channel} objects as keys and
* {@literal JDialog} objects as values
*/
public Map<Channel, JDialog> getPopoutChannels() {
return new HashMap<>(dialogs);
}
/**
* Return the channel from the given input box.
*
* @param input The reference to the input box.
* @return The Channel object, or null if the given reference isn't an
* input box
*/
public Channel getChannelFromInput(Object input) {
if (defaultChannel != null && input == defaultChannel.getInput()) {
return defaultChannel;
}
for (String key : channels.keySet()) {
Channel value = channels.get(key);
if (input == value.getInput()) {
return value;
}
}
return null;
}
public List<Channel> getChannels() {
return getChannelsOfType(null);
}
/**
* A list of all channels, be it in the main window or in popouts.
*
* @param type
* @return The {@code List} of {@code Channel} objects
*/
public List<Channel> getChannelsOfType(Channel.Type type) {
List<Channel> result = new ArrayList<>(getTabs(type));
for (Channel c : channels.values()) {
// Add channels that aren't on tabs (popouts)
if ((type == null || c.getType() == type) && !result.contains(c)) {
result.add(c);
}
}
return result;
}
public Collection<Channel> getTabs() {
return getTabs(null);
}
public Collection<Channel> getTabs(Channel.Type type) {
List<Channel> result = new ArrayList<>();
for (Component comp : tabs.getAllComponents()) {
Channel chan = (Channel)comp;
if ((type == null || chan.getType() == type)
&& channels.containsValue(chan) || chan == defaultChannel) {
result.add((Channel)comp);
}
}
return result;
}
public Collection<Channel> getTabsRelativeToCurrent(int direction) {
return getTabsRelativeTo(getActiveTab(), direction);
}
public Collection<Channel> getTabsRelativeTo(Channel chan, int direction) {
List<Channel> result = new ArrayList<>();
for (Component comp : tabs.getComponents(chan, direction)) {
if (channels.containsValue(comp)) {
result.add((Channel)comp);
}
}
return result;
}
public void setInitialFocus() {
getActiveChannel().requestFocusInWindow();
}
public void refreshStyles() {
for (Channel channel : getChannels()) {
channel.refreshStyles();
}
}
public void updateUserlistSettings() {
for (Channel channel : getChannels()) {
channel.updateUserlistSettings();
}
}
public void switchToChannel(String channel) {
if (isChannel(channel)) {
tabs.setSelectedComponent(get(channel));
}
}
public void switchToNextChannel() {
tabs.setSelectedNext();
}
public void switchToPreviousChannel() {
tabs.setSelectedPrevious();
}
public void setDefaultUserlistWidth(int width, int minWidth) {
defaultUserlistWidth = width;
minUserlistWidth = minWidth;
if (defaultChannel != null) {
// Set the width of the default channel because it's created before
// the width is loaded from the settings
defaultChannel.setUserlistWidth(width, minWidth);
}
}
public void setDefaultUserlistVisibleState(boolean state){
defaultUserlistVisibleState = state;
if (defaultChannel != null) {
defaultChannel.setUserlistEnabled(state);
}
}
public void setChatScrollbarAlways(boolean always) {
chatScrollbarAlaways = always;
for (Channel chan : channels.values()) {
chan.setScrollbarAlways(always);
}
if (defaultChannel != null) {
defaultChannel.setScrollbarAlways(always);
}
}
public void setTabOrder(String order) {
Tabs.TabOrder setting = Tabs.TabOrder.INSERTION;
switch (order) {
case "alphabetical": setting = Tabs.TabOrder.ALPHABETIC; break;
}
tabs.setOrder(setting);
}
/**
* When the active tab is changed, keeps track of the lastActiveChannel and
* does some work necessary when tab is changed.
*/
private class TabChangeListener implements ChangeListener {
@Override
public void stateChanged(ChangeEvent e) {
lastActiveChannel = getActiveTab();
setInitialFocus();
resetChannelTab(getActiveChannel());
channelChanged();
}
}
/**
* Sets the focus to the input bar when clicked anywhere on the channel.
*/
private class MyMouseClickedListener implements MouseClickedListener {
@Override
public void mouseClicked() {
setInitialFocus();
}
}
/**
* Registered to popout dialogs and the main window, cleans up closed popout
* dialogs and keeps the lastActiveChannel up-to-date.
*/
private class MyWindowListener extends WindowAdapter {
@Override
public void windowClosed(WindowEvent e) {
if (e.getSource() == gui) {
return;
}
popoutDisposed(getChannelFromWindow(e.getSource()));
if (savePopoutAttributes) {
Window window = e.getWindow();
dialogsAttributes.add(0, new LocationAndSize(
window.getLocation(), window.getSize()));
}
}
@Override
public void windowActivated(WindowEvent e) {
Channel channel = getChannelFromWindow(e.getSource());
if (channel != lastActiveChannel) {
lastActiveChannel = channel;
channelChanged();
}
}
}
private static class LocationAndSize {
public final Point location;
public final Dimension size;
LocationAndSize(Point location, Dimension size) {
this.location = location;
this.size = size;
}
}
}