From 437391c7de0819a42cb8b919cc0f7dee9acacd05 Mon Sep 17 00:00:00 2001 From: Vadim Lopatin Date: Tue, 15 Mar 2016 14:20:55 +0300 Subject: [PATCH] AsyncSocket support in DlangUI; IRC Client example for AsyncSocket --- dlangui-msvc.sln | 15 + dlangui-msvc.visualdproj | 2 + examples/ircclient/dub.json | 21 ++ examples/ircclient/ircclient.visualdproj | 215 +++++++++++ examples/ircclient/src/ircclient/net/client.d | 274 ++++++++++++++ examples/ircclient/src/main.d | 343 ++++++++++++++++++ examples/ircclient/src/win_app.def | 2 + .../views/res/hdpi/document-close.png | Bin 0 -> 1504 bytes .../views/res/hdpi/document-open-recent.png | Bin 0 -> 1576 bytes .../views/res/hdpi/document-open.png | Bin 0 -> 1798 bytes .../views/res/hdpi/document-save-as.png | Bin 0 -> 2152 bytes .../views/res/hdpi/document-save.png | Bin 0 -> 1263 bytes .../ircclient/views/res/hdpi/edit-clear.png | Bin 0 -> 2066 bytes .../ircclient/views/res/hdpi/edit-copy.png | Bin 0 -> 828 bytes .../ircclient/views/res/hdpi/edit-cut.png | Bin 0 -> 892 bytes .../ircclient/views/res/hdpi/edit-delete.png | Bin 0 -> 1259 bytes .../ircclient/views/res/hdpi/edit-paste.png | Bin 0 -> 1406 bytes .../ircclient/views/res/hdpi/edit-redo.png | Bin 0 -> 1998 bytes .../ircclient/views/res/hdpi/edit-undo.png | Bin 0 -> 2020 bytes examples/ircclient/views/res/i18n/en.ini | 40 ++ examples/ircclient/views/res/i18n/ru.ini | 34 ++ .../ircclient/views/res/mdpi/cr3_logo.png | Bin 0 -> 8433 bytes .../ircclient/views/res/mdpi/debug-run.png | Bin 0 -> 383 bytes .../views/res/mdpi/document-close.png | Bin 0 -> 605 bytes .../ircclient/views/res/mdpi/document-new.png | Bin 0 -> 516 bytes .../views/res/mdpi/document-open-recent.png | Bin 0 -> 593 bytes .../views/res/mdpi/document-open.png | Bin 0 -> 701 bytes .../views/res/mdpi/document-properties.png | Bin 0 -> 635 bytes .../views/res/mdpi/document-save-as.png | Bin 0 -> 771 bytes .../views/res/mdpi/document-save.png | Bin 0 -> 559 bytes .../ircclient/views/res/mdpi/edit-copy.png | Bin 0 -> 436 bytes .../ircclient/views/res/mdpi/edit-cut.png | Bin 0 -> 368 bytes .../ircclient/views/res/mdpi/edit-indent.png | Bin 0 -> 413 bytes .../ircclient/views/res/mdpi/edit-paste.png | Bin 0 -> 598 bytes .../ircclient/views/res/mdpi/edit-redo.png | Bin 0 -> 753 bytes .../ircclient/views/res/mdpi/edit-undo.png | Bin 0 -> 810 bytes .../views/res/mdpi/edit-unindent.png | Bin 0 -> 367 bytes .../ircclient/views/res/mdpi/text-dml.png | Bin 0 -> 684 bytes .../ircclient/views/res/mdpi/tx_fabric.jpg | Bin 0 -> 9061 bytes examples/ircclient/views/resources.list | 20 + src/dlangui/core/asyncsocket.d | 195 ++++++++++ src/dlangui/core/queue.d | 144 ++++++++ src/dlangui/platforms/common/platform.d | 7 + 43 files changed, 1312 insertions(+) create mode 100644 examples/ircclient/dub.json create mode 100644 examples/ircclient/ircclient.visualdproj create mode 100644 examples/ircclient/src/ircclient/net/client.d create mode 100644 examples/ircclient/src/main.d create mode 100644 examples/ircclient/src/win_app.def create mode 100644 examples/ircclient/views/res/hdpi/document-close.png create mode 100644 examples/ircclient/views/res/hdpi/document-open-recent.png create mode 100644 examples/ircclient/views/res/hdpi/document-open.png create mode 100644 examples/ircclient/views/res/hdpi/document-save-as.png create mode 100644 examples/ircclient/views/res/hdpi/document-save.png create mode 100644 examples/ircclient/views/res/hdpi/edit-clear.png create mode 100644 examples/ircclient/views/res/hdpi/edit-copy.png create mode 100644 examples/ircclient/views/res/hdpi/edit-cut.png create mode 100644 examples/ircclient/views/res/hdpi/edit-delete.png create mode 100644 examples/ircclient/views/res/hdpi/edit-paste.png create mode 100644 examples/ircclient/views/res/hdpi/edit-redo.png create mode 100644 examples/ircclient/views/res/hdpi/edit-undo.png create mode 100644 examples/ircclient/views/res/i18n/en.ini create mode 100644 examples/ircclient/views/res/i18n/ru.ini create mode 100644 examples/ircclient/views/res/mdpi/cr3_logo.png create mode 100644 examples/ircclient/views/res/mdpi/debug-run.png create mode 100644 examples/ircclient/views/res/mdpi/document-close.png create mode 100644 examples/ircclient/views/res/mdpi/document-new.png create mode 100644 examples/ircclient/views/res/mdpi/document-open-recent.png create mode 100644 examples/ircclient/views/res/mdpi/document-open.png create mode 100644 examples/ircclient/views/res/mdpi/document-properties.png create mode 100644 examples/ircclient/views/res/mdpi/document-save-as.png create mode 100644 examples/ircclient/views/res/mdpi/document-save.png create mode 100644 examples/ircclient/views/res/mdpi/edit-copy.png create mode 100644 examples/ircclient/views/res/mdpi/edit-cut.png create mode 100644 examples/ircclient/views/res/mdpi/edit-indent.png create mode 100644 examples/ircclient/views/res/mdpi/edit-paste.png create mode 100644 examples/ircclient/views/res/mdpi/edit-redo.png create mode 100644 examples/ircclient/views/res/mdpi/edit-undo.png create mode 100644 examples/ircclient/views/res/mdpi/edit-unindent.png create mode 100644 examples/ircclient/views/res/mdpi/text-dml.png create mode 100644 examples/ircclient/views/res/mdpi/tx_fabric.jpg create mode 100644 examples/ircclient/views/resources.list create mode 100644 src/dlangui/core/asyncsocket.d create mode 100644 src/dlangui/core/queue.d diff --git a/dlangui-msvc.sln b/dlangui-msvc.sln index 5238ee35..1412240a 100644 --- a/dlangui-msvc.sln +++ b/dlangui-msvc.sln @@ -49,6 +49,11 @@ Project("{002A2DE9-8BB6-484D-9802-7E4AD4084715}") = "dwindow", "..\dwindow\dwind EndProject Project("{002A2DE9-8BB6-484D-9802-7E4AD4084715}") = "dmdbug", "examples\dmdbug\dmdbug.visualdproj", "{6FD15ECD-35AF-457D-9327-DD8C2A0EADF1}" EndProject +Project("{002A2DE9-8BB6-484D-9802-7E4AD4084715}") = "ircclient", "examples\ircclient\ircclient.visualdproj", "{20722E6B-CA27-467F-8BB8-07F80106B478}" + ProjectSection(ProjectDependencies) = postProject + {52A2ABB9-2CF7-4D5F-AE8C-75B21F8585A5} = {52A2ABB9-2CF7-4D5F-AE8C-75B21F8585A5} + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Win32 = Debug|Win32 @@ -182,6 +187,16 @@ Global {6FD15ECD-35AF-457D-9327-DD8C2A0EADF1}.Unittest|Win32.Build.0 = Release|Win32 {6FD15ECD-35AF-457D-9327-DD8C2A0EADF1}.Unittest|x64.ActiveCfg = Release|Win32 {6FD15ECD-35AF-457D-9327-DD8C2A0EADF1}.Unittest|x64.Build.0 = Release|Win32 + {20722E6B-CA27-467F-8BB8-07F80106B478}.Debug|Win32.ActiveCfg = Debug|Win32 + {20722E6B-CA27-467F-8BB8-07F80106B478}.Debug|Win32.Build.0 = Debug|Win32 + {20722E6B-CA27-467F-8BB8-07F80106B478}.Debug|x64.ActiveCfg = Debug|Win32 + {20722E6B-CA27-467F-8BB8-07F80106B478}.Release|Win32.ActiveCfg = Release|Win32 + {20722E6B-CA27-467F-8BB8-07F80106B478}.Release|Win32.Build.0 = Release|Win32 + {20722E6B-CA27-467F-8BB8-07F80106B478}.Release|x64.ActiveCfg = Release|Win32 + {20722E6B-CA27-467F-8BB8-07F80106B478}.Unittest|Win32.ActiveCfg = Release|Win32 + {20722E6B-CA27-467F-8BB8-07F80106B478}.Unittest|Win32.Build.0 = Release|Win32 + {20722E6B-CA27-467F-8BB8-07F80106B478}.Unittest|x64.ActiveCfg = Release|Win32 + {20722E6B-CA27-467F-8BB8-07F80106B478}.Unittest|x64.Build.0 = Release|Win32 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/dlangui-msvc.visualdproj b/dlangui-msvc.visualdproj index c88cfe91..cccdbf5d 100644 --- a/dlangui-msvc.visualdproj +++ b/dlangui-msvc.visualdproj @@ -735,6 +735,7 @@ + @@ -747,6 +748,7 @@ + diff --git a/examples/ircclient/dub.json b/examples/ircclient/dub.json new file mode 100644 index 00000000..fb2decd3 --- /dev/null +++ b/examples/ircclient/dub.json @@ -0,0 +1,21 @@ +{ + "name": "ircclient", + "description": "dlangui library example IRC Client", + "homepage": "https://github.com/buggins/dlangui", + "license": "Boost", + "authors": ["Vadim Lopatin"], + + "stringImportPaths": ["views", "views/res", "views/res/i18n", "views/res/mdpi"], + + "targetPath": "bin", + "targetName": "ircclient", + "targetType": "executable", + + "sourceFiles-windows": ["$PACKAGE_DIR/src/win_app.def"], + + "versions": ["EmbedStandardResources"], + + "dependencies": { + "dlangui": {"path": "../../"} + } +} diff --git a/examples/ircclient/ircclient.visualdproj b/examples/ircclient/ircclient.visualdproj new file mode 100644 index 00000000..3b97a2fa --- /dev/null +++ b/examples/ircclient/ircclient.visualdproj @@ -0,0 +1,215 @@ + + {20722E6B-CA27-467F-8BB8-07F80106B478} + + 0 + 0 + 0 + 2 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 3 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 2 + 0 + 0 + 0 + 0 + 0 + $(CC) -c + 1 + $(DMDInstallDir)windows\bin\dmd.exe + $(SolutionDir)/src $(SolutionDir)/3rdparty $(SolutionDir)/deps/DerelictGL3/source $(SolutionDir)/deps/DerelictUtil/source $(SolutionDir)/deps/DerelictFT/source $(SolutionDir)/deps/DerelictSDL2/source + views views/res views/res/i18n views/res/mdpi views/res/hdpi + $(ConfigurationName) + $(OutDir) + + + 0 + + + + + 0 + + + 1 + $(IntDir)\$(TargetName).json + 0 + + 0 + USE_OPENGL EmbedStandardResources ForceLogs + 0 + 0 + 0 + + + + 0 + + 1 + $(VisualDInstallDir)cv2pdb\cv2pdb.exe + 0 + 0 + 0 + + + + + + + + $(OutDir)\$(ProjectName).exe + 1 + 2 + 0 + + + + *.obj;*.cmd;*.build;*.json;*.dep + + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 1 + 1 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 2.043 + 0 + 0 + 0 + 0 + 0 + $(CC) -c + 1 + $(DMDInstallDir)windows\bin\dmd.exe + + + $(ConfigurationName) + $(OutDir) + + + 0 + + + + + 0 + + + 1 + $(IntDir)\$(TargetName).json + 0 + + 0 + + 0 + 0 + 0 + + + + 0 + + 0 + $(VisualDInstallDir)cv2pdb\cv2pdb.exe + 0 + 0 + 0 + + + + + + + + $(OutDir)\$(ProjectName).exe + 1 + 1 + 0 + + + + *.obj;*.cmd;*.build;*.json;*.dep + + + + + + + + + + diff --git a/examples/ircclient/src/ircclient/net/client.d b/examples/ircclient/src/ircclient/net/client.d new file mode 100644 index 00000000..1c06386b --- /dev/null +++ b/examples/ircclient/src/ircclient/net/client.d @@ -0,0 +1,274 @@ +module ircclient.net.client; + +public import dlangui.core.asyncsocket; +import dlangui.core.logger; +import std.string : empty, format; +import std.conv : to; + +interface IRCClientCallback { + void onIRCConnect(IRCClient client); + void onIRCDisconnect(IRCClient client); + void onIRCMessage(IRCClient client, IRCMessage message); + void onIRCPing(IRCClient client, string message); + void onIRCPrivmsg(IRCClient client, IRCAddress source, string target, string message); + void onIRCNotice(IRCClient client, IRCAddress source, string target, string message); +} + +enum IRCCommand : int { + UNKNOWN, + USER = 1000, + PRIVMSG, // :source PRIVMSG :Message + NOTICE, // :source NOTICE :Message + NICK, + PING, // PING :message + PONG, // PONG :message + QUIT, // :source QUIT :reason + JOIN, // :source JOIN :#channel + PART, // :source PART #channel :reason + MODE, // +} + +IRCCommand findCommandId(string s) { + if (s.empty) + return IRCCommand.UNKNOWN; + if (s[0] >= '0' && s[0] <= '9') { + // parse numeric command ID + int n = 0; + foreach(ch; s) + if (ch >= '0' && ch <= '9') + n = n * 10 + (ch - '0'); + return cast(IRCCommand)n; + } + + switch (s) with(IRCCommand) { + case "USER": return USER; + case "PRIVMSG": return PRIVMSG; + case "NICK": return NICK; + case "QUIT": return QUIT; + case "PING": return PING; + case "PONG": return PONG; + case "JOIN": return JOIN; + case "PART": return PART; + case "NOTICE": return NOTICE; + default: + return UNKNOWN; + } +} + +/// IRC message +class IRCMessage { + /// full message text + string msg; + /// optional first parameter of message, starting with : -- e.g. ":holmes.freenode.net" + string source; // source of message + IRCAddress sourceAddress; // parsed source + string command; // command text + IRCCommand commandId = IRCCommand.UNKNOWN; // command id + string[] params; // all parameters after command + string message; // optional message parameter, w/o starting : + string target; // for some command types - message target + /// parse message text + bool parse(string s) { + msg = s; + if (s.empty) + return false; + if (s[0] == ':') { + // parse source + source = parseDelimitedParameter(s); + if (source.length < 2) + return false; + source = source[1 .. $]; + sourceAddress = new IRCAddress(source); + } + command = parseDelimitedParameter(s); + if (command.empty) + return false; + commandId = findCommandId(command); + while (!s.empty) { + if (s[0] == ':') { + params ~= s; + message = s[1 .. $]; + break; + } else { + params ~= parseDelimitedParameter(s); + } + } + switch(commandId) with (IRCCommand) { + case PRIVMSG: + case NOTICE: + case JOIN: + case PART: + if (params.length > 0) + target = params[0]; + break; + default: + break; + } + return true; + } +} + +class IRCAddress { + string full; + string host; + string channel; + string nick; + string username; + this(string s) { + full = s; + string s1 = parseDelimitedParameter(s, '!'); + if (!s.empty) { + // VadimLopatin!~Buggins@149.62.27.44 + nick = s1; + username = s; + } else { + host = s1; + } + } + @property string longName() { + if (!nick.empty) { + return nick ~ " (" ~ username ~ ")"; + } else { + return full; + } + } +} + +/// IRC Client connection implementation +class IRCClient : AsyncSocketCallback { +protected: + AsyncSocket _socket; + IRCClientCallback _callback; + char[] _readbuf; + string _host; + ushort _port; + string _nick; + void onDataReceived(AsyncSocket socket, ubyte[] data) { + _readbuf ~= cast(char[])data; + // split by lines + int start = 0; + for (int i = 0; i + 1 < _readbuf.length; i++) { + if (_readbuf[i] == '\r' && _readbuf[i + 1] == '\n') { + if (i > start) + onMessageText(_readbuf[start .. i].dup); + start = i + 2; + } + } + if (start < _readbuf.length) { + // has unfinished text + _readbuf = _readbuf[start .. $].dup; + } else { + // end of buffer + _readbuf.length = 0; + } + } + void onMessageText(string msgText) { + IRCMessage msg = new IRCMessage(); + if (msg.parse(msgText)) { + onMessage(msg); + } else { + Log.e("cannot parse IRC message " ~ msgText); + } + } + void onMessage(IRCMessage msg) { + Log.d("MSG: " ~ msg.msg); + switch (msg.commandId) with (IRCCommand) { + case PING: + _callback.onIRCPing(this, msg.message); + break; + case PRIVMSG: + _callback.onIRCPrivmsg(this, msg.sourceAddress, msg.target, msg.message); + break; + case NOTICE: + _callback.onIRCNotice(this, msg.sourceAddress, msg.target, msg.message); + break; + default: + _callback.onIRCMessage(this, msg); + break; + } + } + void onConnect(AsyncSocket socket) { + Log.e("onConnect"); + _readbuf.length = 0; + _callback.onIRCConnect(this); + } + void onDisconnect(AsyncSocket socket) { + Log.e("onDisconnect"); + _readbuf.length = 0; + _callback.onIRCDisconnect(this); + } + void onError(AsyncSocket socket, SocketError error, string msg) { + } +public: + this() { + } + ~this() { + if (_socket) + destroy(_socket); + } + @property string host() { return _host; } + @property ushort port() { return _port; } + @property string hostPort() { return "%s:%d".format(_host, _port); } + /// set socket to use + @property void socket(AsyncSocket sock) { + _socket = sock; + } + @property void callback(IRCClientCallback callback) { + _callback = callback; + } + void connect(string host, ushort port) { + _host = host; + _port = port; + _socket.connect(host, port); + } + void sendMessage(string msg) { + Log.d("CMD: " ~ msg); + _socket.send(cast(ubyte[])(msg ~ "\r\n")); + } + void pong(string msg) { + sendMessage("PONG :" ~ msg); + } + void privMsg(string destination, string message) { + sendMessage("PRIVMSG " ~ destination ~ " :" ~ message); + _callback.onIRCPrivmsg(this, new IRCAddress(_nick ~ "!~username@host"), destination, message); + } + void disconnect() { + _socket.disconnect(); + } + @property string nick() { + return _nick; + } + @property void nick(string nickName) { + if (_nick.empty) + _nick = nickName; + sendMessage("NICK " ~ nickName); + } + void join(string channel) { + sendMessage("JOIN " ~ channel); + } + void part(string channel, string message) { + sendMessage("PART " ~ channel ~ (message.empty ? "" : ":" ~ message)); + } + + void quit(string message) { + sendMessage("QUIT " ~ (message.empty ? "" : ":" ~ message)); + } +} + +/// utility function to get first space delimited parameter from string +string parseDelimitedParameter(ref string s, char delimiter = ' ') { + string res; + int i = 0; + // parse source + for (; i < s.length; i++) { + if (s[i] == delimiter) + break; + } + if (i > 0) { + res = s[0 .. i]; + } + i++; + s = i < s.length ? s[i .. $] : null; + return res; +} + diff --git a/examples/ircclient/src/main.d b/examples/ircclient/src/main.d new file mode 100644 index 00000000..f712739b --- /dev/null +++ b/examples/ircclient/src/main.d @@ -0,0 +1,343 @@ +module dmledit; + +import dlangui; +import dlangui.dialogs.filedlg; +import dlangui.dialogs.dialog; +import dlangui.dml.dmlhighlight; +import std.array : replaceFirst; +import ircclient.net.client; +import std.string : startsWith, indexOf; + +mixin APP_ENTRY_POINT; + +// action codes +enum IDEActions : int { + //ProjectOpen = 1010000, + FileNew = 1010000, + FileOpen, + FileSave, + FileSaveAs, + FileSaveAll, + FileClose, + FileExit, + EditPreferences, + DebugStart, + HelpAbout, +} + +// actions +const Action ACTION_FILE_NEW = new Action(IDEActions.FileNew, "MENU_FILE_NEW"c, "document-new", KeyCode.KEY_N, KeyFlag.Control); +const Action ACTION_FILE_SAVE = (new Action(IDEActions.FileSave, "MENU_FILE_SAVE"c, "document-save", KeyCode.KEY_S, KeyFlag.Control)).disableByDefault(); +const Action ACTION_FILE_SAVE_AS = (new Action(IDEActions.FileSaveAs, "MENU_FILE_SAVE_AS"c)).disableByDefault(); +const Action ACTION_FILE_OPEN = new Action(IDEActions.FileOpen, "MENU_FILE_OPEN"c, "document-open", KeyCode.KEY_O, KeyFlag.Control); +const Action ACTION_FILE_EXIT = new Action(IDEActions.FileExit, "MENU_FILE_EXIT"c, "document-close"c, KeyCode.KEY_X, KeyFlag.Alt); +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(); +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(); +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(); +const Action ACTION_EDIT_UNDO = (new Action(EditorActions.Undo, "MENU_EDIT_UNDO"c, "edit-undo"c, KeyCode.KEY_Z, KeyFlag.Control)).disableByDefault(); +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(); +const Action ACTION_EDIT_INDENT = (new Action(EditorActions.Indent, "MENU_EDIT_INDENT"c, "edit-indent"c, KeyCode.TAB, 0)).addAccelerator(KeyCode.KEY_BRACKETCLOSE, KeyFlag.Control).disableByDefault(); +const Action ACTION_EDIT_UNINDENT = (new Action(EditorActions.Unindent, "MENU_EDIT_UNINDENT"c, "edit-unindent", KeyCode.TAB, KeyFlag.Shift)).addAccelerator(KeyCode.KEY_BRACKETOPEN, KeyFlag.Control).disableByDefault(); +const Action ACTION_EDIT_TOGGLE_LINE_COMMENT = (new Action(EditorActions.ToggleLineComment, "MENU_EDIT_TOGGLE_LINE_COMMENT"c, null, KeyCode.KEY_DIVIDE, KeyFlag.Control)).disableByDefault(); +const Action ACTION_EDIT_TOGGLE_BLOCK_COMMENT = (new Action(EditorActions.ToggleBlockComment, "MENU_EDIT_TOGGLE_BLOCK_COMMENT"c, null, KeyCode.KEY_DIVIDE, KeyFlag.Control|KeyFlag.Shift)).disableByDefault(); +const Action ACTION_EDIT_PREFERENCES = (new Action(IDEActions.EditPreferences, "MENU_EDIT_PREFERENCES"c, null)).disableByDefault(); +const Action ACTION_DEBUG_START = new Action(IDEActions.DebugStart, "MENU_DEBUG_UPDATE_PREVIEW"c, "debug-run"c, KeyCode.F5, 0); +const Action ACTION_HELP_ABOUT = new Action(IDEActions.HelpAbout, "MENU_HELP_ABOUT"c); + +class IRCFrame : AppFrame, IRCClientCallback { + + MenuItem mainMenuItems; + IRCClient _client; + + ~this() { + if (_client) + destroy(_client); + } + + override protected void initialize() { + _appName = "DlangUI_IRCClient"; + super.initialize(); + } + + /// create main menu + override protected MainMenu createMainMenu() { + mainMenuItems = new MenuItem(); + MenuItem fileItem = new MenuItem(new Action(1, "MENU_FILE")); + fileItem.add(//ACTION_FILE_NEW, ACTION_FILE_OPEN, + ACTION_FILE_EXIT); + mainMenuItems.add(fileItem); + //MenuItem editItem = new MenuItem(new Action(2, "MENU_EDIT")); + //editItem.add(ACTION_EDIT_COPY, ACTION_EDIT_PASTE, + // ACTION_EDIT_CUT, ACTION_EDIT_UNDO, ACTION_EDIT_REDO, + // ACTION_EDIT_INDENT, ACTION_EDIT_UNINDENT, ACTION_EDIT_TOGGLE_LINE_COMMENT, ACTION_EDIT_TOGGLE_BLOCK_COMMENT, ACTION_DEBUG_START); + // + //editItem.add(ACTION_EDIT_PREFERENCES); + //mainMenuItems.add(editItem); + MainMenu mainMenu = new MainMenu(mainMenuItems); + return mainMenu; + } + + + /// create app toolbars + override protected ToolBarHost createToolbars() { + ToolBarHost res = new ToolBarHost(); + ToolBar tb; + tb = res.getOrAddToolbar("Standard"); + tb.addButtons(//ACTION_FILE_NEW, ACTION_FILE_OPEN, ACTION_FILE_SAVE, ACTION_SEPARATOR, + ACTION_DEBUG_START); + + //tb = res.getOrAddToolbar("Edit"); + //tb.addButtons(ACTION_EDIT_COPY, ACTION_EDIT_PASTE, ACTION_EDIT_CUT, ACTION_SEPARATOR, + // ACTION_EDIT_UNDO, ACTION_EDIT_REDO, ACTION_EDIT_INDENT, ACTION_EDIT_UNINDENT); + return res; + } + + bool onCanClose() { + // todo + return true; + } + + /// override to handle specific actions + override bool handleAction(const Action a) { + if (a) { + switch (a.id) { + case IDEActions.FileExit: + if (onCanClose()) + window.close(); + return true; + case IDEActions.HelpAbout: + window.showMessageBox(UIString("About DlangUI IRC Client"d), + UIString("DLangUI IRC Client\n(C) Vadim Lopatin, 2015\nhttp://github.com/buggins/dlangui\nSimple IRC client"d)); + return true; + case IDEActions.EditPreferences: + //showPreferences(); + return true; + case IDEActions.DebugStart: + connect(); + return true; + default: + return super.handleAction(a); + } + } + return false; + } + + /// override to handle specific actions state (e.g. change enabled state for supported actions) + override bool handleActionStateRequest(const Action a) { + switch (a.id) { + case IDEActions.HelpAbout: + case IDEActions.FileNew: + case IDEActions.FileSave: + case IDEActions.FileOpen: + case IDEActions.DebugStart: + case IDEActions.EditPreferences: + a.state = ACTION_STATE_ENABLED; + return true; + default: + return super.handleActionStateRequest(a); + } + } + + TabWidget _tabs; + /// create app body widget + override protected Widget createBody() { + _tabs = new TabWidget("TABS"); + _tabs.layoutWidth = FILL_PARENT; + _tabs.layoutHeight = FILL_PARENT; + //tabs.addTab(new IRCWindow("sample"), "Sample"d); + statusLine.setStatusText(toUTF32("Not Connected")); + return _tabs; + } + + void connect() { + if (!_client) { + _client = new IRCClient(); + AsyncSocket connection = window.createAsyncSocket(_client); + _client.socket = connection; + _client.callback = this; + } + _client.connect("irc.freenode.net", 6667); + } + + IRCWindow getOrCreateWindowFor(string party) { + string winId = party; + IRCWindow w = cast(IRCWindow)_tabs.tabBody(winId); + if (!w) { + w = new IRCWindow(winId, _client); + _tabs.addTab(w, toUTF32(winId)); + } + return w; + } + + void onIRCConnect(IRCClient client) { + IRCWindow w = getOrCreateWindowFor(client.hostPort); + w.addLine("connected to " ~ client.hostPort); + client.sendMessage("USER username 0 * :Real name"); + client.nick("dlangui_irc"); + client.join("#clienttest"); + statusLine.setStatusText(toUTF32("Connected to " ~ client.hostPort)); + } + + void onIRCDisconnect(IRCClient client) { + IRCWindow w = getOrCreateWindowFor(client.hostPort); + w.addLine("disconnected from " ~ client.hostPort); + statusLine.setStatusText(toUTF32("Disconnected")); + } + + void onIRCPing(IRCClient client, string message) { + IRCWindow w = getOrCreateWindowFor(client.hostPort); + w.addLine("PING " ~ message); + client.pong(message); + } + + void onIRCPrivmsg(IRCClient client, IRCAddress source, string target, string message) { + string wid = target.startsWith("#") ? target : client.hostPort; + if (target == client.nick) + wid = source.nick; + else if (source.nick == client.nick) + wid = target; + IRCWindow w = getOrCreateWindowFor(wid); + w.addLine("<" ~ (!source.nick.empty ? source.nick : source.full) ~ "> " ~ message); + } + + void onIRCNotice(IRCClient client, IRCAddress source, string target, string message) { + IRCWindow w = getOrCreateWindowFor(target.startsWith("#") ? target : client.hostPort); + w.addLine("-" ~ source.full ~ "- " ~ message); + } + + void onIRCMessage(IRCClient client, IRCMessage message) { + IRCWindow w = getOrCreateWindowFor(client.hostPort); + if (message.commandId < 1000) { + // custom server messages + w.addLine(message.message); + return; + } + if (message.commandId == IRCCommand.JOIN || message.commandId == IRCCommand.PART) { + if (message.sourceAddress && !message.sourceAddress.nick.empty && message.target.startsWith("#")) { + w = getOrCreateWindowFor(message.target); + if (message.commandId == IRCCommand.JOIN) { + w.addLine("* " ~ message.sourceAddress.longName ~ " has joined " ~ message.target); + } else { + w.addLine("* " ~ message.sourceAddress.longName ~ " has left " ~ message.target ~ (message.message.empty ? "" : ("(Reason: " ~ message.message ~ ")"))); + } + } + return; + } + w.addLine(message.msg); + } +} + +enum IRCWindowKind { + Server, + Channel, + Private +} + +class IRCWindow : VerticalLayout, EditorActionHandler { + LogWidget _editBox; + StringListWidget _listBox; + EditLine _editLine; + IRCClient _client; + IRCWindowKind _kind; + this(string ID, IRCClient client) { + super(ID); + _client = client; + layoutWidth = FILL_PARENT; + layoutHeight = FILL_PARENT; + HorizontalLayout hlayout = new HorizontalLayout(); + hlayout.layoutWidth = FILL_PARENT; + hlayout.layoutHeight = FILL_PARENT; + _editBox = new LogWidget(); + _editBox.layoutWidth = FILL_PARENT; + _editBox.layoutHeight = FILL_PARENT; + hlayout.addChild(_editBox); + if (ID.startsWith("#")) { + _listBox = new StringListWidget(); + _listBox.layoutHeight = FILL_PARENT; + _listBox.layoutWidth = WRAP_CONTENT; + _listBox.minWidth = 100; + _listBox.maxWidth = 200; + _listBox.orientation = Orientation.Vertical; + _listBox.items = ["Nick1"d, "Nick2"d]; + hlayout.addChild(new ResizerWidget(null, Orientation.Horizontal)); + hlayout.addChild(_listBox); + _kind = IRCWindowKind.Channel; + } else { + if (id.indexOf(':') >= 0) + _kind = IRCWindowKind.Server; + else + _kind = IRCWindowKind.Private; + } + addChild(hlayout); + _editLine = new EditLine(); + addChild(_editLine); + _editLine.editorAction = this; + } + void addLine(string s) { + _editBox.appendText(toUTF32(s ~ "\n")); + if (visible) + window.update(); + } + bool onEditorAction(const Action action) { + if (!_editLine.text.empty) { + string s = toUTF8(_editLine.text); + _editLine.text = ""d; + if (s.startsWith("/")) { + Log.d("Custom command: " ~ s); + // command + string cmd = parseDelimitedParameter(s); + + if (cmd == "/quit") { + _client.quit(param); + return; + } + + string param = parseDelimitedParameter(s); + if (cmd == "/nick" && !param.empty) { + _client.nick(param); + } else if (cmd == "/join" && param.startsWith("#")) { + _client.join(param); + } else if (cmd == "/part" && param.startsWith("#")) { + _client.part(param, s); + } else if (cmd == "/msg" && !param.empty && !s.empty) { + _client.privMsg(param, s); + } else { + Log.d("Unknown command: " ~ cmd); + } + } else { + // message + if (_kind != IRCWindowKind.Server) { + _client.privMsg(id, s); + } + } + } + return true; + } +} + +/// entry point for dlangui based application +extern (C) int UIAppMain(string[] args) { + + // embed non-standard resources listed in views/resources.list into executable + embeddedResourceList.addResources(embedResourcesFromList!("resources.list")()); + + /// set font gamma (1.0 is neutral, < 1.0 makes glyphs lighter, >1.0 makes glyphs bolder) + FontManager.fontGamma = 0.8; + FontManager.hintingMode = HintingMode.Normal; + + // create window + Window window = Platform.instance.createWindow("DlangUI IRC Client"d, null, WindowFlag.Resizable, 700, 470); + + // create some widget to show in window + window.windowIcon = drawableCache.getImage("dlangui-logo1"); + + + // create some widget to show in window + window.mainWidget = new IRCFrame(); + + // show window + window.show(); + + // run message loop + return Platform.instance.enterMessageLoop(); +} diff --git a/examples/ircclient/src/win_app.def b/examples/ircclient/src/win_app.def new file mode 100644 index 00000000..40b796c7 --- /dev/null +++ b/examples/ircclient/src/win_app.def @@ -0,0 +1,2 @@ +EXETYPE NT +SUBSYSTEM WINDOWS diff --git a/examples/ircclient/views/res/hdpi/document-close.png b/examples/ircclient/views/res/hdpi/document-close.png new file mode 100644 index 0000000000000000000000000000000000000000..0bcfe981630a13e513e3053404df96c831caa025 GIT binary patch literal 1504 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 literal 0 HcmV?d00001 diff --git a/examples/ircclient/views/res/hdpi/document-save-as.png b/examples/ircclient/views/res/hdpi/document-save-as.png new file mode 100644 index 0000000000000000000000000000000000000000..9695a56424eda300496caf56addc00657abfbc75 GIT binary patch literal 2152 zcmV-u2$%PXP)40bdUMYLM+G3ZQN z8LL)XFtx&fw8ct8%YYeeJ3z}IZAWEl2`x>3bQ~(SpKmZbDK&86oGA%j)ch5Hv z^n0=YH5Jcx^kFTnxiua~LRIm>4>sYJl`CN}0))l)oQ9Pv12fXh!1X7~0%oQ_Pex&! z`WgyCF1&S|;kiQwB9RE1Mn`e*-b%#Z9KvKG0fPjXHcc>VdI?O+oST5fh&L@f(>ppn zQn4Fxn32fiE5ZBx|faI-}h}E^AtA7k7VIKj`C;?5(O~3*S zda=AX$jj~6VB31k59{a~G_YZhfP|K^!Pnkbg8Kp{;KK87!K|XqO2f> zh;k>+qk*yzpt=WVjDsU`WJ=u+TgJZ+I~dz1D#fJ^8fRFk-O? znzshAxHJMytwTp*9sB_aq9nuZ^OEQ?ea|ul=Ng!{zjZkpXm5K9H-<+MTC^1H?f=5h z_J0Un&-QzRgfG@kAYm2b*KgE=$;aUFcHg7Jmn_B!j6I-^^(@ii`m5Imh{YR+Y#O zpH9FVDp?;aT_S{QX^ORg+n4J{@V)y4Bn=h2U#TZhFTm`eY|%|6lN^+%mXBPt9AH`& zII2UG6Qji*TI=)sF+m!Uv+0+rm)(247U@Anv4O3RU4vH-x8f&jO-PjLH9K4gmHAQR zQvh&qFPMEu8JL=yasX&_GC`y5I+2nkDKke7{;mK{-aaAaO!UQHfv72oXfL$rIn2)= z#TvR;3`YjAy@jEBz=a2Iu0T~)75;wY2!Zivbq?wXi=uZ*kOj|>)G0f*2s3vAfks)B z5Q|0;2nI6*PMnSHzRuGHuQYl7#SJK5O}BkgT*f&K8D90n9m9VzJLGbrSV0h zkx2e0`qfGIo?q+%nIkm12j&u?^K9rkZ$pQGB#QR@;NYO$J@V#tUw!n#(6CTj?(TfM z_2NS>9zHpq7Yu&8thDrLNtC=G(+%AQl4W^XnrXCCpH2U?Lx*op#_R0NIl{aYm@aFR zljrKI0>yuL<#+^un>;TXi=9lSQY|h;Er9@5Rb4H%zW3e&HDih-%Zef^sw9aD^}i&D z{0IVrAn?NLmc{gR+At!~s3xmQ&(MXT6tksw>0(doOd@nlZvNA|tpKhdUr4TGTQfr9 z`ZKu%!0hhsHv9YgQxug+3oX8wh*%Vj#pfu+CS}2*uO9Pyy%uH4;=d0M52q<}QuhBr e0ggFDQvU;t#p**?D{M;u0000M8$}SF`)$W|Vz+h^3K1d&siN<^An_F5_&0!O z)Rzj0@&v6^_10 zg~*Nh4_Qv0^%;Z#{w#!k|1X#ZQ;;@Dp6+HDfFwzvR;z(y+w7a5d4<3CPl}Rf7-tVSvZMG^9)A0BAQf8V&LJ zVG9{>Z3|Y_U^M`ViOHx#!ZB013K_`6*9ZV_uqB#YC@zDZ2d0D?f6(P|tQ3#a@s(2L z01}P3@xh-TA45^onUU7wF%q&o*g97OmE=lL4uFOcKDoWQ{=CRE|L5o=qZ!D84eqm0 z(TY!}fpk-G0x@+f4Xgl&tX`6`UHUODLTUZqH4rBXBHE6TT3rHJy|_6BfHj~JeTEX? z(^0ITcRZGj$IvMk;Sv)L>kUjP&dLt6R- zyYa=Bbgatf8{D{glU698=svQ`7Y~CniEsL{qWLobmqjQA_hgE!!xHWaYvFXB9A-fFUE*Y87hrIt&Mc0u2-(v~0T& zKomz11c4+VD@>5(9wI1v4m741bguyr;G&{9J{<$#x*U9p55;jL|g$=_H zfzf=j5U4rdp6zxU`u#o_*fcf*z=O+|E`3m`*1o|~wbT1m=)U#UEdfQHwk8G7iBS9~ z3|T7zz%osq?{>Q^UTjrN8wdl&ql1G#H#RoD-hSocXL!RJi!cf~kv-Q!YQ;po(4Rhx zD7a`UT{6*-j6*c%v3CUk-TC)uH2Mye-|{@~A`$TRl`GZ{4<78euIssuZ~ literal 0 HcmV?d00001 diff --git a/examples/ircclient/views/res/hdpi/edit-clear.png b/examples/ircclient/views/res/hdpi/edit-clear.png new file mode 100644 index 0000000000000000000000000000000000000000..631ed4457342d9e6f224012c842463ef09bfb8c7 GIT binary patch literal 2066 zcmV+t2<`WYP)x*e)JWac-SH#wQ-nR8CQ z??V9a&birHyP>V2D6Oi1xANEi_WvUA?O=O#N1Yt{FO}b!bpZ~OI%n`&{aCwN2zNS9 z<;}Xl^-CIRSECYcHRXZsglPS&3*5WjSf$N705{Jm`}F5o6%ZbYHS29I`~HkF7JAMW z9h!B4-ixPvn@;edOOyWUnB?HHSr-`S)M$I}4mtW`5AwSp2HFJOx?;+$L~N8m^(Bff|Ht{ui9&=7Wp`+_rHI)*!@x7XeD= zg~}9-Dj8ZUgp)(O_~DmAA;#H!Tty@*5nA)#^~VwVxSGK4al`P7qet=I3Kh7;_ETI< zB;HCO%6|(-!i#t%PmY*8D?9w+No*|EXCf&P-%sP>910ptM#xTIn~somDMGgf#@e37#LwL%m)+;BfE*HhU`HL`UPBN&7IH zg6{4`C^QS9zW%5{{3vSQbj`!CP&}f_#V1V+AD%?We#Y=5wo7uolKVr0G9HxaQBab3 z0OanQ{(ghzgunFh!TnU~RBp2vxHE={1r&sDgi`Tf5OexFeeU!2;}Zm~LTKlVM-b22 zw!bbv?p3!o4Vp_6K`ROdb#??Kc-o&XHZ-``>-`zNzI{9Hb#}%*B-cGX@YmcB+~@77 z|7Z@}RYn4;mp0bn;M;a;fJP2ssoG ziJo0{xZCbexXae|O~}~}-w6uBePm4i@xfS0;Q7<=83>RUMf%0={<8(KaJf7QE|eya z90W}U6Lcav$Pf9p>FZ=fB3IuDuTA)>g9E<0Wee_dalu_X-EhOaxwy@71{#^bp8BM12vn=r`+xhI4vboUDhwTr_v+mZ|YjpPBLS4Rh1s zc9(U-H_ge}aL!P_0R<138sp2Kufyp?BArP5qJSW5o1#oW1O2V`d*M|Y<+xh z;oR@;n;H&1FfhR1n3>=!uA6Zxkq{7xpA|?B+7ezZ^m~>OxdU2?BS9@q|v_3JGcuf()^)G z9t@gvI@F1opi1_$fOX49 zMb2w@HL~D8W;m=4-SOUnp$?kI@DHX&L(ho7GlcNOMq8|Q-GQ?#EErSpcEMId`2ja4 z1=qFzQV|C-BAuZvKO8RRv!VWIAk<{hq3Y-!(5BI#DK`kTS%FXxEOyFAZ|kN;vCvQu;@{m4DddIe+e>emV{RWNwbfY^-qQI%}MY zP$?OM5mLv}%ue!sw|hESb)U+hy_6i-0hi<)&`BAS4e5bX5@mcJXe$f>ozM@|nR}qK zBpeD7sIA2Tn|mr4&QCjH111t3t)Jw1+l%TXtk3ImLN+vrLzdUcg669QG-I))B{gB0 z<->lK{~LIj9zL}LIuUzGd758v5@%~~ZI(Zr6EonPoIP1DrcX9zFyMR+lg!-@+7wJs z6M^p1Fz7tKcQQAASBX|c7pQ#fJ7tbmqXoX7iECxS+Zv=S>sm3xLM^44snxVO`imI| z1V%Yam+n4dZ2Ywzo{Nz2o6%2ophgzo%gv^!w-4V@Z#_QvJ+GxFmd8k&x zVrryx$1|Bs%l7=p-?j?`hI;T@Jrk;Fv^i7uQqIB}8U6hRF=M$}#I(zfbx%%WZ&OwA zcRxKL@P1hq?=>QfcJHf~F`XL}jO7)%f%D!1Je{At0f9cGD^0o4CKZCfdHOw3C3tK~ w4tEYMH2CfEFY~e(IA|La%m4rY07*qoM6N<$f+Lgg_5c6? literal 0 HcmV?d00001 diff --git a/examples/ircclient/views/res/hdpi/edit-copy.png b/examples/ircclient/views/res/hdpi/edit-copy.png new file mode 100644 index 0000000000000000000000000000000000000000..477e83a6abd5d7b2f83f57f76406086e4bbcc2a0 GIT binary patch literal 828 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabRA=0U^EKw332`Z|36R!D{yvo1&aFy z`o||FC8woE#l@uOWM$=L@7S~V_T77j4j(>$;X?0(-o6PFCQX|%bN1|+b7n1AxNzyR zWh+;%T(uem)~;Q(_7Cuwm1tP209@+qrY+u6_G=@7{gz;K4(O4jn#n_{515 zr%#_gd-m+PbLW5tUb}Yf#*G`df#B}FJ9lnBeE9IuqeqV)KYsG$$sPPA;0+kO1%r2B@c#X~4lYCH`t=(G{`~p#_wV0-|Ne2@JbMZlE}|tte!&ckOw6j*uHHTsjgw|9Tet1V z_h0}1U7Yx(6BxWpJY5_^DsJ^oz8Ut&LBut1qww>}cVW!uKHHS6zWbnqt&zu7$Y9#a z5C8Wk+}$M_e5$!NuHwe*UDX@KX1GYvNSpT4)PTT3s1?vA6*xS|6c7rxp0!vz57R5-%j4J*YL1i5Sx{;!iF#EU2gTe~ HDWM4fC}qXE literal 0 HcmV?d00001 diff --git a/examples/ircclient/views/res/hdpi/edit-cut.png b/examples/ircclient/views/res/hdpi/edit-cut.png new file mode 100644 index 0000000000000000000000000000000000000000..073232808a62fba614f56f1daeac9e4addc97ccd GIT binary patch literal 892 zcmV-?1B3jDP)kdg00002b3#c}2nbc| zMg#x=010qNS#tmY19kua19ky@)q>0b000?uMObuGZ)S9NVRB^vcXxL#X>MzCV_|S* zE^l&Yo9;Xs0008}Nklpqd-%TR z|2P*vK`+{{^o5}LLo7{%!9>5_s(X6h6!4&|@wG|7Gj({ZFmArnqtBeLl6m=@OEcNdT02My0)1X$-i2NUA8lO)N72t?3bsjBer@&O??6`$nZpH-O9m zi>d@lw6_?3>DfIM8k)(`ECAxBjVa}-nylHg^2CV=nJ&2iWC1{&#t#Y%3^+N=Ol&$k zmH5&}t1SXR8D(Oq@$%7hZ>Ul+5|rGf1r`G2{YfZ;L{gG4)v~>Yh7|ZQ?i3HQ6rlap zlTe|%n{0{I`c@4w$_2F8VgN*eAB9qAv<(+-&%2sm~ONpS6-?k>+_aRT8l%mV()gRX@>3M$+ogXIGs+SLs_r&rYOhGu<^&tH?Z}VX` zW1Za1j(aYFNs<}>#EO=M4o|h9VIEvYjr}?}pJNfpc(FnL?I{Qh*8{xysR(ES`5lbW z`Rdp?pt)gtx+{3_eFzFF%7EWZJ%F2^iR8a=kNIjIoLWG}<5U&Q?9)ZJMfc`{`$`XR zvsh-A*A1~JTvZ3au^ZsZQQjDArrz|KuMFVjX1p SYgWbp0000~INfC^Ha& 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 M07*qoM6N<$f>H#b?EnA( literal 0 HcmV?d00001 diff --git a/examples/ircclient/views/res/hdpi/edit-redo.png b/examples/ircclient/views/res/hdpi/edit-redo.png new file mode 100644 index 0000000000000000000000000000000000000000..d759f1367b60716926c7904c4cfdc8fe88cbb8ff GIT binary patch literal 1998 zcmV;<2Qm1GP)6jHCf+tx068ATxinL6ePm^ zBGd?#2YBJ4z7W*H0}?8g3W64qiU$zT2UI9ji9{bjf*@6ZLKP6H+a@7b665+3XT9sS zcmMtGGsFDr)KL=Kgc7M|b#`X8J9Ex=zFFxXl2YRT0f9*3?Qe3&1`Zx2t+HnQ139bp z;U24hZz0vcc5SM^t;NXGd{|nT4W?$U1(W|M1rxv4r1$bq?mZdZ6$3k8Eu?MV{EA7& zW7}JHwhd;7&~E3EHf-3M362Pru!2%lg5&$}0~ce?sd)(T!?-Da=E(3F2e;txrAK=< zWZ&KW(e5vvT-Cs?<9(m8p#5M^`-5w?=Z6tc6_E_F8gw!MZsOG3C>Gp_A`!m!i=Cs- z<2t@Hy60P)TJ}AWhFvwCPF0}PL_E6z{5afbO{=c3L%tZ^hR{a+w z51#eM);*qGH}=hp(f!0o`+azQ=C}CKJts(C?oe%dvGb*kJzG9L(lvr|RK(kb1_SGw zcVfXW9V!i7_>ir4Z)$5ALMfbGGI?798-Ct)c=OtAhc*wahsRy423t#kONm$nkhrlA z1A&$*A~b1D(4-E*1Zq(g89j@++8bE2<8L=7Fz~bdXM37@4iyRo%m(G#Db)VQNW4OpZjt*5XpyAhps2M9|ZAg{|OdKn{&Ft~#1 z<-QvU#O2yk!vkBpN;D72Z*B_*!-?`MaFkF7Z-G1;p(9YE7do0{2)otF)tUgK&w?*K zTyid<5>zmd?uVvnH;HoySUiG|z(~g|7!>FrX{KX-!RWE!L>}sqzr--+c%}Szgfc*O z!b0w^hTrO0&uB)p?c8^Z*)va0H4Cl}rYq)?nSYvYZ`_3pnegH&ru=clanxX>7;?op zMxaKJ#B{c{*n-ZdG9T=3>HEFhZr<8+IT1|DtHF%e67=%uqI=G^%>$*0oUXw>~2fWaZdtB$P@Z9jFfoP zr*{VOS~#ET#{N^EpI*QU&@$rD_SSq?#6qN`4w4!)0)&W_xInDPp(sLxo(0DgOrcHL(0`=>r#T2cIgV*_9B=xckNkpvSOveXX7%tTa? zgDXl2_|gU92$?8Gwpd>@V3TDza7v!=!$eTgBarJ#&uh{xFZLskY@EOf_}rO?dWX6T z-!o!>C6$xbh&z@5CqlrL2@#^s9faU$(z1v=S*&}z77b(!3v+O010Do#84yzVie9(X z;ycrR^~#xY1s}n(NHcUgby=OLmetM#ib)`XBo3%|QX?rKM-~_(fW;H4?FWxMd;~fg z5OZGavKk8l0{KLw#JQiCVI%_~;b0gm;>W{(dKEzPO)dFVKaM{#j{k;+foc$rD|flF ztSu%Wk&KP`)nFmmobL`c>RJxK0`36sd7%elf$6)S?Uh1Gaq_E^a#aIP=)T!YYr4ne z(gAFY37seia?N(=RO@{JPU8;1Jz|6F+q5gG5;r5A?fb)^G$T!#v5sS(IxlZCKqk(m zeBFYT2q(N@L?;$HmKSEM)WAJ|?!5Tg{$hYtu=khikrOHFO?^&Wc1ws=29e;>|5&_i z3JeJ}IjYsP3V?+3*I8BKRg<*Zg<7#z=xnL$!rZd^FKe__ds>HH*wTVYG0!a0Nv((f z;aINvNJbzGeY^b;dzX&0(M$v&)Lrhp1WE_$a>X+s)vGDCUVpE%VeqUq6B7^{Op6m? z#n@EyP{hgHOQp?dA@ovPJ9>>z{&6d3A@PxxHR5zIDVsJKV$>g(7Sn~M3^2(-qLK58 zSo+FEc>cnLYn9#hfTN;s&>&VJ`>+kHTIP)DVzoW0OcV=xZo{bi`s)Spl$P%g~NRQNpzA_f+ zq;Z`{5RoL1OmJ-P#bsk&_i`KUXzj<*9iwu^1QIyk2sjZcllU2aZKxjLAqQ-exW z3k=3WRh2T)FeNNz$P_b#&GZ$;6I_0OS$dQHo!<+xhmF<)_ii<&SVfe>dLkeaL3=TY zNF^bXh*c6&BZe1uy>a(%Mw#bQ*1in`X1+Bg&R1rVgC%Wd@P--EMDp2S8eZ7>+R_Gh z{c6^F)Mjn187&0I2MU|yzbkXnBqo!{f3xSzD#Ca1Zb(N7`CN+H?F{yG_Tl+0Czlr7 gH3RP?7~Wz02Ri`Y39;LJga7~l07*qoM6N<$f&e|OB>(^b literal 0 HcmV?d00001 diff --git a/examples/ircclient/views/res/hdpi/edit-undo.png b/examples/ircclient/views/res/hdpi/edit-undo.png new file mode 100644 index 0000000000000000000000000000000000000000..c893a1a65e2d8bac79d3ed7313780605ff0b6d74 GIT binary patch literal 2020 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 T;=!W_00000NkvXXu0mjfkMScx literal 0 HcmV?d00001 diff --git a/examples/ircclient/views/res/mdpi/debug-run.png b/examples/ircclient/views/res/mdpi/debug-run.png new file mode 100644 index 0000000000000000000000000000000000000000..eee3e90552fdba432ad7314fae36137beb627473 GIT binary patch literal 383 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Y)RhkE)4w)f((Lt7nXJcMK}vQ zB8wRqxP?KOkzv*x37{Z*iKnkC`+Y`HK26z(qapPS42(RUE{-75)v%x-)b_i zJP_bLs}Te^XR8P+D_>+wl(?`(H~IMO`tQR3jQ^;gZ(x5fx9j>^y?OQT&Yla~!~@is z@Z;BO>BCoKg_Wnrz1`V-+WY_i|HpF~L^R&n?hjv;8GX#WCN4?ofzvsl)}m``_0HLU zPkUZf#M4lDGT~KUC!_cJZ*LsF3kho${{Q;jT(rbcEXky3bD^Qj4YzHP-fbbq1_lOy ze*d4J!F*4fh6@|#$R!6voIQM^JxQCHnb|qk*EElL zf#12!(|Bg~yi1XoVNs}5#w^nEBwWSvsm_FF7w&ob{4hv5-RslS6OpTGWiE87hliof XB6*U>sxded32#5rSkY z2Y&xvG7_=Vk%+V5_ixV;U10XVAZQpSG);$IDx+LBV3;N-HT!;On7;i8dAXfm9S-@R;%G}K9Bg!3^q^rU?{uT zTaH3~5k)P#iw~!LctZ3;3IybV9mheV(Lic_9dE@pwvPMo`5a;A3LzQt;fcT@`aMXL{PwOYNnu8UkQhjcpK zOQBF;Xv*^+*aEa|8})jf0V!y=+vs#U94YH|yI{s6a0Do;_rVNsc#WTnVqL*ZdIX}f zr+Pok-DNTv{K;nVaAhUV+#tcg>b#~XinzLnEiQgxW{_ZL`<7+lm#99su;76`f?*$y rhhbK__a8mp=!ZGp<2*d4B-EFG{*ujozX)tM00000NkvXXu0mjf0Gko@ literal 0 HcmV?d00001 diff --git a/examples/ircclient/views/res/mdpi/document-new.png b/examples/ircclient/views/res/mdpi/document-new.png new file mode 100644 index 0000000000000000000000000000000000000000..a956a809eef79b53266de1afc493c49493ab4b01 GIT binary patch literal 516 zcmV+f0{i`mP)S2M4FYj6tW0irAsvdyYYY6tyKSjP+SyCt#sFg%t}N-kRp_d7I9HfFr=Gc zR^2HEUD%m?%;aMxnItpEdxp8gWNOm`hj0nc^WOJ{h{`q9)l2(Gc8mMPziZ#t{FQer zzvY!Y1R%_+Wmz~gO<2t)TJ1L4oeny-4cl?xbi3HA@1gvqjE&O`Fpt$|>1!#Fm+~MI zoSE|lg|`LdKjm@vs0ilXaS^wU^N<(vBN8-QEwF?&*i~S!*F(SG$C9yx|3#jQ7YR7A z9m9?W2LrgS3(xaVI4WTAwcxfYbN{{I=Al6L;~Nl6U^Q`|V1mQp5Tnru#!mwa2Mf6I zM!=MnVDUf#`5^0&fM5d0QwjRMkMVdM)%ay#xw4GhVGi<9{>W!pPksennbngCYj84| zM4e8jNy!jS1l*TR!&xv00^Re1LHuCcIXyWIhGD3CUZD5p&RJMPJo7u-c0$FJW(02h z;upbgmnOM`fmqJd&s z+ahABm4dwp0gc!+W`m8i=EwYGck{EGY+h#;7u*c{!OI7;JkQH8ECE31^wYR((?2vy zjtP&*@dpud^zH=NJA8yOj5P%=*i2$ehTFRYUo?s6r=+pnzBeGFmoIRF-?<#}g(Cix zN+?$<*e;Z?=iE~)CW>gN8YZWvP_9atc(I0^XC@Wm3GcQD3dMhLxr|D+3g5FB4&3g!K?W>tgNmgyPe12SPbNp5ViSlie{XznY>wTMQB4$B+dE`YdBOjB-$MWEE{Fkwy8&~CSl{WkU<9{)5B-3sIEjSylh9~tE4 zo{_=LX47ak1)WX@x~`ks2ph(50tVA|4(@ilEbNSaS5c7^oiZbwfWl@w2iNKKL{Y@% zuPprIfkk$IUSQMC&Qhrq-74+{gK-n)1SZ@oU{9OQi0}sj7MK&*O}~TIX7QTU-Bp_I?#hm zV{_(grov1Ksu7uPW}!~a+)B|l&>BwnAzs}+?@Q)rH~hHn=l;0fkB77B{VlT^WL2MD=uEXjq;WW zXiaWps?FdAM{_keT_{QhYipoo&Kl)DpAU_VjlB13c^U4RX`IL|#kGfa#OGE@21l~4 zgG?d~_?!6semtQ!*cR&{WE50DkyR!c9LOlazO+)_qo?x45E+ESA&~O_V6|G&)7=eo zM+aV5ELidV#14g0GT5J5jGe~||NMh+Xmc@yS@%3fK0D#~G>+ksF$~&A@$S6?uWcXT z`Zf<1mmnGJPPzoPHyw+M9ymtF@R1gJ``Zht>Z!0&`XxB zeoTijjKdxnhJL_6+UD3C@}Tb76lx3;s3!CT&t*ij8|Mqk!E!20s5O0owsjm;GCCQ- z;6$}?iyA_Z5j*KBi|8g)nszjFy2xajQ!lGX)Dd?g>ZDYg!riVpD2udUNmrYYqVB<& zo0e^)L}(;wnm!yncLgk8Z>H~=<*s!^y`=mcS*!i4*I@aMoMHh?dCSlzw9U}BJE42( zgqF0tb&_b8lYI6!CNY+Z>#g0)+jEP)Xi%%w{vE(0iX^`YPa+cR!LH7Be46XU!y6}9S{Ds;T9$y4s#5bLK0NM6 zw`V&Rp7bF&JqDU=qsZ{FHQ?RTYxp=jj4qE83-i4Q+%)7Cv0kq;)(OUZTVK2n4WM&J zJ3fc|5tu&7pQ|x8%ZWq+u~-bzXcX~yyvDz?AzwJ%@!;*tKxOZqo%lj>@YX4d%&jc) zoRA$-%Ize<%U1WB^mc_4cALWP8MRf6dU!Ire|GRkJX9Dm~v}{o-2CR!8 z_ZbWq2lMijf8M1dGb3Xtb#W?}I|b#_2e_yE)WqhgG!x>2ER$k_E;^MoLz!Wc0^RzIjLSiumsSo5vWwl|YgZ&2&W| z-6w;&-duo5>`yWz$pT-oyr`HDpGO$_&6B=%yuFEJT4M=O26LfSL3fjps@fyithVuj zh4U9skniC2`(bLEIyu^Sf{~|NS>@29QmLrh9>ld%YSQc_9BXjnu;(LYWWoG-0`_Hj z48DBJodgEcNwCh#6UsFlR0~8EM>awQ+ubFj}n^>w-XDv;>eM zC{Tr|#Hky_zt0#$%*cW{aq(O{5<^%0G-76IFzS`OY7JylNjw&dh5Gt>1_lO*(MF?< zj3UiyL(Is6P0nn(8e*9p8-*}e&BL?)tji5Rr_M^ literal 0 HcmV?d00001 diff --git a/examples/ircclient/views/res/mdpi/document-save.png b/examples/ircclient/views/res/mdpi/document-save.png new file mode 100644 index 0000000000000000000000000000000000000000..3f7fd63cb9d6dd2d024290892c6f6077cd75bc66 GIT binary patch literal 559 zcmV+~0?_@5P)aB)o!U!^iJPX2-yDW~-xn5#GXPG7Kd$?Hm6gjYc)yd@*-0`VkWB4D zPVXW`(u8GM=XhY*yM%z>k7`53k-*<;O%?T47tKx&ZKVgLtKr47=U|=V0q-Rbo<4p8 zMb+>snbmpC$XUFRbBLvK(6qliOC2%bUUcKpgNNw$G<1|M6hbH0?x<+CRWw=(YK;zR zbh0dU#DMFf3-|p29M;>Y^f_c|EtIP*{61>pXQhEsxz4lH5d#Zz^Vqy|7vFwW5q_7_ z*?KF3+m`WiJBj`8WuB#u7?^X;;`Ys3`1JJ$a$oxFmyl)lO8B^UfXwFuWC_bsM+`U} z4s5L7I1%cIf$7O9tX=b+2zA83G8q($MHCAK6bOCh^7%Y$W(&+?MxLXN z7#O$NvF!13@pv4bMK}M6iA16wzdTlp1*>!usUrr=MiX4~3tTiB#mv++tR^#Jv6#*h zT^^PVY~Ew17%+~GmM83XIH?%k3S-=A!-zDDNF<^&>zLu+Z>+6jYD0000-?)JUISV`@i-D@I zgD|6$#_S59pk#?_L`iUdT1k0gQ7S`udAVL@UUqSEVnM22eo^}DcQ#T$MejXb978H@ z<@Oq8F$Ida_AlUg=iqzbmh20?rl|@>J}bFBvZddo?kbdj=ip)NEYWoIwxfrjspE&I zf7kzXeO5gCv;OnD{r8;bw;$;L!puHH-#BVsl-uWB6J%O8F5eg^B)IOC*`2?k$%mE* zZGZSqJ26OnS7zPD-qf;-Cd;FRd6qBT6YZ6_>Ilp2AJ^pTp`U_QHfz-`{!$52l`)9UY%*8-9&1?@*h#)SHUM zk6ZgL_O)%DJ$u9RPC53rhxZrmt-AK^znEXmL=D9W5yeH^`@hV2(!JrW#zdgk89ZJ6 KT-G@yGywp$wU*=n literal 0 HcmV?d00001 diff --git a/examples/ircclient/views/res/mdpi/edit-indent.png b/examples/ircclient/views/res/mdpi/edit-indent.png new file mode 100644 index 0000000000000000000000000000000000000000..ffd0730d8fb289098ee222778cf7d986a89cb79b GIT binary patch literal 413 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 z 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 literal 0 HcmV?d00001 diff --git a/examples/ircclient/views/resources.list b/examples/ircclient/views/resources.list new file mode 100644 index 00000000..d8a1b540 --- /dev/null +++ b/examples/ircclient/views/resources.list @@ -0,0 +1,20 @@ +res/i18n/en.ini +res/i18n/ru.ini +res/mdpi/cr3_logo.png +res/mdpi/debug-run.png +res/mdpi/document-close.png +res/mdpi/document-new.png +res/mdpi/document-open-recent.png +res/mdpi/document-open.png +res/mdpi/document-properties.png +res/mdpi/document-save-as.png +res/mdpi/document-save.png +res/mdpi/edit-copy.png +res/mdpi/edit-cut.png +res/mdpi/edit-indent.png +res/mdpi/edit-paste.png +res/mdpi/edit-redo.png +res/mdpi/edit-undo.png +res/mdpi/edit-unindent.png +res/mdpi/text-dml.png +res/mdpi/tx_fabric.jpg diff --git a/src/dlangui/core/asyncsocket.d b/src/dlangui/core/asyncsocket.d new file mode 100644 index 00000000..953b0775 --- /dev/null +++ b/src/dlangui/core/asyncsocket.d @@ -0,0 +1,195 @@ +module dlangui.core.asyncsocket; + +import std.socket; +import core.thread; +import dlangui.core.queue; +import dlangui.core.logger; + +/// Socket state +enum SocketState { + Disconnected, + Connecting, + Connected +} + +/// Asynchronous socket interface +interface AsyncSocket { + @property SocketState state(); + void connect(string host, ushort port); + void disconnect(); + void send(ubyte[] data); +} + +/// Socket error code +enum SocketError { + ConnectError, + WriteError, + NotConnected, + AlreadyConnected, +} + +/// Callback interface for using by AsyncSocket implementations +interface AsyncSocketCallback { + void onDataReceived(AsyncSocket socket, ubyte[] data); + void onConnect(AsyncSocket socket); + void onDisconnect(AsyncSocket socket); + void onError(AsyncSocket socket, SocketError error, string msg); +} + +/// proxy for AsyncConnectionHandler - to call in GUI thread +class AsyncSocketCallbackProxy : AsyncSocketCallback { +private: + AsyncSocketCallback _handler; + void delegate(void delegate() runnable) _executor; +public: + this(AsyncSocketCallback handler, void delegate(void delegate() runnable) executor) { + _executor = executor; + _handler = handler; + } + void onDataReceived(AsyncSocket socket, ubyte[] data) { + _executor(delegate() { + _handler.onDataReceived(socket, data); + }); + } + void onConnect(AsyncSocket socket) { + _executor(delegate() { + _handler.onConnect(socket); + }); + } + void onDisconnect(AsyncSocket socket) { + _executor(delegate() { + _handler.onDisconnect(socket); + }); + } + void onError(AsyncSocket socket, SocketError error, string msg) { + _executor(delegate() { + _handler.onError(socket, error, msg); + }); + } +} + +/// Asynchrous socket which uses separate thread for operation +class AsyncClientConnection : Thread, AsyncSocket { +protected: + Socket _sock; + SocketSet _readSet; + SocketSet _writeSet; + SocketSet _errorSet; + RunnableQueue _queue; + AsyncSocketCallback _callback; + SocketState _state = SocketState.Disconnected; + void threadProc() { + ubyte[] readBuf = new ubyte[65536]; + Log.d("entering ClientConnection thread proc"); + for(;;) { + if (_queue.closed) + break; + Runnable task; + if (_queue.get(task, _sock ? 10 : 1000)) { + if (_queue.closed) + break; + task(); + } + if (_sock) { + _readSet.reset(); + _writeSet.reset(); + _errorSet.reset(); + _readSet.add(_sock); + _writeSet.add(_sock); + _errorSet.add(_sock); + if (Socket.select(_readSet, _writeSet, _errorSet, dur!"msecs"(10)) > 0) { + if (_writeSet.isSet(_sock)) { + if (_state == SocketState.Connecting) { + _state = SocketState.Connected; + _callback.onConnect(this); + } + } + if (_readSet.isSet(_sock)) { + long bytesRead = _sock.receive(readBuf); + if (bytesRead > 0) { + _callback.onDataReceived(this, readBuf[0 .. cast(int)bytesRead].dup); + } + } + if (_errorSet.isSet(_sock)) { + doDisconnect(); + } + } + } + } + doDisconnect(); + Log.d("exiting ClientConnection thread proc"); + } + void doDisconnect() { + if (_sock) { + _sock.shutdown(SocketShutdown.BOTH); + _sock.close(); + destroy(_sock); + _sock = null; + if (_state != SocketState.Disconnected) { + _state = SocketState.Disconnected; + _callback.onDisconnect(this); + } + } + } +public: + this(AsyncSocketCallback cb) { + super(&threadProc); + _callback = cb; + _queue = new RunnableQueue(); + start(); + } + ~this() { + _queue.close(); + join(); + } + @property SocketState state() { + return _state; + } + void connect(string host, ushort port) { + _queue.put(delegate() { + if (_state == SocketState.Connecting) { + _callback.onError(this, SocketError.NotConnected, "socket is already connecting"); + return; + } + if (_state == SocketState.Connected) { + _callback.onError(this, SocketError.NotConnected, "socket is already connected"); + return; + } + doDisconnect(); + _sock = new TcpSocket(); + _sock.blocking = false; + _readSet = new SocketSet(); + _writeSet = new SocketSet(); + _errorSet = new SocketSet(); + _state = SocketState.Connecting; + _sock.connect(new InternetAddress(host, port)); + }); + } + void disconnect() { + _queue.put(delegate() { + if (!_sock) + return; + doDisconnect(); + }); + } + void send(ubyte[] data) { + _queue.put(delegate() { + if (!_sock) { + _callback.onError(this, SocketError.NotConnected, "socket is not connected"); + return; + } + for (;;) { + long bytesSent = _sock.send(data); + if (bytesSent == Socket.ERROR) { + _callback.onError(this, SocketError.WriteError, "error while writing to connection"); + return; + } else { + //Log.d("Bytes sent:" ~ to!string(bytesSent)); + if (bytesSent >= data.length) + return; + data = data[cast(int)bytesSent .. $]; + } + } + }); + } +} diff --git a/src/dlangui/core/queue.d b/src/dlangui/core/queue.d new file mode 100644 index 00000000..b3bd9f3b --- /dev/null +++ b/src/dlangui/core/queue.d @@ -0,0 +1,144 @@ +module dlangui.core.queue; + +import core.sync.condition; +import core.sync.mutex; + +class BlockingQueue(T) { + + private Mutex _mutex; + private Condition _condition; + private T[] _buffer; + private int _readPos; + private int _writePos; + private shared bool _closed; + + this() { + _mutex = new Mutex(); + _condition = new Condition(_mutex); + _readPos = 0; + _writePos = 0; + } + + ~this() { + close(); + if (_condition) { + destroy(_condition); + _condition = null; + } + if (_mutex) { + destroy(_mutex); + _mutex = null; + } + } + + void close() { + if (_mutex && !_closed) { + synchronized(_mutex) { + _closed = true; + if (_condition !is null) + _condition.notifyAll(); + } + } else { + _closed = true; + } + } + + /// returns true if queue is closed + @property bool closed() { + return _closed; + } + + private void move() { + if (_readPos > 1024 && _readPos > _buffer.length * 3 / 4) { + // move buffer data + for (int i = 0; _readPos + i < _writePos; i++) + _buffer[i] = _buffer[_readPos + i]; + _writePos -= _readPos; + _readPos = 0; + } + } + + private void append(ref T item) { + if (_writePos >= _buffer.length) { + move(); + _buffer.length = _buffer.length == 0 ? 64 : _buffer.length * 2; + } + _buffer[_writePos++] = item; + } + + void put(T item) { + if (_closed) + return; + synchronized(_mutex) { + if (_closed) + return; + append(item); + _condition.notifyAll(); + } + } + + void put(T[] items) { + if (_closed) + return; + synchronized(_mutex) { + if (_closed) + return; + foreach(ref item; items) { + append(item); + } + _condition.notifyAll(); + } + } + + bool get(ref T value, int timeoutMillis = 0) { + if (_closed) + return false; + synchronized(_mutex) { + if (_closed) + return false; + if (_readPos < _writePos) { + value = _buffer[_readPos++]; + return true; + } + try { + if (timeoutMillis <= 0) + _condition.wait(); // no timeout + else if (!_condition.wait(dur!"msecs"(timeoutMillis))) + return false; // timeout + } catch (Exception e) { + // ignore + } + if (_readPos < _writePos) { + value = _buffer[_readPos++]; + return true; + } + } + return false; + } + + bool getAll(ref T[] values, int timeoutMillis) { + if (_closed) + return false; + synchronized(_mutex) { + if (_closed) + return false; + values.length = 0; + while (_readPos < _writePos) + values ~= _buffer[_readPos++]; + if (values.length > 0) + return true; + if (timeoutMillis <= 0) + _condition.wait(); // no timeout + else if (!_condition.wait(dur!"msecs"(timeoutMillis))) + return false; // timeout + while (_readPos < _writePos) + values ~= _buffer[_readPos++]; + if (values.length > 0) + return true; + } + return false; + } +} + +alias Runnable = void delegate(); +alias RunnableQueue = BlockingQueue!Runnable; diff --git a/src/dlangui/platforms/common/platform.d b/src/dlangui/platforms/common/platform.d index 2fbd6135..6f2c1f41 100644 --- a/src/dlangui/platforms/common/platform.d +++ b/src/dlangui/platforms/common/platform.d @@ -26,6 +26,7 @@ import dlangui.widgets.widget; import dlangui.widgets.popup; import dlangui.graphics.drawbuf; import dlangui.core.stdaction; +import dlangui.core.asyncsocket; private import dlangui.graphics.gldrawbuf; private import std.algorithm; @@ -814,6 +815,12 @@ class Window : CustomEventTarget { postEvent(event); } + /// Creates async socket + AsyncSocket createAsyncSocket(AsyncSocketCallback callback) { + AsyncClientConnection conn = new AsyncClientConnection(new AsyncSocketCallbackProxy(callback, &executeInUiThread)); + return conn; + } + /// remove event from queue by unique id if not yet dispatched (this method can be used from background thread) void cancelEvent(uint uniqueId) { CustomEvent ev = _eventList.get(uniqueId);