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 }