You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Channel.java 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674
  1. /*
  2. * Copyright (c) 2006-2011 Chris Smith, Shane Mc Cormack, Gregory Holmes
  3. *
  4. * Permission is hereby granted, free of charge, to any person obtaining a copy
  5. * of this software and associated documentation files (the "Software"), to deal
  6. * in the Software without restriction, including without limitation the rights
  7. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  8. * copies of the Software, and to permit persons to whom the Software is
  9. * furnished to do so, subject to the following conditions:
  10. *
  11. * The above copyright notice and this permission notice shall be included in
  12. * all copies or substantial portions of the Software.
  13. *
  14. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  15. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  16. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  17. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  18. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  19. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  20. * SOFTWARE.
  21. */
  22. package com.dmdirc;
  23. import com.dmdirc.actions.ActionManager;
  24. import com.dmdirc.actions.CoreActionType;
  25. import com.dmdirc.commandparser.CommandManager;
  26. import com.dmdirc.commandparser.CommandType;
  27. import com.dmdirc.commandparser.parsers.ChannelCommandParser;
  28. import com.dmdirc.config.ConfigManager;
  29. import com.dmdirc.interfaces.ConfigChangeListener;
  30. import com.dmdirc.interfaces.NicklistListener;
  31. import com.dmdirc.interfaces.TopicChangeListener;
  32. import com.dmdirc.parser.interfaces.ChannelClientInfo;
  33. import com.dmdirc.parser.interfaces.ChannelInfo;
  34. import com.dmdirc.parser.interfaces.ClientInfo;
  35. import com.dmdirc.ui.WindowManager;
  36. import com.dmdirc.ui.input.TabCompleter;
  37. import com.dmdirc.ui.input.TabCompletionType;
  38. import com.dmdirc.ui.interfaces.ChannelWindow;
  39. import com.dmdirc.ui.messages.ColourManager;
  40. import com.dmdirc.ui.messages.Styliser;
  41. import com.dmdirc.util.RollingList;
  42. import java.awt.Color;
  43. import java.util.ArrayList;
  44. import java.util.Arrays;
  45. import java.util.Collection;
  46. import java.util.List;
  47. import java.util.Map;
  48. /**
  49. * The Channel class represents the client's view of the channel. It handles
  50. * callbacks for channel events from the parser, maintains the corresponding
  51. * ChannelWindow, and handles user input for the channel.
  52. *
  53. * @author chris
  54. */
  55. public class Channel extends MessageTarget<ChannelWindow> implements ConfigChangeListener {
  56. /** The parser's pChannel class. */
  57. private ChannelInfo channelInfo;
  58. /** The server this channel is on. */
  59. private Server server;
  60. /** The tabcompleter used for this channel. */
  61. private final TabCompleter tabCompleter;
  62. /** A list of previous topics we've seen. */
  63. private final RollingList<Topic> topics;
  64. /** Our event handler. */
  65. private final ChannelEventHandler eventHandler;
  66. /** Whether we're in this channel or not. */
  67. private boolean onChannel;
  68. /** Whether we should send WHO requests for this channel. */
  69. private volatile boolean sendWho;
  70. /** Whether we should show mode prefixes in text. */
  71. private volatile boolean showModePrefix;
  72. /** Whether we should show colours in nicks. */
  73. private volatile boolean showColours;
  74. /**
  75. * Creates a new instance of Channel.
  76. *
  77. * @param newServer The server object that this channel belongs to
  78. * @param newChannelInfo The parser's channel object that corresponds to
  79. * this channel
  80. * @param focus Whether or not to focus this channel
  81. */
  82. public Channel(final Server newServer, final ChannelInfo newChannelInfo,
  83. final boolean focus) {
  84. super("channel-inactive", newChannelInfo.getName(),
  85. Styliser.stipControlCodes(newChannelInfo.getName()),
  86. ChannelWindow.class,
  87. new ConfigManager(newServer.getProtocol(), newServer.getIrcd(),
  88. newServer.getNetwork(), newServer.getAddress(), newChannelInfo.getName()),
  89. new ChannelCommandParser(newServer));
  90. channelInfo = newChannelInfo;
  91. server = newServer;
  92. getConfigManager().addChangeListener("channel", this);
  93. getConfigManager().addChangeListener("ui", "shownickcoloursintext", this);
  94. topics = new RollingList<Topic>(getConfigManager().getOptionInt("channel",
  95. "topichistorysize"));
  96. sendWho = getConfigManager().getOptionBool("channel", "sendwho");
  97. showModePrefix = getConfigManager().getOptionBool("channel", "showmodeprefix");
  98. showColours = getConfigManager().getOptionBool("ui", "shownickcoloursintext");
  99. tabCompleter = new TabCompleter(server.getTabCompleter());
  100. tabCompleter.addEntries(TabCompletionType.COMMAND,
  101. CommandManager.getCommandNames(CommandType.TYPE_CHANNEL));
  102. tabCompleter.addEntries(TabCompletionType.COMMAND,
  103. CommandManager.getCommandNames(CommandType.TYPE_CHAT));
  104. WindowManager.addWindow(server, this, focus);
  105. eventHandler = new ChannelEventHandler(this);
  106. registerCallbacks();
  107. ActionManager.processEvent(CoreActionType.CHANNEL_OPENED, null, this);
  108. updateTitle();
  109. selfJoin();
  110. }
  111. /**
  112. * Registers callbacks with the parser for this channel.
  113. */
  114. private void registerCallbacks() {
  115. eventHandler.registerCallbacks();
  116. getConfigManager().migrate(server.getProtocol(), server.getIrcd(),
  117. server.getNetwork(), server.getAddress(), channelInfo.getName());
  118. }
  119. /** {@inheritDoc} */
  120. @Override
  121. public void sendLine(final String line) {
  122. if (server.getState() != ServerState.CONNECTED
  123. || server.getParser().getChannel(channelInfo.getName()) == null) {
  124. // We're not in the channel/connected to the server
  125. return;
  126. }
  127. final ClientInfo me = server.getParser().getLocalClient();
  128. final String[] details = getDetails(channelInfo.getChannelClient(me));
  129. for (String part : splitLine(getTranscoder().encode(line))) {
  130. if (!part.isEmpty()) {
  131. final StringBuffer buff = new StringBuffer("channelSelfMessage");
  132. ActionManager.processEvent(CoreActionType.CHANNEL_SELF_MESSAGE, buff,
  133. this, channelInfo.getChannelClient(me), part);
  134. addLine(buff, details[0], details[1], details[2], details[3],
  135. part, channelInfo);
  136. channelInfo.sendMessage(part);
  137. }
  138. }
  139. }
  140. /** {@inheritDoc} */
  141. @Override
  142. public int getMaxLineLength() {
  143. return server.getState() == ServerState.CONNECTED
  144. ? server.getParser().getMaxLength("PRIVMSG", getChannelInfo().getName())
  145. : -1;
  146. }
  147. /** {@inheritDoc} */
  148. @Override
  149. public void sendAction(final String action) {
  150. if (server.getState() != ServerState.CONNECTED
  151. || server.getParser().getChannel(channelInfo.getName()) == null) {
  152. // We're not on the server/channel
  153. return;
  154. }
  155. final ClientInfo me = server.getParser().getLocalClient();
  156. final String[] details = getDetails(channelInfo.getChannelClient(me));
  157. if (server.getParser().getMaxLength("PRIVMSG", getChannelInfo().getName())
  158. <= action.length()) {
  159. addLine("actionTooLong", action.length());
  160. } else {
  161. final StringBuffer buff = new StringBuffer("channelSelfAction");
  162. ActionManager.processEvent(CoreActionType.CHANNEL_SELF_ACTION, buff,
  163. this, channelInfo.getChannelClient(me), action);
  164. addLine(buff, details[0], details[1], details[2], details[3],
  165. getTranscoder().encode(action), channelInfo);
  166. channelInfo.sendAction(getTranscoder().encode(action));
  167. }
  168. }
  169. /**
  170. * Returns the server object that this channel belongs to.
  171. *
  172. * @return The server object
  173. */
  174. @Override
  175. public Server getServer() {
  176. return server;
  177. }
  178. /**
  179. * Returns the parser's ChannelInfo object that this object is associated
  180. * with.
  181. *
  182. * @return The ChannelInfo object associated with this object
  183. */
  184. public ChannelInfo getChannelInfo() {
  185. return channelInfo;
  186. }
  187. /**
  188. * Sets this object's ChannelInfo reference to the one supplied. This only
  189. * needs to be done if the channel window (and hence this channel object)
  190. * has stayed open while the user has been out of the channel.
  191. *
  192. * @param newChannelInfo The new ChannelInfo object
  193. */
  194. public void setChannelInfo(final ChannelInfo newChannelInfo) {
  195. channelInfo = newChannelInfo;
  196. registerCallbacks();
  197. }
  198. /** {@inheritDoc} */
  199. @Override
  200. public TabCompleter getTabCompleter() {
  201. return tabCompleter;
  202. }
  203. /**
  204. * Are we on the channel?
  205. *
  206. * @return true iff we're on the channel
  207. */
  208. public boolean isOnChannel() {
  209. return onChannel;
  210. }
  211. /**
  212. * Called when we join this channel. Just needs to output a message.
  213. */
  214. public void selfJoin() {
  215. onChannel = true;
  216. final ClientInfo me = server.getParser().getLocalClient();
  217. addLine("channelSelfJoin", "", me.getNickname(), me.getUsername(),
  218. me.getHostname(), channelInfo.getName());
  219. checkWho();
  220. setIcon("channel");
  221. server.removeInvites(channelInfo.getName());
  222. }
  223. /**
  224. * Updates the title of the channel window, and of the main window if
  225. * appropriate.
  226. */
  227. private void updateTitle() {
  228. String temp = Styliser.stipControlCodes(channelInfo.getName());
  229. if (!channelInfo.getTopic().isEmpty()) {
  230. temp += " - " + Styliser.stipControlCodes(channelInfo.getTopic());
  231. }
  232. setTitle(temp);
  233. }
  234. /**
  235. * Joins the specified channel. This only makes sense if used after a call
  236. * to part().
  237. */
  238. public void join() {
  239. server.getParser().joinChannel(channelInfo.getName());
  240. activateFrame();
  241. }
  242. /**
  243. * Parts this channel with the specified message. Parting does NOT close the
  244. * channel window.
  245. *
  246. * @param reason The reason for parting the channel
  247. */
  248. public void part(final String reason) {
  249. channelInfo.part(reason);
  250. resetWindow();
  251. }
  252. /**
  253. * Requests all available list modes for this channel.
  254. *
  255. * @since 0.6.3
  256. */
  257. public void retrieveListModes() {
  258. channelInfo.requestListModes();
  259. }
  260. /**
  261. * Resets the window state after the client has left a channel.
  262. */
  263. public void resetWindow() {
  264. onChannel = false;
  265. setIcon("channel-inactive");
  266. for (NicklistListener listener : listeners.get(NicklistListener.class)) {
  267. listener.clientListUpdated(new ArrayList<ChannelClientInfo>());
  268. }
  269. }
  270. /** {@inheritDoc} */
  271. @Override
  272. public void windowClosing() {
  273. // 2: Remove any callbacks or listeners
  274. eventHandler.unregisterCallbacks();
  275. if (server.getParser() != null) {
  276. server.getParser().getCallbackManager().delAllCallback(eventHandler);
  277. }
  278. // 3: Trigger any actions neccessary
  279. if (onChannel) {
  280. part(getConfigManager().getOption("general", "partmessage"));
  281. }
  282. // 4: Trigger action for the window closing
  283. ActionManager.processEvent(CoreActionType.CHANNEL_CLOSED, null, this);
  284. // 5: Inform any parents that the window is closing
  285. server.delChannel(channelInfo.getName());
  286. }
  287. /** {@inheritDoc} */
  288. @Override
  289. public void windowClosed() {
  290. // 7: Remove any references to the window and parents
  291. synchronized (this) {
  292. server = null; // NOPMD
  293. }
  294. }
  295. /**
  296. * Called every {general.whotime} seconds to check if the channel needs
  297. * to send a who request.
  298. */
  299. public void checkWho() {
  300. if (onChannel && sendWho) {
  301. channelInfo.sendWho();
  302. }
  303. }
  304. /**
  305. * Registers a new nicklist listener for this channel.
  306. *
  307. * @since 0.6.5
  308. * @param listener The listener to be added
  309. */
  310. public void addNicklistListener(final NicklistListener listener) {
  311. listeners.add(NicklistListener.class, listener);
  312. }
  313. /**
  314. * Removes the specified nicklist listener from this channel.
  315. *
  316. * @since 0.6.5
  317. * @param listener The listener to be removed
  318. */
  319. public void removeNicklistListener(final NicklistListener listener) {
  320. listeners.remove(NicklistListener.class, listener);
  321. }
  322. /**
  323. * Adds a ChannelClient to this Channel.
  324. *
  325. * @param client The client to be added
  326. */
  327. public void addClient(final ChannelClientInfo client) {
  328. for (NicklistListener listener : listeners.get(NicklistListener.class)) {
  329. listener.clientAdded(client);
  330. }
  331. tabCompleter.addEntry(TabCompletionType.CHANNEL_NICK, client.getClient().getNickname());
  332. }
  333. /**
  334. * Removes the specified ChannelClient from this channel.
  335. *
  336. * @param client The client to be removed
  337. */
  338. public void removeClient(final ChannelClientInfo client) {
  339. for (NicklistListener listener : listeners.get(NicklistListener.class)) {
  340. listener.clientRemoved(client);
  341. }
  342. tabCompleter.removeEntry(TabCompletionType.CHANNEL_NICK, client.getClient().getNickname());
  343. if (client.getClient().equals(server.getParser().getLocalClient())) {
  344. resetWindow();
  345. }
  346. }
  347. /**
  348. * Replaces the list of known clients on this channel with the specified
  349. * one.
  350. *
  351. * @param clients The list of clients to use
  352. */
  353. public void setClients(final Collection<ChannelClientInfo> clients) {
  354. for (NicklistListener listener : listeners.get(NicklistListener.class)) {
  355. listener.clientListUpdated(clients);
  356. }
  357. tabCompleter.clear(TabCompletionType.CHANNEL_NICK);
  358. for (ChannelClientInfo client : clients) {
  359. tabCompleter.addEntry(TabCompletionType.CHANNEL_NICK, client.getClient().getNickname());
  360. }
  361. }
  362. /**
  363. * Renames a client that is in this channel.
  364. *
  365. * @param oldName The old nickname of the client
  366. * @param newName The new nickname of the client
  367. */
  368. public void renameClient(final String oldName, final String newName) {
  369. tabCompleter.removeEntry(TabCompletionType.CHANNEL_NICK, oldName);
  370. tabCompleter.addEntry(TabCompletionType.CHANNEL_NICK, newName);
  371. refreshClients();
  372. }
  373. /**
  374. * Refreshes the list of clients stored by this channel. Should be called
  375. * when (visible) user modes or nicknames change.
  376. */
  377. public void refreshClients() {
  378. if (!onChannel) {
  379. return;
  380. }
  381. for (NicklistListener listener : listeners.get(NicklistListener.class)) {
  382. listener.clientListUpdated();
  383. }
  384. }
  385. /**
  386. * Returns a string containing the most important mode for the specified
  387. * client.
  388. *
  389. * @param channelClient The channel client to check.
  390. * @return A string containing the most important mode, or an empty string
  391. * if there are no (known) modes.
  392. */
  393. private String getModes(final ChannelClientInfo channelClient) {
  394. if (channelClient == null || !showModePrefix) {
  395. return "";
  396. } else {
  397. return channelClient.getImportantModePrefix();
  398. }
  399. }
  400. /** {@inheritDoc} */
  401. @Override
  402. public void configChanged(final String domain, final String key) {
  403. if ("sendwho".equals(key)) {
  404. sendWho = getConfigManager().getOptionBool("channel", "sendwho");
  405. } else if ("showmodeprefix".equals(key)) {
  406. showModePrefix = getConfigManager().getOptionBool("channel", "showmodeprefix");
  407. } else if ("shownickcoloursintext".equals(key)) {
  408. showColours = getConfigManager().getOptionBool("ui", "shownickcoloursintext");
  409. }
  410. }
  411. /**
  412. * Returns a string[] containing the nickname/ident/host of a channel
  413. * client.
  414. *
  415. * @param client The channel client to check
  416. * @return A string[] containing displayable components
  417. */
  418. private String[] getDetails(final ChannelClientInfo client) {
  419. if (client == null) {
  420. // WTF?
  421. throw new UnsupportedOperationException("getDetails called with"
  422. + " null ChannelClientInfo");
  423. }
  424. final String[] res = new String[] {
  425. getModes(client),
  426. Styliser.CODE_NICKNAME + client.getClient().getNickname() + Styliser.CODE_NICKNAME,
  427. client.getClient().getUsername(),
  428. client.getClient().getHostname()
  429. };
  430. if (showColours) {
  431. final Map<?, ?> map = client.getMap();
  432. if (map.containsKey(ChannelClientProperty.TEXT_FOREGROUND)) {
  433. String prefix;
  434. if (map.containsKey(ChannelClientProperty.TEXT_BACKGROUND)) {
  435. prefix = "," + ColourManager.getHex((Color)
  436. map.get(ChannelClientProperty.TEXT_BACKGROUND));
  437. } else {
  438. prefix = Styliser.CODE_HEXCOLOUR + ColourManager.getHex((Color)
  439. map.get(ChannelClientProperty.TEXT_FOREGROUND));
  440. }
  441. res[1] = prefix + res[1] + Styliser.CODE_HEXCOLOUR;
  442. }
  443. }
  444. return res;
  445. }
  446. /** {@inheritDoc} */
  447. @Override
  448. protected boolean processNotificationArg(final Object arg, final List<Object> args) {
  449. if (arg instanceof ClientInfo) {
  450. // Format ClientInfos
  451. final ClientInfo clientInfo = (ClientInfo) arg;
  452. args.add(clientInfo.getNickname());
  453. args.add(clientInfo.getUsername());
  454. args.add(clientInfo.getHostname());
  455. return true;
  456. } else if (arg instanceof ChannelClientInfo) {
  457. // Format ChannelClientInfos
  458. final ChannelClientInfo clientInfo = (ChannelClientInfo) arg;
  459. args.addAll(Arrays.asList(getDetails(clientInfo)));
  460. return true;
  461. } else if (arg instanceof Topic) {
  462. // Format topics
  463. args.add("");
  464. args.addAll(Arrays.asList(server.parseHostmask(((Topic) arg).getClient())));
  465. args.add(((Topic) arg).getTopic());
  466. args.add(((Topic) arg).getTime() * 1000);
  467. return true;
  468. } else {
  469. // Everything else - default formatting
  470. return super.processNotificationArg(arg, args);
  471. }
  472. }
  473. /** {@inheritDoc} */
  474. @Override
  475. protected void modifyNotificationArgs(final List<Object> actionArgs,
  476. final List<Object> messageArgs) {
  477. messageArgs.add(channelInfo.getName());
  478. }
  479. // ---------------------------------------------------- TOPIC HANDLING -----
  480. /**
  481. * Adds the specified topic to this channel's topic list.
  482. *
  483. * @param topic The topic to be added.
  484. */
  485. public void addTopic(final Topic topic) {
  486. synchronized (topics) {
  487. topics.add(topic);
  488. }
  489. updateTitle();
  490. new Thread(new Runnable() {
  491. /** {@inheritDoc} */
  492. @Override
  493. public void run() {
  494. synchronized (listeners) {
  495. for (TopicChangeListener listener : listeners.get(TopicChangeListener.class)) {
  496. listener.topicChanged(Channel.this, topic);
  497. }
  498. }
  499. }
  500. }, "Topic change listener runner").start();
  501. }
  502. /**
  503. * Retrieve the topics that have been seen on this channel.
  504. *
  505. * @return A list of topics that have been seen on this channel, including
  506. * the current one.
  507. */
  508. public List<Topic> getTopics() {
  509. synchronized (topics) {
  510. return new ArrayList<Topic>(topics.getList());
  511. }
  512. }
  513. /**
  514. * Returns the current topic for this channel.
  515. *
  516. * @return Current channel topic
  517. */
  518. public Topic getCurrentTopic() {
  519. synchronized (topics) {
  520. if (topics.getList().isEmpty()) {
  521. return null;
  522. } else {
  523. return topics.get(topics.getList().size() - 1);
  524. }
  525. }
  526. }
  527. /**
  528. * Adds a new topic change listener to this channel.
  529. *
  530. * @param listener The listener to be added
  531. * @since 0.6.3
  532. */
  533. public void addTopicChangeListener(final TopicChangeListener listener) {
  534. synchronized (listeners) {
  535. listeners.add(TopicChangeListener.class, listener);
  536. }
  537. }
  538. /**
  539. * Removes an existing topic change listener from this channel.
  540. *
  541. * @param listener The listener to be removed
  542. * @since 0.6.3
  543. */
  544. public void removeTopicChangeListener(final TopicChangeListener listener) {
  545. synchronized (listeners) {
  546. listeners.remove(TopicChangeListener.class, listener);
  547. }
  548. }
  549. // ------------------------------------------ PARSER METHOD DELEGATION -----
  550. /**
  551. * Attempts to set the topic of this channel.
  552. *
  553. * @param topic The new topic to be used. An empty string will clear the
  554. * current topic
  555. */
  556. public void setTopic(final String topic) {
  557. channelInfo.setTopic(topic);
  558. }
  559. /**
  560. * Retrieves the maximum length that a topic on this channel can be.
  561. *
  562. * @return The maximum length that this channel's topic may be
  563. * @since 0.6.3
  564. */
  565. public int getMaxTopicLength() {
  566. return server.getParser().getMaxTopicLength();
  567. }
  568. }