1 module ircclient.ui.frame; 2 3 import dlangui; 4 import dlangui.dialogs.dialog; 5 import dlangui.core.settings; 6 7 import std.array : replaceFirst; 8 9 import ircclient.net.client; 10 import ircclient.ui.settingsdlg; 11 import ircclient.ui.settings; 12 13 import std.string : startsWith, indexOf, empty; 14 import std.path; 15 16 // action codes 17 enum IRCActions : int { 18 FileExit = 12300, 19 EditPreferences, 20 Connect, 21 Disconnect, 22 HelpAbout, 23 Join 24 } 25 26 // actions 27 const Action ACTION_FILE_EXIT = new Action(IRCActions.FileExit, "MENU_FILE_EXIT"c, "document-close"c, KeyCode.KEY_X, KeyFlag.Alt); 28 29 const Action ACTION_EDIT_COPY = (new Action(EditorActions.Copy, "MENU_EDIT_COPY"c, "edit-copy"c, KeyCode.KEY_C, KeyFlag.Control)).addAccelerator(KeyCode.INS, KeyFlag.Control).disableByDefault(); 30 const Action ACTION_EDIT_PASTE = (new Action(EditorActions.Paste, "MENU_EDIT_PASTE"c, "edit-paste"c, KeyCode.KEY_V, KeyFlag.Control)).addAccelerator(KeyCode.INS, KeyFlag.Shift).disableByDefault(); 31 const Action ACTION_EDIT_CUT = (new Action(EditorActions.Cut, "MENU_EDIT_CUT"c, "edit-cut"c, KeyCode.KEY_X, KeyFlag.Control)).addAccelerator(KeyCode.DEL, KeyFlag.Shift).disableByDefault(); 32 const Action ACTION_EDIT_UNDO = (new Action(EditorActions.Undo, "MENU_EDIT_UNDO"c, "edit-undo"c, KeyCode.KEY_Z, KeyFlag.Control)).disableByDefault(); 33 const Action ACTION_EDIT_REDO = (new Action(EditorActions.Redo, "MENU_EDIT_REDO"c, "edit-redo"c, KeyCode.KEY_Y, KeyFlag.Control)).addAccelerator(KeyCode.KEY_Z, KeyFlag.Control|KeyFlag.Shift).disableByDefault(); 34 35 const Action ACTION_EDIT_PREFERENCES = (new Action(IRCActions.EditPreferences, "MENU_EDIT_PREFERENCES"c, "configure"c, KeyCode.F9, 0)); 36 37 const Action ACTION_CONNECT = (new Action(IRCActions.Connect, "MENU_CONNECT"c, "connect"c, KeyCode.F5, 0)).disableByDefault(); 38 const Action ACTION_DISCONNECT = (new Action(IRCActions.Disconnect, "MENU_DISCONNECT"c, "disconnect"c, KeyCode.F5, 0)).disableByDefault(); 39 40 const Action ACTION_CHANNEL_JOIN = (new Action(IRCActions.Join, "MENU_CHANNEL_JOIN"c, "channel-join"c, KeyCode.F2, 0)).disableByDefault(); 41 42 const Action ACTION_HELP_ABOUT = new Action(IRCActions.HelpAbout, "MENU_HELP_ABOUT"c, "about"c, KeyCode.F1, 0); 43 44 class IRCFrame : AppFrame, IRCClientCallback { 45 46 MenuItem mainMenuItems; 47 IRCClient _client; 48 IRCSettings _settings; 49 50 51 this() { 52 } 53 54 ~this() { 55 if (_client) 56 destroy(_client); 57 } 58 59 override protected void initialize() { 60 _appName = "DlangUI_IRCClient"; 61 _settings = new IRCSettings(buildNormalizedPath(settingsDir, "settings.json")); 62 _settings.load(); 63 _settings.updateDefaults(); 64 _settings.save(); 65 super.initialize(); 66 } 67 68 /// create main menu 69 override protected MainMenu createMainMenu() { 70 mainMenuItems = new MenuItem(); 71 MenuItem fileItem = new MenuItem(new Action(1, "MENU_FILE")); 72 fileItem.add(//ACTION_FILE_NEW, ACTION_FILE_OPEN, 73 ACTION_HELP_ABOUT, ACTION_EDIT_PREFERENCES, ACTION_FILE_EXIT); 74 mainMenuItems.add(fileItem); 75 //MenuItem editItem = new MenuItem(new Action(2, "MENU_EDIT")); 76 //editItem.add(ACTION_EDIT_COPY, ACTION_EDIT_PASTE, 77 // ACTION_EDIT_CUT, ACTION_EDIT_UNDO, ACTION_EDIT_REDO, 78 // ACTION_EDIT_INDENT, ACTION_EDIT_UNINDENT, ACTION_EDIT_TOGGLE_LINE_COMMENT, ACTION_EDIT_TOGGLE_BLOCK_COMMENT, ACTION_DEBUG_START); 79 // 80 //editItem.add(ACTION_EDIT_PREFERENCES); 81 //mainMenuItems.add(editItem); 82 MainMenu mainMenu = new MainMenu(mainMenuItems); 83 return mainMenu; 84 } 85 86 87 /// create app toolbars 88 override protected ToolBarHost createToolbars() { 89 ToolBarHost res = new ToolBarHost(); 90 ToolBar tb; 91 tb = res.getOrAddToolbar("Standard"); 92 tb.addButtons( 93 ACTION_CONNECT, 94 ACTION_DISCONNECT, 95 ACTION_EDIT_PREFERENCES, 96 ACTION_SEPARATOR, 97 ACTION_HELP_ABOUT); 98 99 //tb = res.getOrAddToolbar("Edit"); 100 //tb.addButtons(ACTION_EDIT_COPY, ACTION_EDIT_PASTE, ACTION_EDIT_CUT, ACTION_SEPARATOR, 101 // ACTION_EDIT_UNDO, ACTION_EDIT_REDO, ACTION_EDIT_INDENT, ACTION_EDIT_UNINDENT); 102 return res; 103 } 104 105 bool onCanClose() { 106 // todo 107 return true; 108 } 109 110 /// override to handle specific actions 111 override bool handleAction(const Action a) { 112 if (a) { 113 switch (a.id) { 114 case IRCActions.FileExit: 115 if (onCanClose()) 116 window.close(); 117 return true; 118 case IRCActions.HelpAbout: 119 window.showMessageBox(UIString.fromRaw("About DlangUI IRC Client"d), 120 UIString.fromRaw("DLangUI IRC Client\n(C) Vadim Lopatin, 2015\nhttp://github.com/buggins/dlangui\nSimple IRC client"d)); 121 return true; 122 case IRCActions.EditPreferences: 123 showPreferences(); 124 return true; 125 case IRCActions.Connect: 126 case IRCActions.Disconnect: 127 if (!_client || _client.state == SocketState.Disconnected) 128 connect(); 129 else 130 _client.disconnect(); 131 return true; 132 default: 133 return super.handleAction(a); 134 } 135 } 136 return false; 137 } 138 139 void showPreferences() { 140 IRCSettings s = _settings.clone(); 141 SettingsDialog dlg = new SettingsDialog(this, s, !_client || _client.state == SocketState.Disconnected); 142 dlg.dialogResult = delegate(Dialog dlg, const Action result) { 143 if (result.id == ACTION_APPLY.id || result.id == ACTION_CONNECT.id) { 144 _settings.applySettings(s.setting); 145 _settings.save(); 146 } 147 if (result.id == ACTION_CONNECT.id) { 148 connect(); 149 } 150 }; 151 dlg.show(); 152 } 153 154 /// override to handle specific actions state (e.g. change enabled state for supported actions) 155 override bool handleActionStateRequest(const Action a) { 156 switch (a.id) { 157 case IRCActions.HelpAbout: 158 case IRCActions.EditPreferences: 159 a.state = ACTION_STATE_ENABLED; 160 return true; 161 case IRCActions.Connect: 162 a.state = !_client || _client.state == SocketState.Disconnected ? ACTION_STATE_ENABLED : ACTION_STATE_INVISIBLE; 163 return true; 164 case IRCActions.Disconnect: 165 a.state = !_client || _client.state == SocketState.Disconnected ? ACTION_STATE_INVISIBLE : ACTION_STATE_ENABLED; 166 return true; 167 default: 168 return super.handleActionStateRequest(a); 169 } 170 } 171 172 TabWidget _tabs; 173 /// create app body widget 174 override protected Widget createBody() { 175 _tabs = new TabWidget("TABS"); 176 _tabs.setStyles(STYLE_DOCK_WINDOW, STYLE_TAB_UP_DARK, STYLE_TAB_UP_BUTTON_DARK, STYLE_TAB_UP_BUTTON_DARK_TEXT, STYLE_DOCK_HOST_BODY); 177 _tabs.layoutWidth = FILL_PARENT; 178 _tabs.layoutHeight = FILL_PARENT; 179 //tabs.addTab(new IRCWindow("sample"), "Sample"d); 180 statusLine.setStatusText(toUTF32("Not Connected")); 181 _tabs.tabChanged = delegate(string newActiveTabId, string previousTabId) { 182 if (IRCWindow w = cast(IRCWindow)_tabs.tabBody(newActiveTabId)) { 183 w.onTabActivated(); 184 } 185 }; 186 return _tabs; 187 } 188 189 void connect() { 190 if (!_client) { 191 _client = new IRCClient(); 192 AsyncSocket connection = window.createAsyncSocket(_client); 193 _client.socket = connection; 194 _client.callback = this; 195 } 196 _client.connect(_settings.host, _settings.port); 197 } 198 199 IRCWindow getOrCreateWindowFor(string party, bool activate) { 200 string winId = party; 201 IRCWindow w = cast(IRCWindow)_tabs.tabBody(winId); 202 if (!w) { 203 w = new IRCWindow(winId, this, _client); 204 _tabs.addTab(w, toUTF32(winId)); 205 activate = true; 206 } 207 if (activate) { 208 _tabs.selectTab(winId); 209 w.onTabActivated(); 210 } 211 return w; 212 } 213 214 void onIRCConnect(IRCClient client) { 215 IRCWindow w = getOrCreateWindowFor(client.hostPort, true); 216 w.addLine("connected to " ~ client.hostPort); 217 client.sendMessage("USER " ~ _settings.userName ~ " 0 * :" ~ _settings.userRealName); 218 client.nick(_settings.nick); 219 string channel = _settings.defChannel; 220 if (_settings.joinOnConnect && channel.length > 1 && channel.startsWith("#")) 221 client.join(channel); 222 statusLine.setStatusText(toUTF32("Connected to " ~ client.hostPort)); 223 } 224 225 void onIRCDisconnect(IRCClient client) { 226 IRCWindow w = getOrCreateWindowFor(client.hostPort, false); 227 w.addLine("disconnected from " ~ client.hostPort); 228 statusLine.setStatusText(toUTF32("Disconnected")); 229 } 230 231 void onIRCPing(IRCClient client, string message) { 232 IRCWindow w = getOrCreateWindowFor(client.hostPort, false); 233 w.addLine("PING " ~ message); 234 client.pong(message); 235 } 236 237 void onIRCPrivmsg(IRCClient client, IRCAddress source, string target, string message) { 238 string wid = target.startsWith("#") ? target : client.hostPort; 239 if (target == client.nick) 240 wid = source.nick; 241 else if (source.nick == client.nick) 242 wid = target; 243 IRCWindow w = getOrCreateWindowFor(wid, false); 244 w.addLine("<" ~ (!source.nick.empty ? source.nick : source.full) ~ "> " ~ message); 245 } 246 247 void onIRCNotice(IRCClient client, IRCAddress source, string target, string message) { 248 IRCWindow w = getOrCreateWindowFor(target.startsWith("#") ? target : client.hostPort, false); 249 w.addLine("-" ~ source.full ~ "- " ~ message); 250 } 251 252 void onIRCMessage(IRCClient client, IRCMessage message) { 253 IRCWindow w = getOrCreateWindowFor(client.hostPort, false); 254 switch (message.commandId) with (IRCCommand) { 255 case JOIN: 256 case PART: 257 if (message.sourceAddress && !message.sourceAddress.nick.empty && message.target.startsWith("#")) { 258 w = getOrCreateWindowFor(message.target, false); 259 if (message.commandId == JOIN) { 260 w.addLine("* " ~ message.sourceAddress.longName ~ " has joined " ~ message.target); 261 } else { 262 w.addLine("* " ~ message.sourceAddress.longName ~ " has left " ~ message.target ~ (message.message.empty ? "" : ("(Reason: " ~ message.message ~ ")"))); 263 } 264 IRCChannel channel = _client.channelByName(message.target); 265 w.updateUserList(channel); 266 } 267 return; 268 case CHANNEL_NAMES_LIST_END: 269 if (message.target.startsWith("#")) { 270 w = getOrCreateWindowFor(message.target, false); 271 IRCChannel channel = _client.channelByName(message.target); 272 w.updateUserList(channel); 273 } 274 return; 275 default: 276 if (message.commandId < 1000) { 277 // custom server messages 278 w.addLine(message.message); 279 return; 280 } 281 break; 282 } 283 w.addLine(message.msg); 284 } 285 } 286 287 enum IRCWindowKind { 288 Server, 289 Channel, 290 Private 291 } 292 293 class IRCWindow : VerticalLayout, EditorActionHandler { 294 private: 295 IRCFrame _frame; 296 LogWidget _editBox; 297 StringListWidget _listBox; 298 EditLine _editLine; 299 IRCClient _client; 300 IRCWindowKind _kind; 301 dstring[] _userNames; 302 public: 303 this(string ID, IRCFrame frame, IRCClient client) { 304 super(ID); 305 _client = client; 306 _frame = frame; 307 layoutWidth = FILL_PARENT; 308 layoutHeight = FILL_PARENT; 309 HorizontalLayout hlayout = new HorizontalLayout(); 310 hlayout.layoutWidth = FILL_PARENT; 311 hlayout.layoutHeight = FILL_PARENT; 312 _editBox = new LogWidget(); 313 _editBox.layoutWidth = FILL_PARENT; 314 _editBox.layoutHeight = FILL_PARENT; 315 hlayout.addChild(_editBox); 316 if (ID.startsWith("#")) { 317 _listBox = new StringListWidget(); 318 _listBox.layoutHeight = FILL_PARENT; 319 _listBox.layoutWidth = WRAP_CONTENT; 320 _listBox.minWidth = pointsToPixels(100); 321 _listBox.maxWidth = pointsToPixels(200); 322 _listBox.orientation = Orientation.Vertical; 323 //_listBox.items = ["Nick1"d, "Nick2"d]; 324 hlayout.addChild(new ResizerWidget(null, Orientation.Horizontal)); 325 hlayout.addChild(_listBox); 326 327 _listBox.itemClick = delegate(Widget source, int itemIndex) { 328 auto user = itemIndex >= 0 && itemIndex < _userNames.length ? toUTF8(_userNames[itemIndex]) : null; 329 if (!user.empty && user != _client.nick) { 330 _frame.getOrCreateWindowFor(user, true); 331 } 332 return true; 333 }; 334 335 _kind = IRCWindowKind.Channel; 336 } else { 337 if (id.indexOf(':') >= 0) 338 _kind = IRCWindowKind.Server; 339 else 340 _kind = IRCWindowKind.Private; 341 } 342 addChild(hlayout); 343 _editLine = new EditLine(); 344 addChild(_editLine); 345 _editLine.editorAction = this; 346 } 347 void addLine(string s) { 348 _editBox.appendText(toUTF32(s ~ "\n")); 349 if (visible) 350 window.update(); 351 } 352 void updateUserList(IRCChannel channel) { 353 _userNames = channel.userNames; 354 _listBox.items = _userNames; 355 if (window) 356 window.update(); 357 } 358 bool onEditorAction(const Action action) { 359 if (!_editLine.text.empty) { 360 string s = toUTF8(_editLine.text); 361 _editLine.text = ""d; 362 if (s.startsWith("/")) { 363 Log.d("Custom command: " ~ s); 364 // command 365 string cmd = parseDelimitedParameter(s); 366 367 if (cmd == "/quit") { 368 _client.quit(s); 369 return true; 370 } 371 372 string param = parseDelimitedParameter(s); 373 if (cmd == "/nick" && !param.empty) { 374 _client.nick(param); 375 } else if (cmd == "/join" && param.startsWith("#")) { 376 _client.join(param); 377 } else if (cmd == "/part" && param.startsWith("#")) { 378 _client.part(param, s); 379 } else if (cmd == "/msg" && !param.empty && !s.empty) { 380 _client.privMsg(param, s); 381 } else { 382 Log.d("Unknown command: " ~ cmd); 383 addLine("Supported commands: /nick /join /part /msg /quit"); 384 } 385 } else { 386 // message 387 if (_kind != IRCWindowKind.Server) { 388 _client.privMsg(id, s); 389 } 390 } 391 } 392 return true; 393 } 394 void onTabActivated() { 395 _editLine.setFocus(); 396 } 397 }