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 }