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 22KB

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