/*
This file is part of leafdigital leafChat.
leafChat 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.
leafChat 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 leafChat. If not, see <http://www.gnu.org/licenses/>.
Copyright 2011 Samuel Marshall.
*/
package com.leafdigital.ircui;
import java.util.*;
import util.xml.XML;
import com.leafdigital.irc.api.*;
import com.leafdigital.prefs.api.*;
import com.leafdigital.ui.api.*;
import leafchat.core.api.*;
/** Implements the Join toolbar icon and dialog. */
@UIHandler("join")
public class JoinTool implements SimpleTool
{
private static final String PATTERN_KEYWORD="[\\x21-\\x2b\\x2d-\\xff]*";
private PluginContext context;
/** UI: Button to join favourite channel(s) */
public Button joinFavouritesUI;
/** UI: Favourites table */
public Table favouritesUI;
/** Join window */
private Window joinWindow = null;
/**
* @param context Context
*/
public JoinTool(PluginContext context)
{
this.context=context;
}
@Override
public void removed()
{
if(joinWindow!=null) joinWindow.close();
}
@Override
public String getLabel()
{
return "Join";
}
@Override
public String getThemeType()
{
return "joinButton";
}
@Override
public int getDefaultPosition()
{
return 110;
}
@Override
public void clicked() throws GeneralException
{
if(joinWindow == null)
{
UI u=context.getSingle(UI.class);
joinWindow = u.createWindow("join", this);
initWindow();
joinWindow.show(false);
}
else
{
joinWindow.activate();
}
}
static class ChannelInfo implements Comparable<ChannelInfo>
{
PreferencesGroup server,channel;
String name,network,host,key;
boolean autoJoin;
@Override
public int compareTo(ChannelInfo o)
{
ChannelInfo other=o;
int i=name.compareTo(other.name);
if(i!=0) return i;
i=network.compareTo(other.network);
if(i!=0) return i;
i=host.compareTo(other.host);
if(i!=0) return i;
i=key.compareTo(other.key);
if(i!=0) return i;
return (autoJoin?1 : 0) - (other.autoJoin ? 1 : 0);
}
@Override
public boolean equals(Object obj)
{
if(obj==null || !(obj instanceof ChannelInfo)) return false;
ChannelInfo other=(ChannelInfo)obj;
return other.name.equals(name) &&
network.equals(other.network) &&
host.equals(other.host) &&
other.key.equals(key) && other.autoJoin==autoJoin;
}
public ChannelInfo(Preferences p,PreferencesGroup server,PreferencesGroup channel)
throws GeneralException
{
this.server=server;
this.channel=channel;
name=channel.get(IRCPrefs.PREF_NAME);
network=server.get(IRCPrefs.PREF_NETWORK,"");
host=server.get(IRCPrefs.PREF_HOST,"");
autoJoin=p.toBoolean(channel.get(IRCPrefs.PREF_AUTOJOIN));
key=channel.get(IRCPrefs.PREF_KEY);
}
}
private static void addChannels(Preferences p, PreferencesGroup pg,
Set<ChannelInfo> channelSet) throws GeneralException
{
// Loop through all channels
PreferencesGroup[] channels =
pg.getChild(IRCPrefs.PREFGROUP_CHANNELS).getAnon();
for(int i=0; i<channels.length; i++)
{
channelSet.add(new ChannelInfo(p, pg, channels[i]));
}
}
static ChannelInfo[] getFavouriteChannels(PluginContext context) throws GeneralException
{
// Fill favourites table
Preferences p=context.getSingle(Preferences.class);
PreferencesGroup[] level1Prefs=p.getGroup(
p.getPluginOwner("com.leafdigital.irc.IRCPlugin")).getChild(
IRCPrefs.PREFGROUP_SERVERS).getAnon();
TreeSet<ChannelInfo> channelSet = new TreeSet<ChannelInfo>();
for(int level1=0;level1<level1Prefs.length;level1++)
{
// Got any channels?
addChannels(p,level1Prefs[level1],channelSet);
PreferencesGroup[] level2Prefs=level1Prefs[level1].getAnon();
for(int level2=0;level2<level2Prefs.length;level2++)
{
addChannels(p,level2Prefs[level2],channelSet);
}
}
return channelSet.toArray(new ChannelInfo[channelSet.size()]);
}
private ChannelInfo[] channels;
private void initWindow() throws GeneralException
{
joinWindow.setRemember("tool","join");
channels=getFavouriteChannels(context);
if(channels.length>0)
{
((TabPanel)joinWindow.getWidget("tabs")).display("favouritesPage");
}
// Get table
for(int i=0; i<channels.length; i++)
{
ChannelInfo ci = channels[i];
int row = favouritesUI.addItem();
favouritesUI.setString(row,0,ci.name);
favouritesUI.setString(row,1,ci.network+ci.host);
favouritesUI.setBoolean(row,2,ci.autoJoin);
favouritesUI.setString(row,3,ci.key);
}
// Start listening for server changes
context.requestMessages(ServerDisconnectedMsg.class,this,null,Msg.PRIORITY_NORMAL);
context.requestMessages(ServerConnectionFinishedMsg.class,this,null,Msg.PRIORITY_NORMAL);
// Update with current servers
updateServers();
}
/**
* Callback: Window closed.
* @throws GeneralException
*/
@UIAction
public void windowClosed() throws GeneralException
{
context.unrequestMessages(null,this,PluginContext.ALLREQUESTS);
joinWindow = null;
joinFavouritesUI = null;
favouritesUI = null;
}
/**
* Message: Possible server connect/disconnect.
* @param msg Message
* @throws GeneralException
*/
public void msg(ServerMsg msg) throws GeneralException
{
updateServers();
}
/**
* Callback: Join button (first screen).
* @throws GeneralException
*/
@UIAction
public void actionJoin() throws GeneralException
{
// Because this can get called by pressing return too
if(!((Button)joinWindow.getWidget("join")).isEnabled()) return;
Dropdown serverCombo=(Dropdown)joinWindow.getWidget("server");
Server s=(Server)serverCombo.getSelected();
String
channel=((EditBox)joinWindow.getWidget("channel")).getValue(),
key=((EditBox)joinWindow.getWidget("keyword")).getValue();
boolean
rememberKeyword=((CheckBox)joinWindow.getWidget("savekeyword")).isChecked();
// Don't save entry (at all) if they chose not to remember the keyword
if(rememberKeyword || key.length()==0)
{
Preferences p=context.getSingle(Preferences.class);
// Obtain prefs for server, or network if it belongs to one
PreferencesGroup pg=s.getPreferences().getAnonParent();
if(pg.get(IRCPrefs.PREF_NETWORK,null)==null)
pg=s.getPreferences();
// Get channels group
PreferencesGroup channels=pg.getChild(IRCPrefs.PREFGROUP_CHANNELS);
PreferencesGroup[] existing=channels.getAnon();
boolean found=false;
for(int i=0;i<existing.length;i++)
{
String name=existing[i].get(IRCPrefs.PREF_NAME);
if(name.equalsIgnoreCase(channel))
{
// OK already there, save key...
existing[i].set(IRCPrefs.PREF_KEY,key);
found=true;
break;
}
}
if(!found)
{
PreferencesGroup newChan=channels.addAnon();
newChan.set(IRCPrefs.PREF_NAME,channel);
newChan.set(IRCPrefs.PREF_KEY,key);
newChan.set(IRCPrefs.PREF_AUTOJOIN,p.fromBoolean(false));
}
}
// Send join command and close window
sendJoin(s,channel,key);
joinWindow.close();
}
private void sendJoin(Server s,String channel,String key)
{
s.sendLine(IRCMsg.constructBytes("JOIN "+channel+(key.length()>0 ? " "+key:"")));
}
/**
* Callback: Update button enable/disabled (first screen).
* @throws GeneralException
*/
@UIAction
public void enableButtons() throws GeneralException
{
Dropdown serverCombo=(Dropdown)joinWindow.getWidget("server");
String
channel=((EditBox)joinWindow.getWidget("channel")).getValue(),
keyword=((EditBox)joinWindow.getWidget("keyword")).getValue();
Server s=(Server)serverCombo.getSelected();
((Button)joinWindow.getWidget("join")).setEnabled(
s!=null
&& channel.matches(".[\\x21-\\x2b\\x2d-\\xff]{1,199}") // Any character except controls, space, comma. Up to 200 letters long
&& s.getChanTypes().indexOf(channel.charAt(0))!=-1
&& keyword.matches(PATTERN_KEYWORD)
);
((CheckBox)joinWindow.getWidget("savekeyword")).setEnabled(
keyword.length()!=0);
}
void updateServers() throws GeneralException
{
// Get connected servers
Connections c=context.getSingle(Connections.class);
Server[] servers=c.getConnected();
// Get current value
Dropdown serverCombo=(Dropdown)joinWindow.getWidget("server");
Dropdown server2Combo=(Dropdown)joinWindow.getWidget("server2");
Server
before=(Server)serverCombo.getSelected(),
before2=(Server)server2Combo.getSelected();
// Update list
serverCombo.clear();
server2Combo.clear();
for(int i=0;i<servers.length;i++)
{
if(!servers[i].isConnectionFinished()) return;
serverCombo.addValue(servers[i],servers[i].getReportedOrConnectedHost());
server2Combo.addValue(servers[i],servers[i].getReportedOrConnectedHost());
if(servers[i]==before)
serverCombo.setSelected(servers[i]);
if(servers[i]==before2)
server2Combo.setSelected(servers[i]);
}
// Hide/show it
serverCombo.setVisible(servers.length>1);
server2Combo.setVisible(servers.length>1);
// Update favourites list
for(int index=0; index<channels.length; index++)
{
ChannelInfo ci=channels[index];
boolean match=false;
for(int server=0;server<servers.length;server++)
{
PreferencesGroup pg=servers[server].getPreferences();
if(pg==ci.server || pg.getAnonParent()==ci.server)
{
match=true;
break;
}
}
favouritesUI.setDim(index, 1, !match);
}
// Fix buttons
enableButtons();
server2Changed();
}
/**
* Callback: Table change.
* @param index Row
* @param col Column
* @param oBefore Previous value of data
* @throws GeneralException
*/
@UIAction
public void favouritesChange(int index,int col,Object oBefore) throws GeneralException
{
Preferences p=context.getSingle(Preferences.class);
ChannelInfo ci=channels[index];
switch(col)
{
case 2: // Autojoin
ci.autoJoin = favouritesUI.getBoolean(index, col);
ci.channel.set(IRCPrefs.PREF_AUTOJOIN,p.fromBoolean(ci.autoJoin));
break;
case 3: // Key
ci.key = favouritesUI.getString(index, col);
ci.channel.set(IRCPrefs.PREF_KEY,ci.key);
break;
default:
assert(false);
}
}
/**
* Callback: Table in process of editing.
* @param index Row
* @param col Column
* @param value Current value
* @param ec Editing control for reporting errors
* @throws GeneralException
*/
@UIAction
public void favouritesEditing(int index,int col,String value,Table.EditingControl ec) throws GeneralException
{
switch(col)
{
case 3 : // Keyword
// Check keyword matches permitted pattern
if(!value.matches(PATTERN_KEYWORD)) ec.markError();
break;
default:
assert(false);
}
}
/**
* Callback: Change to select in favourites window.
* @throws GeneralException
*/
@UIAction
public void favouritesSelect() throws GeneralException
{
int[] selected = favouritesUI.getSelectedIndices();
((Button)joinWindow.getWidget("delete")).setEnabled(selected.length>0);
joinFavouritesUI.setEnabled(selected.length>0);
}
/**
* Callback: Join selected favourites.
* @throws GeneralException
*/
@UIAction
public void favouritesJoin() throws GeneralException
{
// Because this can get called by pressing return or double-clicking too
if(!joinFavouritesUI.isEnabled())
{
return;
}
int[] selected = favouritesUI.getSelectedIndices();
boolean ok = true;
for(int i=0; i<selected.length; i++)
{
ok &= join(channels[selected[i]]);
}
if(ok)
{
joinWindow.close();
}
}
/**
* Joins a channel on the first appropriate (matching host/network) connected
* server.
* @param ci Channel info
* @return True if it joined something, false if it didn't
* @throws GeneralException
*/
private boolean join(ChannelInfo ci) throws GeneralException
{
// Get connected servers
Connections c=context.getSingle(Connections.class);
Server[] servers=c.getConnected();
// Find server for this channel
for(int server=0;server<servers.length;server++)
{
PreferencesGroup pg=servers[server].getPreferences();
if(pg==ci.server || pg.getAnonParent()==ci.server)
{
sendJoin(servers[server],ci.name,ci.key);
return true;
}
}
int confirm=context.getSingle(UI.class).showQuestion(joinWindow,
"Confirm connect",
"You are not connected to an appropriate server for channel <key>"+
XML.esc(ci.name)+"</key>. Connect to <key>"+(ci.network+ci.host)+
"</key> now?",UI.BUTTON_YES|UI.BUTTON_CANCEL,"Connect",null,null,
UI.BUTTON_YES);
if(confirm==UI.BUTTON_YES)
{
((IRCUIPlugin)context.getPlugin()).connectAndJoin(ci.server,ci.name,ci.key);
return true;
}
return false;
}
/**
* Callback: Delete favourite(s).
* @throws GeneralException
*/
@UIAction
public void actionDelete() throws GeneralException
{
int[] deadRows = favouritesUI.getSelectedIndices();
Arrays.sort(deadRows);
LinkedList<ChannelInfo> channelList =
new LinkedList<ChannelInfo>(Arrays.asList(channels));
for(int i=deadRows.length-1;i>=0;i--)
{
favouritesUI.removeItem(deadRows[i]);
ChannelInfo ci=channelList.get(deadRows[i]);
ci.channel.remove();
channelList.remove(deadRows[i]);
}
channels = channelList.toArray(new ChannelInfo[channelList.size()]);
}
/**
* Callback: Server dropdown changed.
* @throws GeneralException
*/
@UIAction
public void server2Changed() throws GeneralException
{
ChoicePanel cp=(ChoicePanel)joinWindow.getWidget("listchoice");
Dropdown server2=(Dropdown)joinWindow.getWidget("server2");
if(server2.getSelected()==null)
cp.display("noserver");
else
{
Server s=(Server)server2.getSelected();
String eList=s.getISupport("ELIST");
if(eList!=null && eList.toUpperCase().indexOf('U')!=-1)
{
Table t=(Table)joinWindow.getWidget("searchresults");
if(searchServer!=s)
t.clear();
cp.display("ok");
}
else
{
cp.display("nosupport");
}
}
}
private boolean searching=false;
private int searchRequest;
private Server searchServer;
/**
* Callback: User changed min users for search.
* @throws GeneralException
*/
@UIAction
public void changeMinUsers() throws GeneralException
{
EditBox minUsers=(EditBox)joinWindow.getWidget("minusers");
boolean ok=minUsers.getValue().matches("[1-9][0-9]{0,2}");
((Button)joinWindow.getWidget("search")).setEnabled(ok && !searching);
minUsers.setFlag(ok ? EditBox.FLAG_NORMAL : EditBox.FLAG_ERROR);
}
/**
* Callback: Search button.
* @throws GeneralException
*/
@UIAction
public void actionSearch() throws GeneralException
{
Button searchButton=(Button)joinWindow.getWidget("search");
if(!searchButton.isEnabled()) return;
searchButton.setEnabled(false);
searching=true;
Table t=(Table)joinWindow.getWidget("searchresults");
t.clear();
selectSearch();
Server s=(Server)((Dropdown)joinWindow.getWidget("server2")).getSelected();
searchServer=s;
String minUsers=((EditBox)joinWindow.getWidget("minusers")).getValue();
searchRequest=context.requestMessages(NumericIRCMsg.class,this,new ServerFilter(s),Msg.PRIORITY_EARLY);
s.sendLine(IRCMsg.constructBytes("LIST >"+minUsers));
}
private final static String EATCOLOURS="(\\x03[0-9]{1,2}(,[0-9]{1,2})?)|[\\x00-\\x1f]";
/**
* Message: Numeric (looking for channel list).
* @param msg Message
* @throws GeneralException
*/
public void msg(NumericIRCMsg msg) throws GeneralException
{
switch(msg.getNumeric())
{
case NumericIRCMsg.RPL_LISTSTART:
msg.markHandled();
break;
case NumericIRCMsg.RPL_LISTEND:
searching=false;
context.unrequestMessages(null,this,searchRequest);
changeMinUsers(); // Enable button again
msg.markHandled();
break;
case NumericIRCMsg.RPL_LIST:
if(msg.getParams().length==4)
{
String name=msg.getParamISO(1);
if(!name.equals("*")) // DALnet puts out these, I dunno what it is, maybe a count of those not in a chan?
{
Table t=(Table)joinWindow.getWidget("searchresults");
int index=t.addItem();
t.setString(index,0,name);
t.setString(index,1,msg.getParamISO(2));
t.setString(index,2,msg.getParamISO(3).replaceAll(EATCOLOURS,""));
}
msg.markHandled();
}
break;
default:
}
}
/**
* Callback: Search result selected.
* @throws GeneralException
*/
@UIAction
public void selectSearch() throws GeneralException
{
Table t=(Table)joinWindow.getWidget("searchresults");
int[] selected=t.getSelectedIndices();
((Button)joinWindow.getWidget("joinSearch")).setEnabled(selected.length>0);
}
/**
* Callback: Join button (search page).
* @throws GeneralException
*/
@UIAction
public void actionJoinSearch() throws GeneralException
{
// Because this can get called by pressing return or double-clicking too
if(!((Button)joinWindow.getWidget("joinSearch")).isEnabled()) return;
Table t=(Table)joinWindow.getWidget("searchresults");
int[] selected=t.getSelectedIndices();
for(int i=0;i<selected.length;i++)
{
sendJoin(searchServer,t.getString(selected[i],0),"");
}
joinWindow.close();
}
}