├── .gitignore ├── assets ├── gdialog-htmlbox.png ├── gdialog-msgbox.png ├── gdialog-textbox.png ├── gdialog-yesno.png ├── gdialog-bannerbox.png ├── gdialog-dark-mode.png ├── gdialog-filesave.png ├── gdialog-inputbox.png ├── gdialog-filesave-2.png ├── gdialog-fileselect-2.png ├── gdialog-fileselect.png ├── gdialog-light-mode.png ├── gdialog-progressbar.gif ├── gdialog-credentialsbox.png ├── gdialog-pickerbox-radio.png ├── gdialog-secure-inputbox.png ├── gdialog-notification-bigsur.png ├── gdialog-pickerbox-dropdown.gif ├── gdialog-pickerbox-segmented.gif ├── gdialog-bannerbox-one-button.png ├── gdialog-bannerbox-two-buttons.png ├── gdialog-notification-catalina.png ├── gdialog-notification-request.png └── gdialog-progressbar-indeterminate.gif ├── source ├── templates │ ├── filesave.dylib │ ├── htmlbox.dylib │ ├── inputbox.dylib │ ├── msgbox.dylib │ ├── textbox.dylib │ ├── bannerbox.dylib │ ├── fileselect.dylib │ ├── pickerbox.dylib │ ├── notification.dylib │ ├── progressbar.dylib │ ├── credentialsbox.dylib │ ├── secure-inputbox.dylib │ └── config.json ├── binding.gyp ├── package.json ├── src │ ├── bannerbox │ │ ├── Package.swift │ │ └── Sources │ │ │ └── bannerbox │ │ │ └── bannerbox.swift │ ├── msgbox │ │ ├── Package.swift │ │ └── Sources │ │ │ └── msgbox │ │ │ └── msgbox.swift │ ├── htmlbox │ │ ├── Package.swift │ │ └── Sources │ │ │ └── htmlbox │ │ │ └── htmlbox.swift │ ├── textbox │ │ ├── Package.swift │ │ └── Sources │ │ │ └── textbox │ │ │ └── textbox.swift │ ├── filesave │ │ ├── Package.swift │ │ └── Sources │ │ │ └── filesave │ │ │ └── filesave.swift │ ├── inputbox │ │ ├── Package.swift │ │ └── Sources │ │ │ └── inputbox │ │ │ └── inputbox.swift │ ├── pickerbox │ │ ├── Package.swift │ │ └── Sources │ │ │ └── pickerbox │ │ │ └── pickerbox.swift │ ├── fileselect │ │ └── Package.swift │ ├── progressbar │ │ ├── Package.swift │ │ └── Sources │ │ │ └── progressbar │ │ │ └── progressbar.swift │ ├── notification │ │ ├── Package.swift │ │ └── Sources │ │ │ └── notification │ │ │ └── notification.swift │ ├── credentialsbox │ │ └── Package.swift │ ├── secure-inputbox │ │ ├── Package.swift │ │ └── Sources │ │ │ └── secure-inputbox │ │ │ └── secure-inputbox.swift │ └── index.cpp ├── gDialog_worker.js ├── package-lock.json └── index.js ├── .github └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── LICENSE.md /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /assets/gdialog-htmlbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giladdarshan/gdialog/HEAD/assets/gdialog-htmlbox.png -------------------------------------------------------------------------------- /assets/gdialog-msgbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giladdarshan/gdialog/HEAD/assets/gdialog-msgbox.png -------------------------------------------------------------------------------- /assets/gdialog-textbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giladdarshan/gdialog/HEAD/assets/gdialog-textbox.png -------------------------------------------------------------------------------- /assets/gdialog-yesno.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giladdarshan/gdialog/HEAD/assets/gdialog-yesno.png -------------------------------------------------------------------------------- /assets/gdialog-bannerbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giladdarshan/gdialog/HEAD/assets/gdialog-bannerbox.png -------------------------------------------------------------------------------- /assets/gdialog-dark-mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giladdarshan/gdialog/HEAD/assets/gdialog-dark-mode.png -------------------------------------------------------------------------------- /assets/gdialog-filesave.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giladdarshan/gdialog/HEAD/assets/gdialog-filesave.png -------------------------------------------------------------------------------- /assets/gdialog-inputbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giladdarshan/gdialog/HEAD/assets/gdialog-inputbox.png -------------------------------------------------------------------------------- /assets/gdialog-filesave-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giladdarshan/gdialog/HEAD/assets/gdialog-filesave-2.png -------------------------------------------------------------------------------- /assets/gdialog-fileselect-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giladdarshan/gdialog/HEAD/assets/gdialog-fileselect-2.png -------------------------------------------------------------------------------- /assets/gdialog-fileselect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giladdarshan/gdialog/HEAD/assets/gdialog-fileselect.png -------------------------------------------------------------------------------- /assets/gdialog-light-mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giladdarshan/gdialog/HEAD/assets/gdialog-light-mode.png -------------------------------------------------------------------------------- /assets/gdialog-progressbar.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giladdarshan/gdialog/HEAD/assets/gdialog-progressbar.gif -------------------------------------------------------------------------------- /source/templates/filesave.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giladdarshan/gdialog/HEAD/source/templates/filesave.dylib -------------------------------------------------------------------------------- /source/templates/htmlbox.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giladdarshan/gdialog/HEAD/source/templates/htmlbox.dylib -------------------------------------------------------------------------------- /source/templates/inputbox.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giladdarshan/gdialog/HEAD/source/templates/inputbox.dylib -------------------------------------------------------------------------------- /source/templates/msgbox.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giladdarshan/gdialog/HEAD/source/templates/msgbox.dylib -------------------------------------------------------------------------------- /source/templates/textbox.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giladdarshan/gdialog/HEAD/source/templates/textbox.dylib -------------------------------------------------------------------------------- /assets/gdialog-credentialsbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giladdarshan/gdialog/HEAD/assets/gdialog-credentialsbox.png -------------------------------------------------------------------------------- /source/templates/bannerbox.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giladdarshan/gdialog/HEAD/source/templates/bannerbox.dylib -------------------------------------------------------------------------------- /source/templates/fileselect.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giladdarshan/gdialog/HEAD/source/templates/fileselect.dylib -------------------------------------------------------------------------------- /source/templates/pickerbox.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giladdarshan/gdialog/HEAD/source/templates/pickerbox.dylib -------------------------------------------------------------------------------- /assets/gdialog-pickerbox-radio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giladdarshan/gdialog/HEAD/assets/gdialog-pickerbox-radio.png -------------------------------------------------------------------------------- /assets/gdialog-secure-inputbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giladdarshan/gdialog/HEAD/assets/gdialog-secure-inputbox.png -------------------------------------------------------------------------------- /source/templates/notification.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giladdarshan/gdialog/HEAD/source/templates/notification.dylib -------------------------------------------------------------------------------- /source/templates/progressbar.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giladdarshan/gdialog/HEAD/source/templates/progressbar.dylib -------------------------------------------------------------------------------- /assets/gdialog-notification-bigsur.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giladdarshan/gdialog/HEAD/assets/gdialog-notification-bigsur.png -------------------------------------------------------------------------------- /assets/gdialog-pickerbox-dropdown.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giladdarshan/gdialog/HEAD/assets/gdialog-pickerbox-dropdown.gif -------------------------------------------------------------------------------- /assets/gdialog-pickerbox-segmented.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giladdarshan/gdialog/HEAD/assets/gdialog-pickerbox-segmented.gif -------------------------------------------------------------------------------- /source/templates/credentialsbox.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giladdarshan/gdialog/HEAD/source/templates/credentialsbox.dylib -------------------------------------------------------------------------------- /source/templates/secure-inputbox.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giladdarshan/gdialog/HEAD/source/templates/secure-inputbox.dylib -------------------------------------------------------------------------------- /assets/gdialog-bannerbox-one-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giladdarshan/gdialog/HEAD/assets/gdialog-bannerbox-one-button.png -------------------------------------------------------------------------------- /assets/gdialog-bannerbox-two-buttons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giladdarshan/gdialog/HEAD/assets/gdialog-bannerbox-two-buttons.png -------------------------------------------------------------------------------- /assets/gdialog-notification-catalina.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giladdarshan/gdialog/HEAD/assets/gdialog-notification-catalina.png -------------------------------------------------------------------------------- /assets/gdialog-notification-request.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giladdarshan/gdialog/HEAD/assets/gdialog-notification-request.png -------------------------------------------------------------------------------- /assets/gdialog-progressbar-indeterminate.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/giladdarshan/gdialog/HEAD/assets/gdialog-progressbar-indeterminate.gif -------------------------------------------------------------------------------- /source/binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | "targets": [ 3 | { 4 | "target_name": "gDialog", 5 | "cflags!": [ "-fno-exceptions" ], 6 | "cflags_cc!": [ "-fno-exceptions" ], 7 | "sources": [ 8 | "./src/index.cpp" 9 | ], 10 | "include_dirs": [ 11 | " { 22 | gDialog_result = result; 23 | }); 24 | this.gDialog.run(result => { 25 | gDialog_result = result; 26 | }, this.workerCallback); 27 | 28 | if (!options.hasOwnProperty("no_return") || (options["no_return"] === false)) { 29 | if (gDialog_result !== "") { 30 | return gDialog_result; 31 | } 32 | } 33 | return ''; 34 | } 35 | workerCallback(returnString) { 36 | process.send({ message: returnString }); 37 | } 38 | } 39 | 40 | // if (require.main === module) { 41 | // console.log('called directly 2'); 42 | // } else { 43 | // console.log('required as a module 2'); 44 | // } 45 | const worker = new Worker(); 46 | 47 | process.on("message", data => { 48 | worker.configure(data.settings); 49 | process.send({ result: worker.show_gDialog(data.options) }); 50 | process.exit(0); 51 | }); 52 | -------------------------------------------------------------------------------- /source/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gdialog", 3 | "version": "0.1.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "gdialog", 9 | "version": "0.1.0", 10 | "license": "ISC", 11 | "dependencies": { 12 | "bindings": "^1.5.0", 13 | "node-addon-api": "^3.0.2" 14 | } 15 | }, 16 | "node_modules/bindings": { 17 | "version": "1.5.0", 18 | "resolved": "http://localhost:4873/bindings/-/bindings-1.5.0.tgz", 19 | "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", 20 | "license": "MIT", 21 | "dependencies": { 22 | "file-uri-to-path": "1.0.0" 23 | } 24 | }, 25 | "node_modules/file-uri-to-path": { 26 | "version": "1.0.0", 27 | "resolved": "http://localhost:4873/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", 28 | "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", 29 | "license": "MIT" 30 | }, 31 | "node_modules/node-addon-api": { 32 | "version": "3.0.2", 33 | "resolved": "http://localhost:4873/node-addon-api/-/node-addon-api-3.0.2.tgz", 34 | "integrity": "sha512-+D4s2HCnxPd5PjjI0STKwncjXTUKKqm74MDMz9OPXavjsGmjkvwgLtA5yoxJUdmpj52+2u+RrXgPipahKczMKg==", 35 | "license": "MIT" 36 | } 37 | }, 38 | "dependencies": { 39 | "bindings": { 40 | "version": "1.5.0", 41 | "resolved": "http://localhost:4873/bindings/-/bindings-1.5.0.tgz", 42 | "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", 43 | "requires": { 44 | "file-uri-to-path": "1.0.0" 45 | } 46 | }, 47 | "file-uri-to-path": { 48 | "version": "1.0.0", 49 | "resolved": "http://localhost:4873/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", 50 | "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" 51 | }, 52 | "node-addon-api": { 53 | "version": "3.0.2", 54 | "resolved": "http://localhost:4873/node-addon-api/-/node-addon-api-3.0.2.tgz", 55 | "integrity": "sha512-+D4s2HCnxPd5PjjI0STKwncjXTUKKqm74MDMz9OPXavjsGmjkvwgLtA5yoxJUdmpj52+2u+RrXgPipahKczMKg==" 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /source/templates/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "global_options": { 3 | "title": "string", 4 | "header": "string", 5 | "text": "string", 6 | "scrollable_text": "bool-true", 7 | "icon_file": "image", 8 | "system_icon": "string", 9 | "width": "float", 10 | "height": "float", 11 | "window_size": "size", 12 | "buttons": "array", 13 | "button": "array-inc", 14 | "allow_quit": "bool-true", 15 | "no_return": "bool-false", 16 | "focus": "bool-true", 17 | "timeout": "int", 18 | "static": "bool-true" 19 | }, 20 | "templates": { 21 | "msgbox": { 22 | "file": "msgbox.dylib", 23 | "options": {} 24 | }, 25 | "inputbox": { 26 | "file": "inputbox.dylib", 27 | "options": { 28 | "initial_text": "string", 29 | "background_text": "string", 30 | "encode_text": "bool-true" 31 | } 32 | }, 33 | "secure-inputbox": { 34 | "file": "secure-inputbox.dylib", 35 | "options": { 36 | "initial_text": "string", 37 | "background_text": "string", 38 | "encode_text": "bool-true" 39 | } 40 | }, 41 | "textbox": { 42 | "file": "textbox.dylib", 43 | "options": { 44 | "initial_text": "string", 45 | "encode_text": "bool-true" 46 | } 47 | }, 48 | "credentialsbox": { 49 | "file": "credentialsbox.dylib", 50 | "options": { 51 | "user_label": "string", 52 | "user_initial_text": "string", 53 | "user_background_text": "string", 54 | "pass_label": "string", 55 | "pass_initial_text": "string", 56 | "pass_background_text": "string", 57 | "extra_field_label": "string", 58 | "extra_field_initial_text": "string", 59 | "extra_field_background_text": "string", 60 | "extra_field_secured": "bool-true", 61 | "encode_text": "bool-true" 62 | } 63 | }, 64 | "htmlbox": { 65 | "file": "htmlbox.dylib", 66 | "options": { 67 | "html_b64": "string", 68 | "file": "string", 69 | "base_path": "string", 70 | "url": "string", 71 | "fullscreen": "bool-true", 72 | "kiosk": "bool-true", 73 | "normal_window": "bool-true" 74 | } 75 | }, 76 | "progressbar": { 77 | "file": "progressbar.dylib", 78 | "options": { 79 | "stoppable": "bool-true", 80 | "indeterminate": "bool-true" 81 | } 82 | }, 83 | "pickerbox": { 84 | "file": "pickerbox.dylib", 85 | "options": { 86 | "items": "array", 87 | "style": "string" 88 | } 89 | }, 90 | "fileselect": { 91 | "file": "fileselect.dylib", 92 | "options": { 93 | "with_file": "string", 94 | "with_directory": "string", 95 | "with_extensions": "array", 96 | "packages_as_directories": "bool-true", 97 | "select_directories": "bool-true", 98 | "select_only_directories": "bool-true", 99 | "select_multiple": "bool-true" 100 | } 101 | }, 102 | "filesave": { 103 | "file": "filesave.dylib", 104 | "options": { 105 | "with_file": "string", 106 | "with_directory": "string", 107 | "with_extensions": "array", 108 | "packages_as_directories": "bool-true", 109 | "dont_create_directories": "bool-true" 110 | } 111 | }, 112 | "bannerbox": { 113 | "file": "bannerbox.dylib", 114 | "options": { 115 | "background_color": "" 116 | } 117 | }, 118 | "swifttests": { 119 | "file": "swifttests.dylib", 120 | "options": {} 121 | }, 122 | "notification": { 123 | "file": "notification.dylib", 124 | "options": { 125 | "dont_wait": "bool-true" 126 | } 127 | } 128 | } 129 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Gilad Darshan (gilad@activedesign.co.il) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | Third Parties: 24 | - Node.js - [MIT License](https://github.com/nodejs/node/blob/master/LICENSE) 25 | ``` 26 | Copyright Node.js contributors. All rights reserved. 27 | 28 | Permission is hereby granted, free of charge, to any person obtaining a copy 29 | of this software and associated documentation files (the "Software"), to 30 | deal in the Software without restriction, including without limitation the 31 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 32 | sell copies of the Software, and to permit persons to whom the Software is 33 | furnished to do so, subject to the following conditions: 34 | 35 | The above copyright notice and this permission notice shall be included in 36 | all copies or substantial portions of the Software. 37 | 38 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 39 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 40 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 41 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 42 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 43 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 44 | IN THE SOFTWARE. 45 | ``` 46 | - node-addon-api - [MIT License](https://github.com/nodejs/node-addon-api/blob/master/LICENSE.md) 47 | ``` 48 | Copyright (c) 2017 Node.js API collaborators 49 | Node.js API collaborators listed at https://github.com/nodejs/node-addon-api#collaborators 50 | 51 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 52 | 53 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 54 | 55 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 56 | ``` 57 | - node-gyp - [MIT License](https://github.com/nodejs/node-gyp/blob/master/LICENSE) 58 | ``` 59 | Copyright (c) 2012 Nathan Rajlich 60 | 61 | Permission is hereby granted, free of charge, to any person 62 | obtaining a copy of this software and associated documentation 63 | files (the "Software"), to deal in the Software without 64 | restriction, including without limitation the rights to use, 65 | copy, modify, merge, publish, distribute, sublicense, and/or sell 66 | copies of the Software, and to permit persons to whom the 67 | Software is furnished to do so, subject to the following 68 | conditions: 69 | 70 | The above copyright notice and this permission notice shall be 71 | included in all copies or substantial portions of the Software. 72 | 73 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 74 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 75 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 76 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 77 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 78 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 79 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 80 | OTHER DEALINGS IN THE SOFTWARE. 81 | ``` 82 | - node-bindings - [MIT License](https://github.com/TooTallNate/node-bindings/blob/master/LICENSE.md) 83 | ``` 84 | Copyright (c) 2012 Nathan Rajlich 85 | 86 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 87 | 88 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 89 | 90 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 91 | ``` 92 | -------------------------------------------------------------------------------- /source/src/index.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | //// 7 | // #include 8 | // #include 9 | /// 10 | 11 | 12 | using std::cerr; 13 | using std::cout; 14 | using std::endl; 15 | using std::string; 16 | // using namespace Napi; 17 | 18 | #ifdef __APPLE__ 19 | #define IS_SUPPORTED_PLATFORM true 20 | const string SHARED_LIBRARY_EXT = "dylib"; 21 | #endif 22 | 23 | #ifdef __linux__ 24 | #define IS_SUPPORTED_PLATFORM true 25 | const string SHARED_LIBRARY_EXT = "so"; 26 | #endif 27 | 28 | #if !defined IS_SUPPORTED_PLATFORM 29 | cerr << "Unsupported Platform"; 30 | exit(EXIT_FAILURE); 31 | #endif 32 | 33 | string SWIFT_SHARED_LIBRARY_PATH = ""; 34 | Napi::Function nodeCB; 35 | Napi::Env nodeEnv = NULL; 36 | 37 | // const auto SWIFT_TEST = "swift_test"; 38 | 39 | const auto SWIFT_SET_OPTIONS = "swift_set_options"; 40 | const auto SWIFT_RUN_DIALOG = "swift_run_dialog"; 41 | 42 | // typedef int (*TestFunc)(); 43 | typedef char* (*SetOptionsFunc)(const char*); 44 | typedef char* (*RunDialogFunc)(void(*)(const char*)); 45 | 46 | void *DLSymOrDie(void *lib, const string& func_name) { 47 | const auto func = dlsym(lib, func_name.c_str()); 48 | const auto dlsym_error = dlerror(); 49 | if (dlsym_error) { 50 | cerr << "Could not load symbol create: " << dlsym_error << endl; 51 | dlclose(lib); 52 | exit(EXIT_FAILURE); 53 | } 54 | return func; 55 | } 56 | 57 | void *DLOpenOrDie(const string& path) { 58 | const auto lib = dlopen(path.c_str(), RTLD_LAZY); 59 | if (!lib) { 60 | cerr << "Could not load library: " << dlerror() << endl; 61 | exit(EXIT_FAILURE); 62 | } 63 | return lib; 64 | } 65 | 66 | // void Test(const Napi::CallbackInfo& info) { 67 | // Napi::Env env = info.Env(); 68 | // int n = -1; 69 | // const auto swiftLib = DLOpenOrDie(SWIFT_SHARED_LIBRARY_PATH); 70 | // const auto _TEST = (TestFunc)DLSymOrDie(swiftLib, SWIFT_TEST); 71 | // Napi::Function cb; 72 | 73 | // n = _TEST(); 74 | // cb = info[0].As(); 75 | // Napi::Number result = Napi::Number::New(env, n); 76 | // cb.MakeCallback(env.Global(), { result }); 77 | // } 78 | 79 | void dialogCallback(const char* returnString) { 80 | // cout << "CPP - BLAAA CALLBACK BLAAA " << returnString << endl; 81 | Napi::String result = Napi::String::New(nodeEnv, returnString); 82 | nodeCB.MakeCallback(nodeEnv.Global(), { result }); 83 | } 84 | 85 | void SetOptions(const Napi::CallbackInfo& info) { 86 | Napi::Env env = info.Env(); 87 | std::string n = "-1"; 88 | std::string str = info[0].ToString(); 89 | const auto swiftLib = DLOpenOrDie(SWIFT_SHARED_LIBRARY_PATH); 90 | const auto _SetOptionsFunc = (SetOptionsFunc)DLSymOrDie(swiftLib, SWIFT_SET_OPTIONS); 91 | Napi::Function cb; 92 | 93 | const char *options = str.c_str(); 94 | n = _SetOptionsFunc(options); 95 | cb = info[1].As(); 96 | // cout << "bla" << endl; 97 | // cout << n << endl; 98 | Napi::String result = Napi::String::New(env, n.c_str()); 99 | cb.MakeCallback(env.Global(), { result }); 100 | } 101 | 102 | void RunDialog(const Napi::CallbackInfo& info) { 103 | // cout << "RUNNING - " << sizeof(info)/sizeof(info[0]) << endl; 104 | // Napi::Env env = info.Env(); 105 | nodeEnv = info.Env(); 106 | std::string n = "-1"; 107 | const auto swiftLib = DLOpenOrDie(SWIFT_SHARED_LIBRARY_PATH); 108 | const auto _RUN_DIALOG = (RunDialogFunc)DLSymOrDie(swiftLib, SWIFT_RUN_DIALOG); 109 | Napi::Function cb; 110 | nodeCB = info[1].As(); 111 | void (*callbackPtr)(const char*) { &dialogCallback }; 112 | n = _RUN_DIALOG(callbackPtr); 113 | cb = info[0].As(); 114 | // n = _RUN_DIALOG(); 115 | // cb = info[0].As(); 116 | Napi::String result = Napi::String::New(nodeEnv, n.c_str()); 117 | cb.MakeCallback(nodeEnv.Global(), { result }); 118 | } 119 | 120 | 121 | void SetLibPath(const Napi::CallbackInfo& info) { 122 | // Napi::Env env = info.Env(); 123 | SWIFT_SHARED_LIBRARY_PATH = info[0].ToString(); 124 | // cout << "Setting Path:" << endl; 125 | // cout << SWIFT_SHARED_LIBRARY_PATH << endl; 126 | } 127 | 128 | 129 | 130 | 131 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 132 | // Napi::Promise RunDialogPromise(const Napi::CallbackInfo& info) { 133 | // Napi::Env env = info.Env(); 134 | 135 | // // std::string n = "-1"; 136 | // // const auto swiftLib = DLOpenOrDie(SWIFT_SHARED_LIBRARY_PATH); 137 | // // const auto _RUN_DIALOG = (RunDialogFunc)DLSymOrDie(swiftLib, SWIFT_RUN_DIALOG); 138 | // // Napi::Function cb; 139 | 140 | // // n = _RUN_DIALOG(); 141 | // // cb = info[0].As(); 142 | // // Napi::String result = Napi::String::New(env, n.c_str()); 143 | // // cb.MakeCallback(env.Global(), { result }); 144 | 145 | 146 | // auto deferred = Napi::Promise::Deferred::New(env); 147 | // if (info.Length() != 2) { 148 | // deferred.Reject(Napi::TypeError::New(env, "Invalid argument count").Value()); 149 | // } 150 | // else if (!info[0].IsNumber() || !info[1].IsNumber()) { 151 | // deferred.Reject(Napi::TypeError::New(env, "Invalid argument types").Value()); 152 | // } 153 | // else { 154 | // // double arg0 = info[0].As().DoubleValue(); 155 | // // double arg1 = info[1].As().DoubleValue(); 156 | // // Napi::Number num = Napi::Number::New(env, arg0 + arg1); 157 | 158 | // std::string n = "-1"; 159 | // const auto swiftLib = DLOpenOrDie(SWIFT_SHARED_LIBRARY_PATH); 160 | // const auto _RUN_DIALOG = (RunDialogFunc)DLSymOrDie(swiftLib, SWIFT_RUN_DIALOG); 161 | // // Napi::Function cb; 162 | 163 | // n = _RUN_DIALOG(); 164 | // // cb = info[0].As(); 165 | // Napi::String result = Napi::String::New(env, n.c_str()); 166 | // // cb.MakeCallback(env.Global(), { result }); 167 | 168 | // deferred.Resolve({ result }); 169 | // } 170 | 171 | // return deferred.Promise(); 172 | 173 | // } 174 | 175 | // class DialogAsyncWorker : public Napi::AsyncWorker { 176 | 177 | // public: 178 | // DialogAsyncWorker(Napi::Function& callback): AsyncWorker(callback), result("-1") {}; 179 | // virtual ~DialogAsyncWorker() {}; 180 | 181 | // void Execute() { 182 | // // Napi::Env env = info.Env(); 183 | // // std::string n = "-1"; 184 | // cout << "Test 1" << endl; 185 | // const auto swiftLib = DLOpenOrDie(SWIFT_SHARED_LIBRARY_PATH); 186 | // const auto _RUN_DIALOG = (RunDialogFunc)DLSymOrDie(swiftLib, SWIFT_RUN_DIALOG); 187 | // // Napi::Function cb; 188 | // cout << "Test 2" << endl; 189 | 190 | // result = _RUN_DIALOG(); 191 | // cout << "Test 3" << endl; 192 | // // n = _RUN_DIALOG(); 193 | // // cb = info[0].As(); 194 | // // Napi::String result = Napi::String::New(env, n.c_str()); 195 | // // cb.MakeCallback(env.Global(), { result }); 196 | 197 | // // std::this_thread::sleep_for(std::chrono::seconds(runTime)); 198 | // // if (runTime == 4) { 199 | // // SetError ("Oops! Failed after 'working' 4 seconds."); 200 | // // } 201 | // } 202 | // void OnOK() { 203 | // // std::string msg = "SimpleAsyncWorker returning after 'working' " + std::to_string(runTime) + " seconds."; 204 | // Callback().Call({Env().Null(), Napi::String::New(Env(), result)}); 205 | // } 206 | 207 | // private: 208 | // std::string result = "-1"; 209 | // }; 210 | 211 | // Napi::Value runDialogAsyncWorker(const Napi::CallbackInfo& info) { 212 | // // int runTime = info[0].As(); 213 | // Napi::Function callback = info[0].As(); 214 | // DialogAsyncWorker* asyncWorker = new DialogAsyncWorker(callback); 215 | // asyncWorker->Queue(); 216 | // // std::string msg = "SimpleAsyncWorker for " + std::to_string(runTime) + " seconds queued."; 217 | // std::string msg = "Dialog loaded"; 218 | // return Napi::String::New(info.Env(),msg.c_str()); 219 | // }; 220 | 221 | 222 | 223 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 224 | 225 | // callback method when module is registered with Node.js 226 | Napi::Object Init(Napi::Env env, Napi::Object exports){ 227 | // set a key on `exports` object 228 | // exports.Set( 229 | // Napi::String::New(env, "test"), 230 | // Napi::Function::New(env, Test) 231 | // ); 232 | exports.Set( 233 | Napi::String::New(env, "setOptions"), 234 | Napi::Function::New(env, SetOptions) 235 | ); 236 | exports.Set( 237 | Napi::String::New(env, "setLibPath"), 238 | Napi::Function::New(env, SetLibPath) 239 | ); 240 | exports.Set( 241 | Napi::String::New(env, "run"), 242 | Napi::Function::New(env, RunDialog) 243 | ); 244 | // exports.Set( 245 | // Napi::String::New(env, "runPromise"), 246 | // Napi::Function::New(env, RunDialogPromise) 247 | // ); 248 | // exports.Set( 249 | // Napi::String::New(env, "runSimpleAsyncWorker"), 250 | // Napi::Function::New(env, runSimpleAsyncWorker) 251 | // ); 252 | // exports["runSimpleAsyncWorker"] = Napi::Function::New(env, runSimpleAsyncWorker, std::string("runSimpleAsyncWorker")); 253 | // exports["runDialogAsyncWorker"] = Napi::Function::New(env, runDialogAsyncWorker, std::string("runDialogAsyncWorker")); 254 | 255 | 256 | return exports; 257 | } 258 | 259 | NODE_API_MODULE(gDialog, Init) -------------------------------------------------------------------------------- /source/src/notification/Sources/notification/notification.swift: -------------------------------------------------------------------------------- 1 | // swift build --package-path ./src/notification -Xswiftc "-target" -Xswiftc "x86_64-apple-macosx10.15" -Xlinker -rpath -Xlinker @executable_path/Frameworks 2 | 3 | import AppKit 4 | // import SwiftUI 5 | import UserNotifications 6 | 7 | // Variables - Defaults 8 | var gdialog_type = "notification"; 9 | public typealias `func` = @convention(c) (UnsafePointer) -> Void 10 | var callbackHolder: `func`? 11 | var options: [String: Any] = [ 12 | "async": false, 13 | "header": "", 14 | "title": "", 15 | "text": "", 16 | "buttons": [ "OK" ], 17 | "dont_wait": false 18 | ]; 19 | 20 | 21 | /////////////////////// 22 | // DO NOT EDIT BELOW // 23 | /////////////////////// 24 | 25 | let app = NSApplication.shared 26 | let delegate = AppDelegate() 27 | var dialogResult = "-1"; 28 | var gotResponse = false; 29 | 30 | class WindowDelegate: NSObject, NSWindowDelegate { 31 | func windowWillClose(_ notification: Notification) { 32 | NSApplication.shared.terminate(0) 33 | } 34 | } 35 | 36 | class CustomWindow: NSWindow { 37 | override var canBecomeKey: Bool { 38 | return true 39 | } 40 | } 41 | 42 | func runNotification(identifier: String, buttons: [String]) -> Void { 43 | let center = UNUserNotificationCenter.current() 44 | var actionsArr: [UNNotificationAction] = [] 45 | if (buttons.count > 0) { 46 | for button in buttons { 47 | actionsArr.append(UNNotificationAction(identifier: button, title: button, options: .foreground)); 48 | } 49 | } 50 | 51 | let meetingInviteCategory = 52 | UNNotificationCategory.init(identifier: identifier, 53 | actions: actionsArr, 54 | intentIdentifiers: [], 55 | hiddenPreviewsBodyPlaceholder: "", 56 | options: .customDismissAction) 57 | 58 | center.setNotificationCategories([meetingInviteCategory]) 59 | 60 | let content = UNMutableNotificationContent() 61 | 62 | if options["title"] as! String != "" { 63 | content.title = options["title"] as! String 64 | } 65 | if options["header"] as! String != "" { 66 | content.subtitle = options["header"] as! String 67 | } 68 | if options["text"] as! String != "" { 69 | content.body = options["text"] as! String 70 | } 71 | 72 | content.categoryIdentifier = identifier 73 | let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.1, repeats: false) 74 | let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger) 75 | 76 | center.add(request) { (error) in 77 | if error != nil { 78 | print("Error adding request: \(String(describing: error))") 79 | NSApplication.shared.terminate(0) 80 | } 81 | if (options["dont_wait"] as! Bool) { 82 | dialogResult = "" 83 | app.stop("quite") 84 | } 85 | } 86 | } 87 | class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate { 88 | let window = CustomWindow() 89 | let windowDelegate = WindowDelegate() 90 | let buttons: [String] = options["buttons"] as! [String]; 91 | var contentIdentifier = "" 92 | 93 | func applicationDidFinishLaunching(_ notification: Notification) { 94 | let center = UNUserNotificationCenter.current() 95 | center.delegate = self; 96 | center.requestAuthorization(options: [ .alert, .sound, .badge ]) { granted, error in 97 | if let error = error { 98 | print("Got an error while asking for permissions: \(error)") 99 | NSApplication.shared.terminate(0) 100 | } 101 | 102 | self.contentIdentifier = UUID().uuidString 103 | runNotification(identifier: self.contentIdentifier, buttons: self.buttons) 104 | } 105 | 106 | } 107 | func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { 108 | // print("will present"); 109 | // print(notification); 110 | completionHandler([.alert]) 111 | } 112 | func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { 113 | if response.notification.request.content.categoryIdentifier == self.contentIdentifier { 114 | var sendNextEvent = false; 115 | switch response.actionIdentifier { 116 | case "com.apple.UNNotificationDefaultActionIdentifier": 117 | dialogResult = "clicked" 118 | case "com.apple.UNNotificationDismissActionIdentifier": 119 | dialogResult = "dismissed" 120 | sendNextEvent = true; 121 | default: 122 | if self.buttons.contains(response.actionIdentifier) { 123 | dialogResult = response.actionIdentifier 124 | } 125 | else { 126 | dialogResult = "Default action: \(response.actionIdentifier)" 127 | } 128 | } 129 | // print("a") 130 | app.stop("stop") 131 | // print(app.nextEvent(matching: NSEvent.EventTypeMask.any, until: nil, inMode: RunLoop.Mode.eventTracking, dequeue: false)) 132 | if (sendNextEvent) { 133 | let dummyEvent = NSEvent.otherEvent(with: NSEvent.EventType.applicationDefined, location: NSZeroPoint, modifierFlags: NSEvent.ModifierFlags(rawValue: 0), timestamp: 0, windowNumber: 0, context: nil, subtype:0, data1:0, data2:0)!; 134 | NSApp.postEvent(dummyEvent, atStart: true); 135 | 136 | } 137 | // print("b") 138 | } 139 | // else { 140 | // print("Category missmatch: \(response.notification.request.content.categoryIdentifier)") 141 | // } 142 | completionHandler() 143 | } 144 | } 145 | 146 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 147 | 148 | @_cdecl("swift_run_dialog") 149 | public func run_dialog() -> UnsafePointer { 150 | // options["buttons"] = [ "OK" ]; 151 | 152 | let delegate = AppDelegate() 153 | app.delegate = delegate 154 | app.run() 155 | // print("done running") 156 | return (dialogResult as NSString).utf8String!; 157 | } 158 | 159 | @_cdecl("swift_set_options") 160 | public func set_options(_options: UnsafePointer) -> UnsafePointer { 161 | var result_dict: [String: Any] = [ 162 | "status" as String: "failed" 163 | ]; 164 | 165 | if let options_dict = convertOptionsToDictionary(options: _options) { 166 | if let value = options_dict["type"] as? String { 167 | if gdialog_type != value { 168 | result_dict["error"] = "gDialog type does not match."; 169 | return convertDictToCReturn(dict: result_dict); 170 | } 171 | } 172 | 173 | if let option_types = options_dict["option_types"] as? [String: String] { 174 | options_dict.forEach { option in 175 | if (options[option.key] != nil) { 176 | switch option_types[option.key] { 177 | case "string": 178 | options[option.key] = option.value as! String; 179 | case "array": 180 | options[option.key] = option.value as! [String]; 181 | case "bool-true": 182 | options[option.key] = true; 183 | case "bool-false": 184 | options[option.key] = false; 185 | case "float": 186 | options[option.key] = option.value as! CGFloat; 187 | case "int": 188 | options[option.key] = option.value as! Int; 189 | case "size": 190 | if let size_value = option.value as? [String: CGFloat] { 191 | options[option.key] = CGSize(width: size_value["width"]! as CGFloat, height: size_value["height"]! as CGFloat) 192 | } 193 | case "image": 194 | options[option.key] = NSImage(contentsOfFile: option.value as! String); 195 | default: 196 | break; 197 | } 198 | } 199 | } 200 | if (options["width"] as? CGFloat ?? 0 != 0) { 201 | options["window_size"] = CGSize(width: options["width"] as! CGFloat, height: (options["window_size"] as! CGSize).height) 202 | } 203 | if (options["height"] as? CGFloat ?? 0 != 0) { 204 | options["window_size"] = CGSize(width: (options["window_size"] as! CGSize).width, height: options["height"] as! CGFloat) 205 | } 206 | } 207 | result_dict["status"] = "success"; 208 | } 209 | return convertDictToCReturn(dict: result_dict); 210 | } 211 | 212 | 213 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 214 | func convertToDictionary(text: String) -> [String: Any]? { 215 | if let data = text.data(using: .utf8) { 216 | do { 217 | return try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] 218 | } catch {} 219 | } 220 | return nil 221 | } 222 | func convertDictToJsonString(dict: [String: Any]) -> String? { 223 | guard let theJSONData = try? JSONSerialization.data(withJSONObject: dict, options: []) else { 224 | return nil 225 | } 226 | return String(data: theJSONData, encoding: .utf8) 227 | } 228 | func convertDictToCReturn(dict: [String: Any]) -> UnsafePointer { 229 | let return_json = convertDictToJsonString(dict: dict); 230 | return (return_json! as NSString).utf8String!; 231 | } 232 | func convertOptionsToDictionary(options: UnsafePointer) -> [String: Any]? { 233 | return convertToDictionary(text: String(cString: options)); 234 | } 235 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -------------------------------------------------------------------------------- /source/index.js: -------------------------------------------------------------------------------- 1 | class gDialog { 2 | constructor() { 3 | this.application_name = "gDialog"; 4 | this.application_version = "1.0.1"; 5 | this.fs = require('fs'); 6 | this.path = require('path'); 7 | this.os = require('os'); 8 | this.is_app_bundle = false; 9 | this.root_folder = (process.hasOwnProperty('pkg') ? this.path.dirname(process.execPath) : __dirname) + ''; 10 | if (this.root_folder.endsWith(".app/Contents/MacOS")) { 11 | this.is_app_bundle = true; 12 | } 13 | this.directRun = require.main === module; 14 | this.templates_folder = this.is_app_bundle ? this.path.join(this.root_folder, '..', "Resources", "templates") : this.path.join(this.root_folder, "templates"); 15 | 16 | this.templates_config_file = this.path.join(this.templates_folder, 'config.json'); 17 | 18 | this.templates_config = {}; 19 | 20 | this.options = { 21 | "option_types": { 22 | "icon_type": "string", 23 | "async": "bool-true" 24 | } 25 | }; 26 | this.settings = { 27 | dylib_name: '.dylib' 28 | }; 29 | 30 | this.callbackFunc = undefined; 31 | this.suppressAbortErr = false; 32 | this.argv_counter = 0; 33 | this.argv = process.argv.length > 2 ? process.argv.slice(2, process.argv.length) : []; 34 | this.#readTemplatesConfiguration(); 35 | } 36 | printVersion() { 37 | console.log(this.application_version); 38 | } 39 | #findSwiftLib(libName, path) { 40 | try { 41 | path = path || this.root_folder; 42 | // console.log("Path:", path); 43 | let dirItems = fs.readdirSync(path); 44 | let folders = []; 45 | for (let i = 0; i < dirItems.length; i++) { 46 | let item = dirItems[i]; 47 | let itemPath = `${path}/${item}`; 48 | if (fs.lstatSync(itemPath).isDirectory()) { 49 | folders.push(itemPath); 50 | } 51 | else { 52 | if (item === libName) { 53 | return itemPath; 54 | } 55 | } 56 | } 57 | for (let j = 0; j < folders.length; j++) { 58 | let dirCheck = this.#findSwiftLib(libName, folders[j]); 59 | if (dirCheck !== false) { 60 | return dirCheck; 61 | } 62 | } 63 | } 64 | catch(e) { 65 | console.error(e); 66 | } 67 | return false; 68 | } 69 | #readTemplatesConfiguration() { 70 | try { 71 | if (!this.fs.existsSync(this.templates_config_file)) { 72 | console.error(`Unable to find templates configuration file at "${this.templates_config_file}"`); 73 | return false; 74 | } 75 | this.templates_config = JSON.parse(this.fs.readFileSync(this.templates_config_file, 'utf8')); 76 | // Adding additional hidden flags to the global options 77 | this.templates_config.global_options["icon_type"] = "none"; 78 | } 79 | catch (err) { 80 | console.error(err); 81 | return false; 82 | } 83 | return true; 84 | } 85 | #getOptionsSource(type, arg) { 86 | if (this.templates_config.global_options.hasOwnProperty(arg)) { 87 | return this.templates_config.global_options; 88 | } 89 | else if (this.templates_config.templates[type].options.hasOwnProperty(arg)) { 90 | return this.templates_config.templates[type].options; 91 | } 92 | else if (this.options.option_types.hasOwnProperty(arg)) { 93 | return this.options.option_types; 94 | } 95 | return; 96 | } 97 | #process_argv(arg, options) { 98 | let options_source = this.#getOptionsSource(options.type, arg); 99 | 100 | if (options_source) { 101 | try { 102 | let type_found = true; 103 | let value; 104 | switch(options_source[arg]) { 105 | case 'string': 106 | options[arg] = this.argv[++this.argv_counter]; 107 | break; 108 | case 'image': 109 | options[arg] = this.argv[++this.argv_counter]; 110 | break; 111 | case 'float': 112 | options[arg] = parseFloat(this.argv[++this.argv_counter]) 113 | break; 114 | case 'int': 115 | options[arg] = parseInt(this.argv[++this.argv_counter]) 116 | break; 117 | case 'array-inc': 118 | value = this.argv[++this.argv_counter]; 119 | if (!options[arg]) { 120 | options[arg] = []; 121 | } 122 | options[arg].push(value); 123 | break; 124 | case 'array': 125 | value = this.argv[++this.argv_counter]; 126 | if (value.indexOf('[') == 0) { 127 | options[arg] = JSON.parse(value); 128 | } 129 | else { 130 | options[arg] = [value.toString()]; 131 | } 132 | break; 133 | case 'size': 134 | value = this.argv[++this.argv_counter].toLowerCase().split('x'); 135 | options[arg] = { 136 | width: parseFloat(value[0]), 137 | height: parseFloat(value[1]) 138 | }; 139 | break; 140 | case 'bool-true': 141 | options[arg] = true; 142 | break; 143 | case 'bool-false': 144 | options[arg] = false; 145 | break; 146 | case 'none': 147 | options[arg] = arg; 148 | break; 149 | default: 150 | type_found = false; 151 | console.log(`Type not found for arg ${arg}`); 152 | break; 153 | } 154 | if (type_found) { 155 | options.option_types[arg] = options_source[arg]; 156 | } 157 | } 158 | catch(e) { 159 | console.error(`Switch Exception - Unable to find the option "${arg}."`); 160 | console.error(e); 161 | } 162 | } 163 | else { 164 | console.error(`Unable to find the option "${arg}".`); 165 | } 166 | } 167 | #sendJobToWorker(data) { 168 | return new Promise((resolve, reject) => { 169 | this.worker.on('message', (message) => { 170 | if (message.hasOwnProperty("result")) { 171 | resolve(message.result) 172 | } 173 | else { 174 | this.callbackFunc(message.message); 175 | } 176 | }); 177 | this.worker.on('error', reject); 178 | this.worker.on('close', (code) => { 179 | if (code !== 0) 180 | reject(new Error(`Worker stopped with exit code ${code}`)); 181 | }); 182 | this.worker.send(data); 183 | }); 184 | } 185 | 186 | setCallback(callbackFunc) { 187 | this.callbackFunc = callbackFunc; 188 | } 189 | close() { 190 | this.suppressAbortErr = true; 191 | this.abortController.abort(); 192 | } 193 | async run(providedOptions) { 194 | return new Promise((resolve, reject) => { 195 | try { 196 | const options = Object.assign({}, this.options); 197 | const settings = Object.assign({}, this.settings); 198 | this.#processOptions(options, settings, providedOptions); 199 | if (this.callbackFunc !== undefined) { 200 | options.async = 'true'; 201 | } 202 | this.fork = require('child_process').fork; 203 | this.abortController = new AbortController(); 204 | 205 | const { signal } = this.abortController; 206 | this.worker = this.fork(this.path.join(this.root_folder, 'gDialog_worker.js'), { signal }); 207 | 208 | this.#sendJobToWorker({ 209 | settings: { 210 | is_app_bundle: this.is_app_bundle, 211 | root_folder: this.root_folder, 212 | application_name: this.application_name, 213 | swiftLibPath: settings.swiftLibPath 214 | }, 215 | options: options 216 | }).then(result => { 217 | resolve(result); 218 | }).catch(err => { 219 | if ((err.code == "ABORT_ERR") && this.suppressAbortErr) { 220 | this.suppressAbortErr = false; 221 | resolve(); 222 | } 223 | else { 224 | reject(err); 225 | } 226 | }); 227 | 228 | } 229 | catch(e) { 230 | reject(e); 231 | } 232 | }); 233 | } 234 | runSync(providedOptions) { 235 | try { 236 | const options = Object.assign({}, this.options); 237 | const settings = Object.assign({}, this.settings); 238 | this.#processOptions(options, settings, providedOptions); 239 | const gDialog = this.is_app_bundle ? require(this.path.join(this.root_folder, '..', "Resources", `${this.application_name}`)) : require('bindings')(this.application_name); 240 | gDialog.setLibPath(settings.swiftLibPath); 241 | let gDialog_result; 242 | gDialog.setOptions(JSON.stringify(options), result => { 243 | gDialog_result = result; 244 | }); 245 | gDialog.run(result => { 246 | gDialog_result = result; 247 | }); 248 | if (!options.hasOwnProperty("no_return") || (options["no_return"] === false)) { 249 | if (gDialog_result !== "") { 250 | if (this.directRun) { 251 | console.log(gDialog_result); 252 | } 253 | else { 254 | return gDialog_result; 255 | } 256 | } 257 | } 258 | } 259 | catch(e) { 260 | console.error(e); 261 | return false; 262 | } 263 | 264 | return true; 265 | } 266 | #processOptions(options, settings, providedOptions) { 267 | // Initialization 268 | if (providedOptions) { 269 | let dialogType = providedOptions.type; 270 | for (let key in providedOptions) { 271 | options[key] = providedOptions[key]; 272 | if (key !== "type") { 273 | let option_type = this.#getOptionsSource(dialogType, key); 274 | if (option_type) { 275 | options.option_types[key] = option_type[key]; 276 | } 277 | } 278 | } 279 | } 280 | else if (this.argv.length > 0) { 281 | if (this.argv[0] == '--version' || this.argv[0] == '-v') { 282 | this.printVersion(); 283 | process.exit(0); 284 | } 285 | if (this.templates_config.templates.hasOwnProperty(this.argv[0])) { 286 | options["type"] = this.argv[0]; 287 | } 288 | else { 289 | console.error(`${this.application_name} does not have a template for type "${this.argv[0]}"`); 290 | process.exit(1); 291 | } 292 | for (this.argv_counter = 1; this.argv_counter < this.argv.length; this.argv_counter++) { 293 | let argv_value = this.argv[this.argv_counter] + ''; 294 | this.#process_argv(argv_value.indexOf('--') == 0 ? argv_value.slice(2, argv_value.length) : argv_value.indexOf('-') == 0 ? argv_value.slice(1, argv_value.length) : argv_value, options); 295 | } 296 | } 297 | else { 298 | console.error(`${this.application_name} cannot be executed without parameters`); 299 | process.exit(1); 300 | } 301 | 302 | settings.dylib_name = this.templates_config.templates[options.type].file; 303 | settings.swiftLibPath = this.fs.existsSync(this.path.join(this.templates_folder, settings.dylib_name)) ? this.path.join(this.templates_folder, settings.dylib_name) : this.#findSwiftLib(settings.dylib_name); 304 | if (settings.swiftLibPath === false) { 305 | console.error(`Unable to find template "${settings.dylib_name}"`); 306 | process.exit(2); 307 | } 308 | 309 | options["icon_type"] = "none"; 310 | if (options.hasOwnProperty("icon_file")) { 311 | if (options.icon_file.indexOf('~/') == 0) { 312 | options.icon_file = this.path.join(this.os.homedir(), options.icon_file.slice(2, options.icon_file.length)); 313 | 314 | } 315 | if (this.fs.existsSync(options.icon_file)) { 316 | options["icon_type"] = "file"; 317 | } 318 | else { 319 | delete options.icon_file; 320 | if (options.hasOwnProperty("system_icon")) { 321 | options["icon_type"] = "system"; 322 | } 323 | } 324 | } 325 | else if (options.hasOwnProperty("system_icon")) { 326 | options["icon_type"] = "system"; 327 | } 328 | } 329 | } 330 | module.exports = gDialog; 331 | 332 | const gdialog = new gDialog(); 333 | if (require.main === module) { 334 | gdialog.runSync(); 335 | } -------------------------------------------------------------------------------- /source/src/msgbox/Sources/msgbox/msgbox.swift: -------------------------------------------------------------------------------- 1 | // swift build --package-path ./src/msgbox -Xswiftc "-target" -Xswiftc "x86_64-apple-macosx10.15" -Xlinker -rpath -Xlinker @executable_path/Frameworks 2 | 3 | import AppKit 4 | import SwiftUI 5 | 6 | // Variables - Defaults 7 | var gdialog_type = "msgbox"; 8 | public typealias `func` = @convention(c) (UnsafePointer) -> Void 9 | var callbackHolder: `func`? 10 | var options: [String: Any] = [ 11 | "async": false, 12 | "header": "", 13 | "allow_quit": false, 14 | "width": 0, 15 | "height": 0, 16 | "window_size": CGSize(width: 500, height: 150), 17 | "icon_file": "", // NSImage 18 | "system_icon": "", 19 | "icon_type": "none", 20 | "title": "", 21 | "text": "", 22 | "buttons": [ "OK" ], 23 | "scrollable_text": false, 24 | "focus": false, 25 | "timeout": 0, 26 | "timeout_text": "Time Remaining", 27 | "static": false 28 | ]; 29 | 30 | 31 | /////////////////////////////// 32 | // TEMPLATE BEGIN - EDIT HERE// 33 | /////////////////////////////// 34 | 35 | // Message Box UI 36 | struct ContentView: View { 37 | var body: some View { 38 | VStack(spacing: 0) { 39 | HeaderView() 40 | BodyView() 41 | FooterView() 42 | } 43 | .padding(5) 44 | } 45 | } 46 | 47 | struct HeaderView: View { 48 | var body: some View { 49 | VStack() { 50 | Text(options["header"] as! String).font(.title) 51 | } 52 | .frame(maxWidth: .infinity) 53 | .padding(.bottom, 3) 54 | } 55 | } 56 | 57 | struct BodyView: View { 58 | var body: some View { 59 | HStack(alignment: .top) { 60 | if (options["icon_type"] as! String != "none") { 61 | IconView() 62 | } 63 | MainView() 64 | } 65 | .frame(maxHeight: .infinity) 66 | } 67 | } 68 | 69 | struct IconView: View { 70 | var body: some View { 71 | VStack() { 72 | if options["icon_type"] as! String == "file" { 73 | Image(nsImage: options["icon_file"] as! NSImage) 74 | .resizable() 75 | .aspectRatio(contentMode: .fit) 76 | .frame(maxWidth: 75) 77 | } 78 | else { 79 | Image(nsImage: NSImage(named: (options["system_icon"] as! String).starts(with: "NSImageName") ? (options["system_icon"] as! String).replacingOccurrences(of: "NSImageName", with: "NS") : options["system_icon"] as! String)!) 80 | .resizable() 81 | .aspectRatio(contentMode: .fit) 82 | .frame(width: 75) 83 | 84 | } 85 | } 86 | .frame(maxHeight: .infinity) 87 | } 88 | } 89 | 90 | struct MainView: View { 91 | var body: some View { 92 | VStack { 93 | if (options["scrollable_text"] as! Bool) { 94 | ScrollView { 95 | TextView() 96 | } 97 | } 98 | else { 99 | TextView() 100 | } 101 | } 102 | .frame(maxHeight: .infinity, alignment: .top) 103 | .padding(.leading, 5) 104 | } 105 | } 106 | 107 | 108 | struct TextView: View { 109 | var body: some View { 110 | VStack(alignment: .leading) { 111 | Text(options["text"] as! String) 112 | .font(.body) 113 | .frame(maxWidth: .infinity, alignment: .leading) 114 | .multilineTextAlignment(.leading) 115 | } 116 | .frame(maxHeight: .infinity, alignment: .top) 117 | } 118 | } 119 | 120 | struct FooterView: View { 121 | var body: some View { 122 | HStack() { 123 | if (options["timeout"] as! Int > 0) { 124 | TimerView() 125 | } 126 | ButtonsView() 127 | } 128 | } 129 | } 130 | 131 | struct TimerView: View { 132 | @State private var timeRemaining = options["timeout"] as! Int; 133 | let timer = Timer.publish(every: 1, on: .current, in: .common).autoconnect() 134 | var body: some View { 135 | HStack() { 136 | Text("\(options["timeout_text"] as! String): \(buildRemainingTime(timeRemaining))") 137 | } 138 | .padding(.leading, 5) 139 | .onReceive(timer) { time in 140 | if self.timeRemaining > 0 { 141 | self.timeRemaining -= 1 142 | } 143 | else { 144 | timer.upstream.connect().cancel() 145 | dialogResult = ""; 146 | dialogInteraction("timeout"); 147 | let dummyEvent = NSEvent.otherEvent(with: NSEvent.EventType.applicationDefined, location: NSZeroPoint, modifierFlags: NSEvent.ModifierFlags(rawValue: 0), timestamp: 0, windowNumber: 0, context: nil, subtype:0, data1:0, data2:0)!; 148 | NSApp.postEvent(dummyEvent, atStart: true); 149 | 150 | // } 151 | // print("timeout") 152 | // app.stop("timeout"); 153 | // NSApplication.shared.terminate(0) 154 | } 155 | } 156 | } 157 | } 158 | 159 | func buildRemainingTime(_ time: Int) -> String { 160 | var remainingTime: Int = time; 161 | var days = 0; 162 | var hours = 0; 163 | var minutes = 0; 164 | var seconds = 0; 165 | 166 | if (remainingTime > 59) { 167 | seconds = remainingTime % 60; 168 | remainingTime = remainingTime / 60; 169 | if (remainingTime > 59) { 170 | minutes = remainingTime % 60; 171 | remainingTime = remainingTime / 60; 172 | if (remainingTime > 23) { 173 | hours = remainingTime % 24; 174 | days = remainingTime / 24; 175 | } 176 | else { 177 | hours = remainingTime; 178 | } 179 | } 180 | else { 181 | minutes = remainingTime; 182 | } 183 | } 184 | else { 185 | seconds = remainingTime; 186 | } 187 | var result = days > 0 ? String(days) + " " : ""; 188 | result += (hours < 10 ? "0" : "") + String(hours); 189 | result += ":" + (minutes < 10 ? "0" : "") + String(minutes); 190 | result += ":" + (seconds < 10 ? "0" : "") + String(seconds); 191 | 192 | return result; 193 | } 194 | 195 | var buttonCount: Int = 0; 196 | func increaseButtonCount() -> Int { 197 | buttonCount += 1; 198 | return buttonCount; 199 | } 200 | struct ButtonsView : View { 201 | var body: some View { 202 | HStack { 203 | ForEach(options["buttons"] as! [String], id: \.self) { 204 | ButtonView( 205 | labelText: "\($0)", 206 | actionReturn: String(increaseButtonCount()) 207 | ) 208 | } 209 | } 210 | .frame(maxWidth: .infinity, alignment: .leading) 211 | .environment(\.layoutDirection, .rightToLeft) 212 | .padding(.top, 3) 213 | } 214 | } 215 | struct ButtonView: View { 216 | var labelText: String = "Button"; 217 | var actionReturn: String = "0"; 218 | 219 | var body: some View { 220 | HStack { 221 | Button(action: buttonClicked, label: { 222 | Text(labelText) 223 | .frame(minWidth: 50) 224 | }) 225 | .padding(.horizontal, 5) 226 | .padding(.vertical, 2) 227 | } 228 | 229 | } 230 | func buttonClicked() { 231 | dialogInteraction(actionReturn); 232 | // app.stop("button"); 233 | } 234 | } 235 | 236 | /////////////////////// 237 | // DO NOT EDIT BELOW // 238 | /////////////////////// 239 | 240 | func dialogInteraction(_ returnString: String) { 241 | if ((options["async"] as! Bool) && (returnString != "timeout")) { 242 | callbackHolder!((returnString as NSString).utf8String!); 243 | } 244 | else { 245 | dialogResult = returnString; 246 | app.stop(""); 247 | } 248 | } 249 | 250 | let app = NSApplication.shared 251 | let delegate = AppDelegate() 252 | var dialogResult = "-1"; 253 | 254 | class WindowDelegate: NSObject, NSWindowDelegate { 255 | func windowWillClose(_ notification: Notification) { 256 | // print("windowWillClose") 257 | NSApplication.shared.terminate(0) 258 | // app.stop("") 259 | } 260 | } 261 | 262 | class CustomWindow: NSWindow { 263 | override var canBecomeKey: Bool { 264 | return true 265 | } 266 | } 267 | 268 | class AppDelegate: NSObject, NSApplicationDelegate { 269 | let window = CustomWindow() 270 | let windowDelegate = WindowDelegate() 271 | 272 | func applicationDidFinishLaunching(_ notification: Notification) { 273 | if (options["allow_quit"] as! Bool) { 274 | let appMenu = NSMenuItem() 275 | appMenu.submenu = NSMenu() 276 | appMenu.submenu?.addItem(NSMenuItem(title: "Quit", action: #selector(app.stop(_:)), keyEquivalent: "q")) 277 | let mainMenu = NSMenu(title: options["title"] as! String) 278 | mainMenu.addItem(appMenu) 279 | NSApplication.shared.mainMenu = mainMenu 280 | } 281 | window.setContentSize(options["window_size"] as! CGSize) 282 | window.delegate = windowDelegate 283 | if (options["title"] as? String ?? "" == "") { 284 | window.styleMask = [] 285 | } 286 | else { 287 | window.styleMask = [.titled] 288 | window.title = options["title"] as! String 289 | } 290 | 291 | let view = NSHostingView(rootView: ContentView()) 292 | view.frame = CGRect(origin: .zero, size: options["window_size"] as! CGSize) 293 | view.autoresizingMask = [.height, .width] 294 | window.contentView!.addSubview(view) 295 | if (options["static"] as! Bool) { 296 | window.isMovableByWindowBackground = false 297 | window.isMovable = false 298 | } 299 | else { 300 | window.isMovableByWindowBackground = true 301 | window.isMovable = true 302 | } 303 | window.center() 304 | window.makeKeyAndOrderFront(window) 305 | 306 | NSApp.setActivationPolicy(.accessory) 307 | if (options["focus"] as! Bool) { 308 | NSApp.activate(ignoringOtherApps: true) // Makes the window take focus 309 | } 310 | } 311 | } 312 | 313 | 314 | 315 | 316 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 317 | 318 | 319 | @_cdecl("swift_run_dialog") 320 | public func run_dialog(callBack:@escaping `func`) -> UnsafePointer { 321 | callbackHolder = callBack; 322 | let delegate = AppDelegate() 323 | app.delegate = delegate 324 | app.run() 325 | return (dialogResult as NSString).utf8String!; 326 | } 327 | 328 | @_cdecl("swift_set_options") 329 | public func set_options(_options: UnsafePointer) -> UnsafePointer { 330 | var result_dict: [String: Any] = [ 331 | "status" as String: "failed" 332 | ]; 333 | 334 | if let options_dict = convertOptionsToDictionary(options: _options) { 335 | if let value = options_dict["type"] as? String { 336 | if gdialog_type != value { 337 | result_dict["error"] = "gDialog type does not match."; 338 | return convertDictToCReturn(dict: result_dict); 339 | } 340 | } 341 | 342 | 343 | if let option_types = options_dict["option_types"] as? [String: String] { 344 | options_dict.forEach { option in 345 | if (options[option.key] != nil) { 346 | switch option_types[option.key] { 347 | case "string": 348 | options[option.key] = option.value as! String; 349 | case "array": 350 | options[option.key] = option.value as! [String]; 351 | case "bool-true": 352 | options[option.key] = true; 353 | case "bool-false": 354 | options[option.key] = false; 355 | case "float": 356 | options[option.key] = option.value as! CGFloat; 357 | case "int": 358 | options[option.key] = option.value as! Int; 359 | case "size": 360 | if let size_value = option.value as? [String: CGFloat] { 361 | options[option.key] = CGSize(width: size_value["width"]! as CGFloat, height: size_value["height"]! as CGFloat) 362 | } 363 | case "image": 364 | options[option.key] = NSImage(contentsOfFile: option.value as! String); 365 | default: 366 | break; 367 | } 368 | } 369 | } 370 | if (options["width"] as? CGFloat ?? 0 != 0) { 371 | options["window_size"] = CGSize(width: options["width"] as! CGFloat, height: (options["window_size"] as! CGSize).height) 372 | } 373 | if (options["height"] as? CGFloat ?? 0 != 0) { 374 | options["window_size"] = CGSize(width: (options["window_size"] as! CGSize).width, height: options["height"] as! CGFloat) 375 | } 376 | } 377 | result_dict["status"] = "success"; 378 | } 379 | 380 | return convertDictToCReturn(dict: result_dict); 381 | } 382 | 383 | 384 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 385 | func convertToDictionary(text: String) -> [String: Any]? { 386 | if let data = text.data(using: .utf8) { 387 | do { 388 | return try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] 389 | } catch {} 390 | } 391 | return nil 392 | } 393 | func convertDictToJsonString(dict: [String: Any]) -> String? { 394 | guard let theJSONData = try? JSONSerialization.data(withJSONObject: dict, options: []) else { 395 | return nil 396 | } 397 | return String(data: theJSONData, encoding: .utf8) 398 | } 399 | func convertDictToCReturn(dict: [String: Any]) -> UnsafePointer { 400 | let return_json = convertDictToJsonString(dict: dict); 401 | return (return_json! as NSString).utf8String!; 402 | } 403 | func convertOptionsToDictionary(options: UnsafePointer) -> [String: Any]? { 404 | return convertToDictionary(text: String(cString: options)); 405 | } 406 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -------------------------------------------------------------------------------- /source/src/progressbar/Sources/progressbar/progressbar.swift: -------------------------------------------------------------------------------- 1 | // swift build --package-path ./src/progressbar -Xswiftc "-target" -Xswiftc "x86_64-apple-macosx10.15" -Xlinker -rpath -Xlinker @executable_path/Frameworks 2 | 3 | import AppKit 4 | import SwiftUI 5 | 6 | 7 | // Variables - Defaults 8 | var gdialog_type = "progressbar"; 9 | public typealias `func` = @convention(c) (UnsafePointer) -> Void 10 | var callbackHolder: `func`? 11 | var options: [String: Any] = [ 12 | "async": false, 13 | "stoppable": false, 14 | "indeterminate": false, 15 | "allow_quit": false, 16 | "width": 0, 17 | "height": 0, 18 | "window_size": CGSize(width: 500, height: 100), 19 | "icon_file": "", // NSImage 20 | "system_icon": "", 21 | "icon_type": "none", 22 | "title": "", 23 | "text": "", 24 | "focus": false, 25 | "timeout": 0, 26 | "timeout_text": "Time Remaining", 27 | "static": false 28 | ]; 29 | var input_progress: Double = 0.0; 30 | var input_progress_text: String = ""; 31 | 32 | // Progress Bar UI 33 | struct ContentView: View { 34 | var body: some View { 35 | HStack(alignment: .center) { 36 | if (options["icon_type"] as! String != "none") { 37 | IconView() 38 | } 39 | MainView() 40 | if (options["stoppable"] as! Bool) { 41 | StopButtonView() 42 | } 43 | } 44 | .frame(maxHeight: .infinity) 45 | .padding(5) 46 | } 47 | } 48 | 49 | struct IconView: View { 50 | var body: some View { 51 | VStack() { 52 | if options["icon_type"] as! String == "file" { 53 | Image(nsImage: options["icon_file"] as! NSImage) 54 | .resizable() 55 | .aspectRatio(contentMode: .fit) 56 | .frame(maxWidth: 75) 57 | } 58 | else { 59 | Image(nsImage: NSImage(named: (options["system_icon"] as! String).starts(with: "NSImageName") ? (options["system_icon"] as! String).replacingOccurrences(of: "NSImageName", with: "NS") : options["system_icon"] as! String)!) 60 | .resizable() 61 | .aspectRatio(contentMode: .fit) 62 | .frame(width: 75) 63 | 64 | } 65 | } 66 | .frame(maxHeight: .infinity) 67 | } 68 | } 69 | 70 | struct MainView: View { 71 | var body: some View { 72 | VStack { 73 | ProgressBarView() 74 | if (options["timeout"] as! Int > 0) { 75 | TimerView() 76 | } 77 | } 78 | .padding(.leading, 5) 79 | } 80 | } 81 | 82 | struct TimerView: View { 83 | @State private var timeRemaining = options["timeout"] as! Int; 84 | let timer = Timer.publish(every: 1, on: .current, in: .common).autoconnect() 85 | var body: some View { 86 | HStack() { 87 | Text("\(options["timeout_text"] as! String): \(buildRemainingTime(timeRemaining))") 88 | } 89 | .padding(.leading, 5) 90 | .onReceive(timer) { time in 91 | if self.timeRemaining > 0 { 92 | self.timeRemaining -= 1 93 | } 94 | else { 95 | timer.upstream.connect().cancel() 96 | dialogResult = ""; 97 | dialogInteraction("timeout"); 98 | let dummyEvent = NSEvent.otherEvent(with: NSEvent.EventType.applicationDefined, location: NSZeroPoint, modifierFlags: NSEvent.ModifierFlags(rawValue: 0), timestamp: 0, windowNumber: 0, context: nil, subtype:0, data1:0, data2:0)!; 99 | NSApp.postEvent(dummyEvent, atStart: true); 100 | } 101 | } 102 | } 103 | } 104 | 105 | func buildRemainingTime(_ time: Int) -> String { 106 | var remainingTime: Int = time; 107 | var days = 0; 108 | var hours = 0; 109 | var minutes = 0; 110 | var seconds = 0; 111 | 112 | if (remainingTime > 59) { 113 | seconds = remainingTime % 60; 114 | remainingTime = remainingTime / 60; 115 | if (remainingTime > 59) { 116 | minutes = remainingTime % 60; 117 | remainingTime = remainingTime / 60; 118 | if (remainingTime > 23) { 119 | hours = remainingTime % 24; 120 | days = remainingTime / 24; 121 | } 122 | else { 123 | hours = remainingTime; 124 | } 125 | } 126 | else { 127 | minutes = remainingTime; 128 | } 129 | } 130 | else { 131 | seconds = remainingTime; 132 | } 133 | var result = days > 0 ? String(days) + " " : ""; 134 | result += (hours < 10 ? "0" : "") + String(hours); 135 | result += ":" + (minutes < 10 ? "0" : "") + String(minutes); 136 | result += ":" + (seconds < 10 ? "0" : "") + String(seconds); 137 | 138 | return result; 139 | } 140 | 141 | struct StopButtonView: View { 142 | var body: some View { 143 | HStack { 144 | Button(action: buttonClicked, label: { 145 | Text("Stop") 146 | .frame(minWidth: 50) 147 | }) 148 | .padding(.horizontal, 5) 149 | .padding(.vertical, 2) 150 | } 151 | 152 | } 153 | func buttonClicked() { 154 | dialogInteraction("1"); 155 | // app.stop("button"); 156 | } 157 | } 158 | 159 | struct ProgressBarView: View { 160 | @State var progress: Double = 0.0 161 | @State var progress_text: String = options["text"] as? String ?? "" 162 | 163 | let timer = Timer.publish(every: 0.5, on: .main, in: .common).autoconnect() 164 | var body: some View { 165 | VStack(spacing: 0) { 166 | Text(progress_text) 167 | .frame(maxWidth: .infinity, alignment: .leading) 168 | ProgressIndicator(progress: $progress, minValue: 0.0, maxValue: 100.0) 169 | } 170 | .onReceive(timer) { _ in 171 | if (options["indeterminate"] as! Bool) { 172 | if progress_text != input_progress_text { 173 | progress_text = input_progress_text 174 | } 175 | } 176 | else { 177 | if progress != input_progress { 178 | progress = input_progress; 179 | if progress_text != input_progress_text { 180 | progress_text = input_progress_text 181 | } 182 | } 183 | } 184 | } 185 | } 186 | } 187 | 188 | struct ProgressIndicator: NSViewRepresentable { 189 | @Binding var progress: Double 190 | var minValue: Double 191 | var maxValue: Double 192 | func makeNSView(context: NSViewRepresentableContext) -> NSProgressIndicator { 193 | let result = NSProgressIndicator() 194 | if (options["indeterminate"] as! Bool) { 195 | result.isIndeterminate = true 196 | } 197 | else { 198 | result.isIndeterminate = false 199 | } 200 | result.minValue = minValue; 201 | result.maxValue = maxValue; 202 | result.startAnimation(nil) 203 | return result 204 | } 205 | 206 | func updateNSView(_ nsView: NSProgressIndicator, context: NSViewRepresentableContext) { 207 | if (progress != nsView.doubleValue) { 208 | nsView.doubleValue = progress; 209 | } 210 | } 211 | } 212 | 213 | 214 | func checkProgress() -> Void { 215 | input_progress_text = options["text"] as? String ?? "" 216 | DispatchQueue(label: "background").async { 217 | var str = "" 218 | repeat { 219 | str = readLine() ?? "" 220 | if (str != "EOF" && str != "") { 221 | if (options["indeterminate"] as! Bool) { 222 | input_progress_text = str; 223 | } 224 | else { 225 | let input_arr = str.split(separator: " ", maxSplits: 1) 226 | if let new_prog = Double(input_arr[0]) { 227 | if new_prog != input_progress { 228 | input_progress = new_prog 229 | if input_arr.count == 2 { 230 | input_progress_text = String(input_arr[1]) 231 | } 232 | } 233 | } 234 | } 235 | } 236 | } while str != "EOF" 237 | NSApplication.shared.terminate(0) 238 | } 239 | 240 | } 241 | 242 | /////////////////////// 243 | // DO NOT EDIT BELOW // 244 | /////////////////////// 245 | 246 | let app = NSApplication.shared 247 | let delegate = AppDelegate() 248 | var dialogResult = "-1"; 249 | 250 | class WindowDelegate: NSObject, NSWindowDelegate { 251 | func windowWillClose(_ notification: Notification) { 252 | NSApplication.shared.terminate(0) 253 | } 254 | } 255 | 256 | class CustomWindow: NSWindow { 257 | override var canBecomeKey: Bool { 258 | return true 259 | } 260 | } 261 | 262 | class AppDelegate: NSObject, NSApplicationDelegate { 263 | let window = CustomWindow() 264 | let windowDelegate = WindowDelegate() 265 | 266 | func applicationDidFinishLaunching(_ notification: Notification) { 267 | if (options["allow_quit"] as! Bool) { 268 | let appMenu = NSMenuItem() 269 | appMenu.submenu = NSMenu() 270 | appMenu.submenu?.addItem(NSMenuItem(title: "Quit", action: #selector(app.stop(_:)), keyEquivalent: "q")) 271 | let mainMenu = NSMenu(title: options["title"] as! String) 272 | mainMenu.addItem(appMenu) 273 | NSApplication.shared.mainMenu = mainMenu 274 | } 275 | window.setContentSize(options["window_size"] as! CGSize) 276 | window.delegate = windowDelegate 277 | if (options["title"] as? String ?? "" == "") { 278 | window.styleMask = [] 279 | } 280 | else { 281 | window.styleMask = [.titled] 282 | window.title = options["title"] as! String 283 | } 284 | 285 | let view = NSHostingView(rootView: ContentView()) 286 | view.frame = CGRect(origin: .zero, size: options["window_size"] as! CGSize) 287 | view.autoresizingMask = [.height, .width] 288 | window.contentView!.addSubview(view) 289 | if (options["static"] as! Bool) { 290 | window.isMovableByWindowBackground = false 291 | window.isMovable = false 292 | } 293 | else { 294 | window.isMovableByWindowBackground = true 295 | window.isMovable = true 296 | } 297 | window.center() 298 | window.makeKeyAndOrderFront(window) 299 | 300 | NSApp.setActivationPolicy(.accessory) 301 | if (options["focus"] as! Bool) { 302 | NSApp.activate(ignoringOtherApps: true) // Makes the window take focus 303 | } 304 | checkProgress() 305 | } 306 | } 307 | 308 | 309 | func dialogInteraction(_ returnString: String) { 310 | if ((options["async"] as! Bool) && (returnString != "timeout")) { 311 | callbackHolder!((returnString as NSString).utf8String!); 312 | } 313 | else { 314 | dialogResult = returnString; 315 | app.stop(""); 316 | } 317 | } 318 | 319 | 320 | @_cdecl("swift_run_dialog") 321 | public func run_dialog(callBack:@escaping `func`) -> UnsafePointer { 322 | callbackHolder = callBack; 323 | let delegate = AppDelegate() 324 | app.delegate = delegate 325 | app.run() 326 | return (dialogResult as NSString).utf8String!; 327 | } 328 | 329 | @_cdecl("swift_set_options") 330 | public func set_options(_options: UnsafePointer) -> UnsafePointer { 331 | var result_dict: [String: Any] = [ 332 | "status" as String: "failed" 333 | ]; 334 | 335 | if let options_dict = convertOptionsToDictionary(options: _options) { 336 | if let value = options_dict["type"] as? String { 337 | if gdialog_type != value { 338 | result_dict["error"] = "gDialog type does not match."; 339 | return convertDictToCReturn(dict: result_dict); 340 | } 341 | } 342 | 343 | 344 | if let option_types = options_dict["option_types"] as? [String: String] { 345 | options_dict.forEach { option in 346 | if (options[option.key] != nil) { 347 | switch option_types[option.key] { 348 | case "string": 349 | options[option.key] = option.value as! String; 350 | case "array": 351 | options[option.key] = option.value as! [String]; 352 | case "bool-true": 353 | options[option.key] = true; 354 | case "bool-false": 355 | options[option.key] = false; 356 | case "float": 357 | options[option.key] = option.value as! CGFloat; 358 | case "int": 359 | options[option.key] = option.value as! Int; 360 | case "size": 361 | if let size_value = option.value as? [String: CGFloat] { 362 | options[option.key] = CGSize(width: size_value["width"]! as CGFloat, height: size_value["height"]! as CGFloat) 363 | } 364 | case "image": 365 | options[option.key] = NSImage(contentsOfFile: option.value as! String); 366 | default: 367 | break; 368 | } 369 | } 370 | } 371 | if (options["width"] as? CGFloat ?? 0 != 0) { 372 | options["window_size"] = CGSize(width: options["width"] as! CGFloat, height: (options["window_size"] as! CGSize).height) 373 | } 374 | if (options["height"] as? CGFloat ?? 0 != 0) { 375 | options["window_size"] = CGSize(width: (options["window_size"] as! CGSize).width, height: options["height"] as! CGFloat) 376 | } 377 | } 378 | result_dict["status"] = "success"; 379 | } 380 | 381 | return convertDictToCReturn(dict: result_dict); 382 | } 383 | 384 | 385 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 386 | func convertToDictionary(text: String) -> [String: Any]? { 387 | if let data = text.data(using: .utf8) { 388 | do { 389 | return try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] 390 | } catch {} 391 | } 392 | return nil 393 | } 394 | func convertDictToJsonString(dict: [String: Any]) -> String? { 395 | guard let theJSONData = try? JSONSerialization.data(withJSONObject: dict, options: []) else { 396 | return nil 397 | } 398 | return String(data: theJSONData, encoding: .utf8) 399 | } 400 | func convertDictToCReturn(dict: [String: Any]) -> UnsafePointer { 401 | let return_json = convertDictToJsonString(dict: dict); 402 | return (return_json! as NSString).utf8String!; 403 | } 404 | func convertOptionsToDictionary(options: UnsafePointer) -> [String: Any]? { 405 | return convertToDictionary(text: String(cString: options)); 406 | } 407 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -------------------------------------------------------------------------------- /source/src/inputbox/Sources/inputbox/inputbox.swift: -------------------------------------------------------------------------------- 1 | // swift build --package-path ./src/inputbox -Xswiftc "-target" -Xswiftc "x86_64-apple-macosx10.15" -Xlinker -rpath -Xlinker @executable_path/Frameworks 2 | 3 | import AppKit 4 | import SwiftUI 5 | 6 | 7 | // Variables - Defaults 8 | var gdialog_type = "inputbox"; 9 | public typealias `func` = @convention(c) (UnsafePointer) -> Void 10 | var callbackHolder: `func`? 11 | var options: [String: Any] = [ 12 | "async": false, 13 | "initial_text": "", 14 | "background_text": "", 15 | "encode_text": false, 16 | "header": "", 17 | "allow_quit": false, 18 | "width": 0, 19 | "height": 0, 20 | "window_size": CGSize(width: 500, height: 150), 21 | "icon_file": "", // NSImage 22 | "system_icon": "", 23 | "icon_type": "none", 24 | "title": "", 25 | "text": "", 26 | "buttons": [ "OK" ], 27 | "scrollable_text": false, 28 | "focus": false, 29 | "timeout": 0, 30 | "timeout_text": "Time Remaining", 31 | "static": false 32 | ]; 33 | 34 | // Input Box UI 35 | struct ContentView: View { 36 | @State private var textField_text: String = options["initial_text"] as! String; 37 | var body: some View { 38 | VStack(spacing: 0) { 39 | HeaderView() 40 | BodyView(textField_text: $textField_text) 41 | FooterView(textField_text: $textField_text) 42 | } 43 | .padding(5) 44 | } 45 | } 46 | 47 | struct HeaderView: View { 48 | var body: some View { 49 | VStack() { 50 | Text(options["header"] as! String).font(.title) 51 | } 52 | .frame(maxWidth: .infinity) 53 | .padding(.bottom, 3) 54 | } 55 | } 56 | 57 | struct BodyView: View { 58 | @Binding var textField_text: String; 59 | var body: some View { 60 | HStack(alignment: .top) { 61 | if (options["icon_type"] as! String != "none") { 62 | IconView() 63 | } 64 | MainView(textField_text: $textField_text) 65 | } 66 | .frame(maxHeight: .infinity) 67 | } 68 | } 69 | 70 | struct IconView: View { 71 | var body: some View { 72 | VStack() { 73 | if options["icon_type"] as! String == "file" { 74 | Image(nsImage: options["icon_file"] as! NSImage) 75 | .resizable() 76 | .aspectRatio(contentMode: .fit) 77 | .frame(maxWidth: 75) 78 | } 79 | else { 80 | Image(nsImage: NSImage(named: (options["system_icon"] as! String).starts(with: "NSImageName") ? (options["system_icon"] as! String).replacingOccurrences(of: "NSImageName", with: "NS") : options["system_icon"] as! String)!) 81 | .resizable() 82 | .aspectRatio(contentMode: .fit) 83 | .frame(width: 75) 84 | 85 | } 86 | } 87 | .frame(maxHeight: .infinity) 88 | } 89 | } 90 | 91 | struct MainView: View { 92 | @Binding var textField_text: String; 93 | var body: some View { 94 | VStack { 95 | if (options["scrollable_text"] as! Bool) { 96 | ScrollView { 97 | TextView() 98 | } 99 | } 100 | else { 101 | TextView() 102 | } 103 | TextFieldView(textField_text: $textField_text) 104 | } 105 | .frame(maxHeight: .infinity, alignment: .top) 106 | .padding(.leading, 5) 107 | } 108 | } 109 | 110 | 111 | struct TextView: View { 112 | var body: some View { 113 | VStack(alignment: .leading) { 114 | Text(options["text"] as! String) 115 | .font(.body) 116 | .frame(maxWidth: .infinity, alignment: .leading) 117 | .multilineTextAlignment(.leading) 118 | } 119 | } 120 | } 121 | 122 | struct TextFieldView: View { 123 | @Binding var textField_text: String; 124 | 125 | var body: some View { 126 | VStack() { 127 | TextField(options["background_text"] as! String, text: $textField_text) 128 | .disableAutocorrection(true) 129 | } 130 | .textFieldStyle(RoundedBorderTextFieldStyle()) 131 | } 132 | } 133 | 134 | struct FooterView: View { 135 | @Binding var textField_text: String; 136 | var body: some View { 137 | HStack() { 138 | if (options["timeout"] as! Int > 0) { 139 | TimerView() 140 | } 141 | ButtonsView(textField_text: $textField_text) 142 | } 143 | } 144 | } 145 | 146 | struct TimerView: View { 147 | @State private var timeRemaining = options["timeout"] as! Int; 148 | let timer = Timer.publish(every: 1, on: .current, in: .common).autoconnect() 149 | var body: some View { 150 | HStack() { 151 | Text("\(options["timeout_text"] as! String): \(buildRemainingTime(timeRemaining))") 152 | } 153 | .padding(.leading, 5) 154 | .onReceive(timer) { time in 155 | if self.timeRemaining > 0 { 156 | self.timeRemaining -= 1 157 | } 158 | else { 159 | timer.upstream.connect().cancel() 160 | dialogResult = ""; 161 | dialogInteraction("timeout"); 162 | let dummyEvent = NSEvent.otherEvent(with: NSEvent.EventType.applicationDefined, location: NSZeroPoint, modifierFlags: NSEvent.ModifierFlags(rawValue: 0), timestamp: 0, windowNumber: 0, context: nil, subtype:0, data1:0, data2:0)!; 163 | NSApp.postEvent(dummyEvent, atStart: true); 164 | } 165 | } 166 | } 167 | } 168 | 169 | func buildRemainingTime(_ time: Int) -> String { 170 | var remainingTime: Int = time; 171 | var days = 0; 172 | var hours = 0; 173 | var minutes = 0; 174 | var seconds = 0; 175 | 176 | if (remainingTime > 59) { 177 | seconds = remainingTime % 60; 178 | remainingTime = remainingTime / 60; 179 | if (remainingTime > 59) { 180 | minutes = remainingTime % 60; 181 | remainingTime = remainingTime / 60; 182 | if (remainingTime > 23) { 183 | hours = remainingTime % 24; 184 | days = remainingTime / 24; 185 | } 186 | else { 187 | hours = remainingTime; 188 | } 189 | } 190 | else { 191 | minutes = remainingTime; 192 | } 193 | } 194 | else { 195 | seconds = remainingTime; 196 | } 197 | var result = days > 0 ? String(days) + " " : ""; 198 | result += (hours < 10 ? "0" : "") + String(hours); 199 | result += ":" + (minutes < 10 ? "0" : "") + String(minutes); 200 | result += ":" + (seconds < 10 ? "0" : "") + String(seconds); 201 | 202 | return result; 203 | } 204 | 205 | var buttonCount: Int = 0; 206 | func increaseButtonCount() -> Int { 207 | buttonCount += 1; 208 | return buttonCount; 209 | } 210 | struct ButtonsView : View { 211 | @Binding var textField_text: String; 212 | var body: some View { 213 | HStack { 214 | ForEach(options["buttons"] as! [String], id: \.self) { 215 | ButtonView( 216 | textField_text: $textField_text, 217 | labelText: "\($0)", 218 | actionReturn: String(increaseButtonCount()) 219 | ) 220 | } 221 | } 222 | .frame(maxWidth: .infinity, alignment: .leading) 223 | .environment(\.layoutDirection, .rightToLeft) 224 | .padding(.top, 3) 225 | } 226 | } 227 | struct ButtonView: View { 228 | @Binding var textField_text: String; 229 | var labelText: String = "Button"; 230 | var actionReturn: String = "0"; 231 | 232 | var body: some View { 233 | HStack { 234 | Button(action: buttonClicked, label: { 235 | Text(labelText) 236 | .frame(minWidth: 50) 237 | }) 238 | .padding(.horizontal, 5) 239 | .padding(.vertical, 2) 240 | } 241 | 242 | } 243 | 244 | func buttonClicked() { 245 | if options["encode_text"] as! Bool { 246 | dialogInteraction(actionReturn + "\n" + Data(textField_text.utf8).base64EncodedString()); 247 | } 248 | else { 249 | dialogInteraction(actionReturn + "\n" + textField_text); 250 | } 251 | // app.stop("button"); 252 | } 253 | } 254 | 255 | /////////////////////// 256 | // DO NOT EDIT BELOW // 257 | /////////////////////// 258 | 259 | func dialogInteraction(_ returnString: String) { 260 | if ((options["async"] as! Bool) && (returnString != "timeout")) { 261 | callbackHolder!((returnString as NSString).utf8String!); 262 | } 263 | else { 264 | dialogResult = returnString; 265 | app.stop(""); 266 | } 267 | } 268 | 269 | let app = NSApplication.shared 270 | let delegate = AppDelegate() 271 | var dialogResult = "-1"; 272 | 273 | class WindowDelegate: NSObject, NSWindowDelegate { 274 | func windowWillClose(_ notification: Notification) { 275 | NSApplication.shared.terminate(0) 276 | } 277 | } 278 | 279 | class CustomWindow: NSWindow { 280 | override var canBecomeKey: Bool { 281 | return true 282 | } 283 | } 284 | 285 | class AppDelegate: NSObject, NSApplicationDelegate { 286 | let window = CustomWindow() 287 | let windowDelegate = WindowDelegate() 288 | 289 | func applicationDidFinishLaunching(_ notification: Notification) { 290 | let mainMenu = NSMenu(title: options["title"] as! String) 291 | if (options["allow_quit"] as! Bool) { 292 | let appMenu = NSMenuItem() 293 | appMenu.submenu = NSMenu() 294 | appMenu.submenu?.addItem(NSMenuItem(title: "Quit", action: #selector(app.stop(_:)), keyEquivalent: "q")) 295 | mainMenu.addItem(appMenu) 296 | } 297 | let editMenu = NSMenuItem() 298 | editMenu.submenu = NSMenu(title: "Edit") 299 | editMenu.submenu?.addItem(NSMenuItem(title: "Undo", action: Selector(("undo:")), keyEquivalent: "z")) 300 | editMenu.submenu?.addItem(NSMenuItem(title: "Redo", action: Selector(("redo:")), keyEquivalent: "y")) 301 | editMenu.submenu?.addItem(NSMenuItem.separator()) 302 | editMenu.submenu?.addItem(NSMenuItem(title: "Cut", action: #selector(NSText.cut(_:)), keyEquivalent: "x")) 303 | editMenu.submenu?.addItem(NSMenuItem(title: "Copy", action: #selector(NSText.copy(_:)), keyEquivalent: "c")) 304 | editMenu.submenu?.addItem(NSMenuItem(title: "Paste", action: #selector(NSText.paste(_:)), keyEquivalent: "v")) 305 | editMenu.submenu?.addItem(NSMenuItem(title: "Select All", action: #selector(NSStandardKeyBindingResponding.selectAll(_:)), keyEquivalent: "a")) 306 | mainMenu.addItem(editMenu) 307 | NSApplication.shared.mainMenu = mainMenu 308 | 309 | window.setContentSize(options["window_size"] as! CGSize) 310 | window.delegate = windowDelegate 311 | if (options["title"] as? String ?? "" == "") { 312 | window.styleMask = [] 313 | } 314 | else { 315 | window.styleMask = [.titled] 316 | window.title = options["title"] as! String 317 | } 318 | 319 | let view = NSHostingView(rootView: ContentView()) 320 | view.frame = CGRect(origin: .zero, size: options["window_size"] as! CGSize) 321 | view.autoresizingMask = [.height, .width] 322 | window.contentView!.addSubview(view) 323 | if (options["static"] as! Bool) { 324 | window.isMovableByWindowBackground = false 325 | window.isMovable = false 326 | } 327 | else { 328 | window.isMovableByWindowBackground = true 329 | window.isMovable = true 330 | } 331 | window.center() 332 | window.makeKeyAndOrderFront(window) 333 | 334 | NSApp.setActivationPolicy(.accessory) 335 | if (options["focus"] as! Bool) { 336 | NSApp.activate(ignoringOtherApps: true) // Makes the window take focus 337 | } 338 | } 339 | } 340 | 341 | @_cdecl("swift_run_dialog") 342 | public func run_dialog(callBack:@escaping `func`) -> UnsafePointer { 343 | callbackHolder = callBack; 344 | let delegate = AppDelegate() 345 | app.delegate = delegate 346 | app.run() 347 | return (dialogResult as NSString).utf8String!; 348 | } 349 | 350 | @_cdecl("swift_set_options") 351 | public func set_options(_options: UnsafePointer) -> UnsafePointer { 352 | var result_dict: [String: Any] = [ 353 | "status" as String: "failed" 354 | ]; 355 | 356 | if let options_dict = convertOptionsToDictionary(options: _options) { 357 | if let value = options_dict["type"] as? String { 358 | if gdialog_type != value { 359 | result_dict["error"] = "gDialog type does not match."; 360 | return convertDictToCReturn(dict: result_dict); 361 | } 362 | } 363 | 364 | 365 | if let option_types = options_dict["option_types"] as? [String: String] { 366 | options_dict.forEach { option in 367 | if (options[option.key] != nil) { 368 | switch option_types[option.key] { 369 | case "string": 370 | options[option.key] = option.value as! String; 371 | case "array": 372 | options[option.key] = option.value as! [String]; 373 | case "bool-true": 374 | options[option.key] = true; 375 | case "bool-false": 376 | options[option.key] = false; 377 | case "float": 378 | options[option.key] = option.value as! CGFloat; 379 | case "int": 380 | options[option.key] = option.value as! Int; 381 | case "size": 382 | if let size_value = option.value as? [String: CGFloat] { 383 | options[option.key] = CGSize(width: size_value["width"]! as CGFloat, height: size_value["height"]! as CGFloat) 384 | } 385 | case "image": 386 | options[option.key] = NSImage(contentsOfFile: option.value as! String); 387 | default: 388 | break; 389 | } 390 | } 391 | } 392 | if (options["width"] as? CGFloat ?? 0 != 0) { 393 | options["window_size"] = CGSize(width: options["width"] as! CGFloat, height: (options["window_size"] as! CGSize).height) 394 | } 395 | if (options["height"] as? CGFloat ?? 0 != 0) { 396 | options["window_size"] = CGSize(width: (options["window_size"] as! CGSize).width, height: options["height"] as! CGFloat) 397 | } 398 | } 399 | result_dict["status"] = "success"; 400 | } 401 | 402 | return convertDictToCReturn(dict: result_dict); 403 | } 404 | 405 | 406 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 407 | func convertToDictionary(text: String) -> [String: Any]? { 408 | if let data = text.data(using: .utf8) { 409 | do { 410 | return try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] 411 | } catch {} 412 | } 413 | return nil 414 | } 415 | func convertDictToJsonString(dict: [String: Any]) -> String? { 416 | guard let theJSONData = try? JSONSerialization.data(withJSONObject: dict, options: []) else { 417 | return nil 418 | } 419 | return String(data: theJSONData, encoding: .utf8) 420 | } 421 | func convertDictToCReturn(dict: [String: Any]) -> UnsafePointer { 422 | let return_json = convertDictToJsonString(dict: dict); 423 | return (return_json! as NSString).utf8String!; 424 | } 425 | func convertOptionsToDictionary(options: UnsafePointer) -> [String: Any]? { 426 | return convertToDictionary(text: String(cString: options)); 427 | } 428 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -------------------------------------------------------------------------------- /source/src/textbox/Sources/textbox/textbox.swift: -------------------------------------------------------------------------------- 1 | // swift build --package-path ./src/textbox -Xswiftc "-target" -Xswiftc "x86_64-apple-macosx10.15" -Xlinker -rpath -Xlinker @executable_path/Frameworks 2 | 3 | import AppKit 4 | import SwiftUI 5 | 6 | 7 | // Variables - Defaults 8 | var gdialog_type = "textbox"; 9 | public typealias `func` = @convention(c) (UnsafePointer) -> Void 10 | var callbackHolder: `func`? 11 | var options: [String: Any] = [ 12 | "async": false, 13 | "initial_text": "", 14 | "encode_text": false, 15 | "header": "", 16 | "allow_quit": false, 17 | "width": 0, 18 | "height": 0, 19 | "window_size": CGSize(width: 500, height: 150), 20 | "icon_file": "", // NSImage 21 | "system_icon": "", 22 | "icon_type": "none", 23 | "title": "", 24 | "text": "", 25 | "buttons": [ "OK" ], 26 | "scrollable_text": false, 27 | "focus": false, 28 | "timeout": 0, 29 | "timeout_text": "Time Remaining", 30 | "static": false 31 | ]; 32 | 33 | // Text Box UI 34 | struct ContentView: View { 35 | @State private var textField_text: String = options["initial_text"] as! String; 36 | var body: some View { 37 | VStack(spacing: 0) { 38 | HeaderView() 39 | BodyView(textField_text: $textField_text) 40 | FooterView(textField_text: $textField_text) 41 | } 42 | .padding(5) 43 | } 44 | } 45 | 46 | struct HeaderView: View { 47 | var body: some View { 48 | VStack() { 49 | Text(options["header"] as! String).font(.title) 50 | } 51 | .frame(maxWidth: .infinity) 52 | .padding(.bottom, 3) 53 | } 54 | } 55 | 56 | struct BodyView: View { 57 | @Binding var textField_text: String; 58 | var body: some View { 59 | HStack(alignment: .top) { 60 | if (options["icon_type"] as! String != "none") { 61 | IconView() 62 | } 63 | MainView(textField_text: $textField_text) 64 | } 65 | .frame(maxHeight: .infinity) 66 | } 67 | } 68 | 69 | struct IconView: View { 70 | var body: some View { 71 | VStack() { 72 | if options["icon_type"] as! String == "file" { 73 | Image(nsImage: options["icon_file"] as! NSImage) 74 | .resizable() 75 | .aspectRatio(contentMode: .fit) 76 | .frame(maxWidth: 75) 77 | } 78 | else { 79 | Image(nsImage: NSImage(named: (options["system_icon"] as! String).starts(with: "NSImageName") ? (options["system_icon"] as! String).replacingOccurrences(of: "NSImageName", with: "NS") : options["system_icon"] as! String)!) 80 | .resizable() 81 | .aspectRatio(contentMode: .fit) 82 | .frame(width: 75) 83 | 84 | } 85 | } 86 | .frame(maxHeight: .infinity) 87 | } 88 | } 89 | 90 | struct MainView: View { 91 | @Binding var textField_text: String; 92 | var body: some View { 93 | VStack { 94 | if (options["scrollable_text"] as! Bool) { 95 | ScrollView { 96 | TextView() 97 | } 98 | } 99 | else { 100 | TextView() 101 | } 102 | TextEditorView(textField_text: $textField_text) 103 | } 104 | .frame(maxHeight: .infinity, alignment: .top) 105 | .padding(.leading, 5) 106 | } 107 | } 108 | 109 | 110 | struct TextView: View { 111 | var body: some View { 112 | VStack(alignment: .leading) { 113 | Text(options["text"] as! String) 114 | .font(.body) 115 | .frame(maxWidth: .infinity, alignment: .leading) 116 | .multilineTextAlignment(.leading) 117 | } 118 | } 119 | } 120 | 121 | struct TextEditorView: View { 122 | @Binding var textField_text: String; 123 | 124 | var body: some View { 125 | VStack() { 126 | if #available(macOS 10.16, *) { 127 | TextEditor(text: $textField_text) 128 | .font(.body) 129 | .multilineTextAlignment(.leading) 130 | .disableAutocorrection(true) 131 | .frame(maxHeight: .infinity, alignment: .top) 132 | } 133 | } 134 | .textFieldStyle(RoundedBorderTextFieldStyle()) 135 | } 136 | } 137 | 138 | struct FooterView: View { 139 | @Binding var textField_text: String; 140 | var body: some View { 141 | HStack() { 142 | if (options["timeout"] as! Int > 0) { 143 | TimerView() 144 | } 145 | ButtonsView(textField_text: $textField_text) 146 | } 147 | } 148 | } 149 | 150 | struct TimerView: View { 151 | @State private var timeRemaining = options["timeout"] as! Int; 152 | let timer = Timer.publish(every: 1, on: .current, in: .common).autoconnect() 153 | var body: some View { 154 | HStack() { 155 | Text("\(options["timeout_text"] as! String): \(buildRemainingTime(timeRemaining))") 156 | } 157 | .padding(.leading, 5) 158 | .onReceive(timer) { time in 159 | if self.timeRemaining > 0 { 160 | self.timeRemaining -= 1 161 | } 162 | else { 163 | timer.upstream.connect().cancel() 164 | dialogResult = ""; 165 | dialogInteraction("timeout"); 166 | let dummyEvent = NSEvent.otherEvent(with: NSEvent.EventType.applicationDefined, location: NSZeroPoint, modifierFlags: NSEvent.ModifierFlags(rawValue: 0), timestamp: 0, windowNumber: 0, context: nil, subtype:0, data1:0, data2:0)!; 167 | NSApp.postEvent(dummyEvent, atStart: true); 168 | } 169 | } 170 | } 171 | } 172 | 173 | func buildRemainingTime(_ time: Int) -> String { 174 | var remainingTime: Int = time; 175 | var days = 0; 176 | var hours = 0; 177 | var minutes = 0; 178 | var seconds = 0; 179 | 180 | if (remainingTime > 59) { 181 | seconds = remainingTime % 60; 182 | remainingTime = remainingTime / 60; 183 | if (remainingTime > 59) { 184 | minutes = remainingTime % 60; 185 | remainingTime = remainingTime / 60; 186 | if (remainingTime > 23) { 187 | hours = remainingTime % 24; 188 | days = remainingTime / 24; 189 | } 190 | else { 191 | hours = remainingTime; 192 | } 193 | } 194 | else { 195 | minutes = remainingTime; 196 | } 197 | } 198 | else { 199 | seconds = remainingTime; 200 | } 201 | var result = days > 0 ? String(days) + " " : ""; 202 | result += (hours < 10 ? "0" : "") + String(hours); 203 | result += ":" + (minutes < 10 ? "0" : "") + String(minutes); 204 | result += ":" + (seconds < 10 ? "0" : "") + String(seconds); 205 | 206 | return result; 207 | } 208 | 209 | var buttonCount: Int = 0; 210 | func increaseButtonCount() -> Int { 211 | buttonCount += 1; 212 | return buttonCount; 213 | } 214 | struct ButtonsView : View { 215 | @Binding var textField_text: String; 216 | var body: some View { 217 | HStack { 218 | ForEach(options["buttons"] as! [String], id: \.self) { 219 | ButtonView( 220 | textField_text: $textField_text, 221 | labelText: "\($0)", 222 | actionReturn: String(increaseButtonCount()) 223 | ) 224 | } 225 | } 226 | .frame(maxWidth: .infinity, alignment: .leading) 227 | .environment(\.layoutDirection, .rightToLeft) 228 | .padding(.top, 3) 229 | } 230 | } 231 | struct ButtonView: View { 232 | @Binding var textField_text: String; 233 | var labelText: String = "Button"; 234 | var actionReturn: String = "0"; 235 | 236 | var body: some View { 237 | HStack { 238 | Button(action: buttonClicked, label: { 239 | Text(labelText) 240 | .frame(minWidth: 50) 241 | }) 242 | .padding(.horizontal, 5) 243 | .padding(.vertical, 2) 244 | } 245 | 246 | } 247 | func buttonClicked() { 248 | if options["encode_text"] as! Bool { 249 | dialogInteraction(actionReturn + "\n" + Data(textField_text.utf8).base64EncodedString()); 250 | } 251 | else { 252 | dialogInteraction(actionReturn + "\n" + textField_text); 253 | } 254 | // app.stop("button"); 255 | } 256 | } 257 | 258 | /////////////////////// 259 | // DO NOT EDIT BELOW // 260 | /////////////////////// 261 | 262 | func dialogInteraction(_ returnString: String) { 263 | if ((options["async"] as! Bool) && (returnString != "timeout")) { 264 | callbackHolder!((returnString as NSString).utf8String!); 265 | } 266 | else { 267 | dialogResult = returnString; 268 | app.stop(""); 269 | } 270 | } 271 | 272 | let app = NSApplication.shared 273 | let delegate = AppDelegate() 274 | var dialogResult = "-1"; 275 | 276 | class WindowDelegate: NSObject, NSWindowDelegate { 277 | func windowWillClose(_ notification: Notification) { 278 | NSApplication.shared.terminate(0) 279 | } 280 | } 281 | 282 | class CustomWindow: NSWindow { 283 | override var canBecomeKey: Bool { 284 | return true 285 | } 286 | } 287 | 288 | class AppDelegate: NSObject, NSApplicationDelegate { 289 | let window = CustomWindow() 290 | let windowDelegate = WindowDelegate() 291 | 292 | func applicationDidFinishLaunching(_ notification: Notification) { 293 | let mainMenu = NSMenu(title: options["title"] as! String) 294 | if (options["allow_quit"] as! Bool) { 295 | let appMenu = NSMenuItem() 296 | appMenu.submenu = NSMenu() 297 | appMenu.submenu?.addItem(NSMenuItem(title: "Quit", action: #selector(app.stop(_:)), keyEquivalent: "q")) 298 | mainMenu.addItem(appMenu) 299 | } 300 | let editMenu = NSMenuItem() 301 | editMenu.submenu = NSMenu(title: "Edit") 302 | editMenu.submenu?.addItem(NSMenuItem(title: "Undo", action: Selector(("undo:")), keyEquivalent: "z")) 303 | editMenu.submenu?.addItem(NSMenuItem(title: "Redo", action: Selector(("redo:")), keyEquivalent: "y")) 304 | editMenu.submenu?.addItem(NSMenuItem.separator()) 305 | editMenu.submenu?.addItem(NSMenuItem(title: "Cut", action: #selector(NSText.cut(_:)), keyEquivalent: "x")) 306 | editMenu.submenu?.addItem(NSMenuItem(title: "Copy", action: #selector(NSText.copy(_:)), keyEquivalent: "c")) 307 | editMenu.submenu?.addItem(NSMenuItem(title: "Paste", action: #selector(NSText.paste(_:)), keyEquivalent: "v")) 308 | editMenu.submenu?.addItem(NSMenuItem(title: "Select All", action: #selector(NSStandardKeyBindingResponding.selectAll(_:)), keyEquivalent: "a")) 309 | mainMenu.addItem(editMenu) 310 | NSApplication.shared.mainMenu = mainMenu 311 | window.setContentSize(options["window_size"] as! CGSize) 312 | window.delegate = windowDelegate 313 | if (options["title"] as? String ?? "" == "") { 314 | window.styleMask = [] 315 | } 316 | else { 317 | window.styleMask = [.titled] 318 | window.title = options["title"] as! String 319 | } 320 | 321 | let view = NSHostingView(rootView: ContentView()) 322 | view.frame = CGRect(origin: .zero, size: options["window_size"] as! CGSize) 323 | view.autoresizingMask = [.height, .width] 324 | window.contentView!.addSubview(view) 325 | if (options["static"] as! Bool) { 326 | window.isMovableByWindowBackground = false 327 | window.isMovable = false 328 | } 329 | else { 330 | window.isMovableByWindowBackground = true 331 | window.isMovable = true 332 | } 333 | window.center() 334 | window.makeKeyAndOrderFront(window) 335 | 336 | NSApp.setActivationPolicy(.accessory) 337 | if (options["focus"] as! Bool) { 338 | NSApp.activate(ignoringOtherApps: true) // Makes the window take focus 339 | } 340 | } 341 | } 342 | 343 | @_cdecl("swift_run_dialog") 344 | public func run_dialog(callBack:@escaping `func`) -> UnsafePointer { 345 | callbackHolder = callBack; 346 | let delegate = AppDelegate() 347 | app.delegate = delegate 348 | app.run() 349 | return (dialogResult as NSString).utf8String!; 350 | } 351 | 352 | @_cdecl("swift_set_options") 353 | public func set_options(_options: UnsafePointer) -> UnsafePointer { 354 | var result_dict: [String: Any] = [ 355 | "status" as String: "failed" 356 | ]; 357 | 358 | if let options_dict = convertOptionsToDictionary(options: _options) { 359 | if let value = options_dict["type"] as? String { 360 | if gdialog_type != value { 361 | result_dict["error"] = "gDialog type does not match."; 362 | return convertDictToCReturn(dict: result_dict); 363 | } 364 | } 365 | 366 | 367 | if let option_types = options_dict["option_types"] as? [String: String] { 368 | options_dict.forEach { option in 369 | if (options[option.key] != nil) { 370 | switch option_types[option.key] { 371 | case "string": 372 | options[option.key] = option.value as! String; 373 | case "array": 374 | options[option.key] = option.value as! [String]; 375 | case "bool-true": 376 | options[option.key] = true; 377 | case "bool-false": 378 | options[option.key] = false; 379 | case "float": 380 | options[option.key] = option.value as! CGFloat; 381 | case "int": 382 | options[option.key] = option.value as! Int; 383 | case "size": 384 | if let size_value = option.value as? [String: CGFloat] { 385 | options[option.key] = CGSize(width: size_value["width"]! as CGFloat, height: size_value["height"]! as CGFloat) 386 | } 387 | case "image": 388 | options[option.key] = NSImage(contentsOfFile: option.value as! String); 389 | default: 390 | break; 391 | } 392 | } 393 | } 394 | if (options["width"] as? CGFloat ?? 0 != 0) { 395 | options["window_size"] = CGSize(width: options["width"] as! CGFloat, height: (options["window_size"] as! CGSize).height) 396 | } 397 | if (options["height"] as? CGFloat ?? 0 != 0) { 398 | options["window_size"] = CGSize(width: (options["window_size"] as! CGSize).width, height: options["height"] as! CGFloat) 399 | } 400 | } 401 | result_dict["status"] = "success"; 402 | } 403 | 404 | return convertDictToCReturn(dict: result_dict); 405 | } 406 | 407 | 408 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 409 | func convertToDictionary(text: String) -> [String: Any]? { 410 | if let data = text.data(using: .utf8) { 411 | do { 412 | return try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] 413 | } catch {} 414 | } 415 | return nil 416 | } 417 | func convertDictToJsonString(dict: [String: Any]) -> String? { 418 | guard let theJSONData = try? JSONSerialization.data(withJSONObject: dict, options: []) else { 419 | return nil 420 | } 421 | return String(data: theJSONData, encoding: .utf8) 422 | } 423 | func convertDictToCReturn(dict: [String: Any]) -> UnsafePointer { 424 | let return_json = convertDictToJsonString(dict: dict); 425 | return (return_json! as NSString).utf8String!; 426 | } 427 | func convertOptionsToDictionary(options: UnsafePointer) -> [String: Any]? { 428 | return convertToDictionary(text: String(cString: options)); 429 | } 430 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -------------------------------------------------------------------------------- /source/src/secure-inputbox/Sources/secure-inputbox/secure-inputbox.swift: -------------------------------------------------------------------------------- 1 | // swift build --package-path ./src/secure-inputbox -Xswiftc "-target" -Xswiftc "x86_64-apple-macosx10.15" -Xlinker -rpath -Xlinker @executable_path/Frameworks 2 | 3 | // import Foundation 4 | // import Security 5 | 6 | import AppKit 7 | import SwiftUI 8 | 9 | 10 | // Variables - Defaults 11 | var gdialog_type = "secure-inputbox"; 12 | public typealias `func` = @convention(c) (UnsafePointer) -> Void 13 | var callbackHolder: `func`? 14 | var options: [String: Any] = [ 15 | "async": false, 16 | "initial_text": "", 17 | "background_text": "", 18 | "encode_text": false, 19 | "header": "", 20 | "allow_quit": false, 21 | "width": 0, 22 | "height": 0, 23 | "window_size": CGSize(width: 500, height: 150), 24 | "icon_file": "", // NSImage 25 | "system_icon": "", 26 | "icon_type": "none", 27 | "title": "", 28 | "text": "", 29 | "buttons": [ "OK" ], 30 | "scrollable_text": false, 31 | "focus": false, 32 | "timeout": 0, 33 | "timeout_text": "Time Remaining", 34 | "static": false 35 | ]; 36 | 37 | // Secure Input Box UI 38 | struct ContentView: View { 39 | @State private var textField_text: String = ""; 40 | var body: some View { 41 | VStack(spacing: 0) { 42 | HeaderView() 43 | BodyView(textField_text: $textField_text) 44 | FooterView(textField_text: $textField_text) 45 | } 46 | .padding(5) 47 | } 48 | } 49 | 50 | struct HeaderView: View { 51 | var body: some View { 52 | VStack() { 53 | Text(options["header"] as! String).font(.title) 54 | } 55 | .frame(maxWidth: .infinity) 56 | .padding(.bottom, 3) 57 | } 58 | } 59 | 60 | struct BodyView: View { 61 | @Binding var textField_text: String; 62 | var body: some View { 63 | HStack(alignment: .top) { 64 | if (options["icon_type"] as! String != "none") { 65 | IconView() 66 | } 67 | MainView(textField_text: $textField_text) 68 | } 69 | .frame(maxHeight: .infinity) 70 | } 71 | } 72 | 73 | struct IconView: View { 74 | var body: some View { 75 | VStack() { 76 | if options["icon_type"] as! String == "file" { 77 | Image(nsImage: options["icon_file"] as! NSImage) 78 | .resizable() 79 | .aspectRatio(contentMode: .fit) 80 | .frame(maxWidth: 75) 81 | } 82 | else { 83 | Image(nsImage: NSImage(named: (options["system_icon"] as! String).starts(with: "NSImageName") ? (options["system_icon"] as! String).replacingOccurrences(of: "NSImageName", with: "NS") : options["system_icon"] as! String)!) 84 | .resizable() 85 | .aspectRatio(contentMode: .fit) 86 | .frame(width: 75) 87 | 88 | } 89 | } 90 | .frame(maxHeight: .infinity) 91 | } 92 | } 93 | 94 | struct MainView: View { 95 | @Binding var textField_text: String; 96 | var body: some View { 97 | VStack { 98 | if (options["scrollable_text"] as! Bool) { 99 | ScrollView { 100 | TextView() 101 | } 102 | } 103 | else { 104 | TextView() 105 | } 106 | SecureFieldView(textField_text: $textField_text) 107 | } 108 | .frame(maxHeight: .infinity, alignment: .top) 109 | .padding(.leading, 5) 110 | } 111 | } 112 | 113 | 114 | struct TextView: View { 115 | var body: some View { 116 | VStack(alignment: .leading) { 117 | Text(options["text"] as! String) 118 | .font(.body) 119 | .frame(maxWidth: .infinity, alignment: .leading) 120 | .multilineTextAlignment(.leading) 121 | } 122 | } 123 | } 124 | 125 | struct SecureFieldView: View { 126 | @Binding var textField_text: String; 127 | 128 | var body: some View { 129 | VStack() { 130 | SecureField(options["background_text"] as! String, text: $textField_text) 131 | .disableAutocorrection(true) 132 | } 133 | .textFieldStyle(RoundedBorderTextFieldStyle()) 134 | } 135 | 136 | // func textOnCommit() { 137 | // print("commit"); 138 | // } 139 | } 140 | 141 | struct FooterView: View { 142 | @Binding var textField_text: String; 143 | var body: some View { 144 | HStack() { 145 | if (options["timeout"] as! Int > 0) { 146 | TimerView() 147 | } 148 | ButtonsView(textField_text: $textField_text) 149 | } 150 | } 151 | } 152 | 153 | struct TimerView: View { 154 | @State private var timeRemaining = options["timeout"] as! Int; 155 | let timer = Timer.publish(every: 1, on: .current, in: .common).autoconnect() 156 | var body: some View { 157 | HStack() { 158 | Text("\(options["timeout_text"] as! String): \(buildRemainingTime(timeRemaining))") 159 | } 160 | .padding(.leading, 5) 161 | .onReceive(timer) { time in 162 | if self.timeRemaining > 0 { 163 | self.timeRemaining -= 1 164 | } 165 | else { 166 | timer.upstream.connect().cancel() 167 | dialogResult = ""; 168 | dialogInteraction("timeout"); 169 | let dummyEvent = NSEvent.otherEvent(with: NSEvent.EventType.applicationDefined, location: NSZeroPoint, modifierFlags: NSEvent.ModifierFlags(rawValue: 0), timestamp: 0, windowNumber: 0, context: nil, subtype:0, data1:0, data2:0)!; 170 | NSApp.postEvent(dummyEvent, atStart: true); 171 | } 172 | } 173 | } 174 | } 175 | 176 | func buildRemainingTime(_ time: Int) -> String { 177 | var remainingTime: Int = time; 178 | var days = 0; 179 | var hours = 0; 180 | var minutes = 0; 181 | var seconds = 0; 182 | 183 | if (remainingTime > 59) { 184 | seconds = remainingTime % 60; 185 | remainingTime = remainingTime / 60; 186 | if (remainingTime > 59) { 187 | minutes = remainingTime % 60; 188 | remainingTime = remainingTime / 60; 189 | if (remainingTime > 23) { 190 | hours = remainingTime % 24; 191 | days = remainingTime / 24; 192 | } 193 | else { 194 | hours = remainingTime; 195 | } 196 | } 197 | else { 198 | minutes = remainingTime; 199 | } 200 | } 201 | else { 202 | seconds = remainingTime; 203 | } 204 | var result = days > 0 ? String(days) + " " : ""; 205 | result += (hours < 10 ? "0" : "") + String(hours); 206 | result += ":" + (minutes < 10 ? "0" : "") + String(minutes); 207 | result += ":" + (seconds < 10 ? "0" : "") + String(seconds); 208 | 209 | return result; 210 | } 211 | 212 | var buttonCount: Int = 0; 213 | func increaseButtonCount() -> Int { 214 | buttonCount += 1; 215 | return buttonCount; 216 | } 217 | struct ButtonsView : View { 218 | @Binding var textField_text: String; 219 | var body: some View { 220 | HStack { 221 | ForEach(options["buttons"] as! [String], id: \.self) { 222 | ButtonView( 223 | textField_text: $textField_text, 224 | labelText: "\($0)", 225 | actionReturn: String(increaseButtonCount()) 226 | ) 227 | } 228 | } 229 | .frame(maxWidth: .infinity, alignment: .leading) 230 | .environment(\.layoutDirection, .rightToLeft) 231 | .padding(.top, 3) 232 | } 233 | } 234 | struct ButtonView: View { 235 | @Binding var textField_text: String; 236 | var labelText: String = "Button"; 237 | var actionReturn: String = "0"; 238 | 239 | var body: some View { 240 | HStack { 241 | Button(action: buttonClicked, label: { 242 | Text(labelText) 243 | .frame(minWidth: 50) 244 | }) 245 | .padding(.horizontal, 5) 246 | .padding(.vertical, 2) 247 | } 248 | 249 | } 250 | func buttonClicked() { 251 | if options["encode_text"] as! Bool { 252 | dialogInteraction(actionReturn + "\n" + Data(textField_text.utf8).base64EncodedString()); 253 | } 254 | else { 255 | dialogInteraction(actionReturn + "\n" + textField_text); 256 | } 257 | // app.stop("button"); 258 | } 259 | } 260 | 261 | /////////////////////// 262 | // DO NOT EDIT BELOW // 263 | /////////////////////// 264 | 265 | func dialogInteraction(_ returnString: String) { 266 | if ((options["async"] as! Bool) && (returnString != "timeout")) { 267 | callbackHolder!((returnString as NSString).utf8String!); 268 | } 269 | else { 270 | dialogResult = returnString; 271 | app.stop(""); 272 | } 273 | } 274 | 275 | let app = NSApplication.shared 276 | let delegate = AppDelegate() 277 | var dialogResult = "-1"; 278 | 279 | class WindowDelegate: NSObject, NSWindowDelegate { 280 | func windowWillClose(_ notification: Notification) { 281 | NSApplication.shared.terminate(0) 282 | } 283 | } 284 | 285 | class CustomWindow: NSWindow { 286 | override var canBecomeKey: Bool { 287 | return true 288 | } 289 | } 290 | 291 | class AppDelegate: NSObject, NSApplicationDelegate { 292 | let window = CustomWindow() 293 | let windowDelegate = WindowDelegate() 294 | 295 | func applicationDidFinishLaunching(_ notification: Notification) { 296 | let mainMenu = NSMenu(title: options["title"] as! String) 297 | if (options["allow_quit"] as! Bool) { 298 | let appMenu = NSMenuItem() 299 | appMenu.submenu = NSMenu() 300 | appMenu.submenu?.addItem(NSMenuItem(title: "Quit", action: #selector(app.stop(_:)), keyEquivalent: "q")) 301 | mainMenu.addItem(appMenu) 302 | } 303 | let editMenu = NSMenuItem() 304 | editMenu.submenu = NSMenu(title: "Edit") 305 | editMenu.submenu?.addItem(NSMenuItem(title: "Undo", action: Selector(("undo:")), keyEquivalent: "z")) 306 | editMenu.submenu?.addItem(NSMenuItem(title: "Redo", action: Selector(("redo:")), keyEquivalent: "y")) 307 | editMenu.submenu?.addItem(NSMenuItem.separator()) 308 | editMenu.submenu?.addItem(NSMenuItem(title: "Cut", action: #selector(NSText.cut(_:)), keyEquivalent: "x")) 309 | editMenu.submenu?.addItem(NSMenuItem(title: "Copy", action: #selector(NSText.copy(_:)), keyEquivalent: "c")) 310 | editMenu.submenu?.addItem(NSMenuItem(title: "Paste", action: #selector(NSText.paste(_:)), keyEquivalent: "v")) 311 | editMenu.submenu?.addItem(NSMenuItem(title: "Select All", action: #selector(NSStandardKeyBindingResponding.selectAll(_:)), keyEquivalent: "a")) 312 | mainMenu.addItem(editMenu) 313 | NSApplication.shared.mainMenu = mainMenu 314 | 315 | window.setContentSize(options["window_size"] as! CGSize) 316 | window.delegate = windowDelegate 317 | if (options["title"] as? String ?? "" == "") { 318 | window.styleMask = [] 319 | } 320 | else { 321 | window.styleMask = [.titled] 322 | window.title = options["title"] as! String 323 | } 324 | 325 | let view = NSHostingView(rootView: ContentView()) 326 | view.frame = CGRect(origin: .zero, size: options["window_size"] as! CGSize) 327 | view.autoresizingMask = [.height, .width] 328 | window.contentView!.addSubview(view) 329 | if (options["static"] as! Bool) { 330 | window.isMovableByWindowBackground = false 331 | window.isMovable = false 332 | } 333 | else { 334 | window.isMovableByWindowBackground = true 335 | window.isMovable = true 336 | } 337 | window.center() 338 | window.makeKeyAndOrderFront(window) 339 | 340 | NSApp.setActivationPolicy(.accessory) 341 | if (options["focus"] as! Bool) { 342 | NSApp.activate(ignoringOtherApps: true) // Makes the window take focus 343 | } 344 | } 345 | } 346 | 347 | @_cdecl("swift_run_dialog") 348 | public func run_dialog(callBack:@escaping `func`) -> UnsafePointer { 349 | callbackHolder = callBack; 350 | let delegate = AppDelegate() 351 | app.delegate = delegate 352 | app.run() 353 | return (dialogResult as NSString).utf8String!; 354 | } 355 | 356 | @_cdecl("swift_set_options") 357 | public func set_options(_options: UnsafePointer) -> UnsafePointer { 358 | var result_dict: [String: Any] = [ 359 | "status" as String: "failed" 360 | ]; 361 | 362 | if let options_dict = convertOptionsToDictionary(options: _options) { 363 | if let value = options_dict["type"] as? String { 364 | if gdialog_type != value { 365 | result_dict["error"] = "gDialog type does not match."; 366 | return convertDictToCReturn(dict: result_dict); 367 | } 368 | } 369 | 370 | 371 | if let option_types = options_dict["option_types"] as? [String: String] { 372 | options_dict.forEach { option in 373 | if (options[option.key] != nil) { 374 | switch option_types[option.key] { 375 | case "string": 376 | options[option.key] = option.value as! String; 377 | case "array": 378 | options[option.key] = option.value as! [String]; 379 | case "bool-true": 380 | options[option.key] = true; 381 | case "bool-false": 382 | options[option.key] = false; 383 | case "float": 384 | options[option.key] = option.value as! CGFloat; 385 | case "int": 386 | options[option.key] = option.value as! Int; 387 | case "size": 388 | if let size_value = option.value as? [String: CGFloat] { 389 | options[option.key] = CGSize(width: size_value["width"]! as CGFloat, height: size_value["height"]! as CGFloat) 390 | } 391 | case "image": 392 | options[option.key] = NSImage(contentsOfFile: option.value as! String); 393 | default: 394 | break; 395 | } 396 | } 397 | } 398 | if (options["width"] as? CGFloat ?? 0 != 0) { 399 | options["window_size"] = CGSize(width: options["width"] as! CGFloat, height: (options["window_size"] as! CGSize).height) 400 | } 401 | if (options["height"] as? CGFloat ?? 0 != 0) { 402 | options["window_size"] = CGSize(width: (options["window_size"] as! CGSize).width, height: options["height"] as! CGFloat) 403 | } 404 | } 405 | result_dict["status"] = "success"; 406 | } 407 | 408 | return convertDictToCReturn(dict: result_dict); 409 | } 410 | 411 | 412 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 413 | func convertToDictionary(text: String) -> [String: Any]? { 414 | if let data = text.data(using: .utf8) { 415 | do { 416 | return try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] 417 | } catch {} 418 | } 419 | return nil 420 | } 421 | func convertDictToJsonString(dict: [String: Any]) -> String? { 422 | guard let theJSONData = try? JSONSerialization.data(withJSONObject: dict, options: []) else { 423 | return nil 424 | } 425 | return String(data: theJSONData, encoding: .utf8) 426 | } 427 | func convertDictToCReturn(dict: [String: Any]) -> UnsafePointer { 428 | let return_json = convertDictToJsonString(dict: dict); 429 | return (return_json! as NSString).utf8String!; 430 | } 431 | func convertOptionsToDictionary(options: UnsafePointer) -> [String: Any]? { 432 | return convertToDictionary(text: String(cString: options)); 433 | } 434 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -------------------------------------------------------------------------------- /source/src/filesave/Sources/filesave/filesave.swift: -------------------------------------------------------------------------------- 1 | // swift build --package-path ./src/filesave -Xswiftc "-target" -Xswiftc "x86_64-apple-macosx10.15" -Xlinker -rpath -Xlinker @executable_path/Frameworks 2 | 3 | import AppKit 4 | import SwiftUI 5 | 6 | 7 | // Variables - Defaults 8 | var gdialog_type = "filesave"; 9 | public typealias `func` = @convention(c) (UnsafePointer) -> Void 10 | var callbackHolder: `func`? 11 | var options: [String: Any] = [ 12 | "async": false, 13 | "dont_create_directories": false, 14 | "with_directory": "", 15 | "with_file": "", 16 | "packages_as_directories": false, 17 | "with_extensions": [], 18 | "header": "", 19 | "allow_quit": false, 20 | "width": 0, 21 | "height": 0, 22 | "window_size": CGSize(width: 500, height: 150), 23 | "icon_file": "", // NSImage 24 | "system_icon": "", 25 | "icon_type": "none", 26 | "title": "", 27 | "text": "", 28 | "buttons": [ "OK" ], 29 | "scrollable_text": false, 30 | "focus": false, 31 | "timeout": 0, 32 | "timeout_text": "Time Remaining", 33 | "static": false 34 | ]; 35 | 36 | // File Save UI 37 | struct ContentView: View { 38 | @State private var file_path = options["with_file"] as! String; 39 | 40 | var body: some View { 41 | VStack(spacing: 0) { 42 | HeaderView() 43 | BodyView(file_path: $file_path) 44 | FooterView(file_path: $file_path) 45 | } 46 | .padding(5) 47 | } 48 | } 49 | 50 | struct HeaderView: View { 51 | var body: some View { 52 | VStack() { 53 | Text(options["header"] as! String).font(.title) 54 | } 55 | .frame(maxWidth: .infinity) 56 | .padding(.bottom, 3) 57 | } 58 | } 59 | 60 | struct BodyView: View { 61 | @Binding var file_path: String; 62 | 63 | var body: some View { 64 | HStack(alignment: .top) { 65 | if (options["icon_type"] as! String != "none") { 66 | IconView() 67 | } 68 | MainView(file_path: $file_path) 69 | } 70 | .frame(maxHeight: .infinity) 71 | } 72 | } 73 | 74 | struct IconView: View { 75 | var body: some View { 76 | VStack() { 77 | if options["icon_type"] as! String == "file" { 78 | Image(nsImage: options["icon_file"] as! NSImage) 79 | .resizable() 80 | .aspectRatio(contentMode: .fit) 81 | .frame(maxWidth: 75) 82 | } 83 | else { 84 | Image(nsImage: NSImage(named: (options["system_icon"] as! String).starts(with: "NSImageName") ? (options["system_icon"] as! String).replacingOccurrences(of: "NSImageName", with: "NS") : options["system_icon"] as! String)!) 85 | .resizable() 86 | .aspectRatio(contentMode: .fit) 87 | .frame(width: 75) 88 | 89 | } 90 | } 91 | .frame(maxHeight: .infinity) 92 | } 93 | } 94 | 95 | struct MainView: View { 96 | @Binding var file_path: String; 97 | 98 | var body: some View { 99 | VStack { 100 | if (options["scrollable_text"] as! Bool) { 101 | ScrollView { 102 | TextView() 103 | } 104 | } 105 | else { 106 | TextView() 107 | } 108 | FileSaveView(file_path: $file_path) 109 | } 110 | .frame(maxHeight: .infinity, alignment: .top) 111 | .padding(.leading, 5) 112 | } 113 | } 114 | 115 | 116 | struct TextView: View { 117 | var body: some View { 118 | VStack(alignment: .leading) { 119 | Text(options["text"] as! String) 120 | .font(.body) 121 | .frame(maxWidth: .infinity, alignment: .leading) 122 | .multilineTextAlignment(.leading) 123 | } 124 | } 125 | } 126 | 127 | // options["select_multiple"] = true; 128 | struct FileSaveView: View { 129 | @Binding var file_path: String; 130 | 131 | var body: some View { 132 | HStack { 133 | Text("File:") 134 | TextField("", text: $file_path) 135 | .disableAutocorrection(true) 136 | Button(action: openPanel, label: { 137 | Text("Open") 138 | .frame(minWidth: 50) 139 | }) 140 | .padding(.horizontal, 5) 141 | .padding(.vertical, 2) 142 | } 143 | // .frame(maxHeight: .infinity) 144 | // .background(Color.pink) 145 | } 146 | 147 | func openPanel() { 148 | let dialog = NSSavePanel(); 149 | dialog.showsResizeIndicator = true; 150 | dialog.showsHiddenFiles = true; 151 | 152 | if !(options["with_extensions"] as! [String]).isEmpty { 153 | dialog.allowedFileTypes = options["with_extensions"] as? [String] 154 | } 155 | 156 | 157 | if (options["dont_create_directories"] as! Bool) { 158 | dialog.canCreateDirectories = false; 159 | } 160 | else { 161 | dialog.canCreateDirectories = true; 162 | } 163 | if (options["packages_as_directories"] as! Bool) { 164 | dialog.treatsFilePackagesAsDirectories = true; 165 | } 166 | if (options["with_directory"] as! String != "") { 167 | let initial_directory = (options["with_directory"] as! NSString).expandingTildeInPath 168 | dialog.directoryURL = NSURL.fileURL(withPath: initial_directory, isDirectory: true) 169 | } 170 | if (options["with_file"] as! String != "") { 171 | let initial_file = (options["with_file"] as! NSString).expandingTildeInPath 172 | dialog.directoryURL = NSURL.fileURL(withPath: initial_file, isDirectory: false) 173 | } 174 | 175 | if (dialog.runModal() == NSApplication.ModalResponse.OK) { 176 | // Results contains an array with all the selected paths 177 | file_path = dialog.url!.path; 178 | } 179 | } 180 | } 181 | 182 | 183 | 184 | struct FooterView: View { 185 | @Binding var file_path: String; 186 | 187 | var body: some View { 188 | HStack() { 189 | if (options["timeout"] as! Int > 0) { 190 | TimerView() 191 | } 192 | ButtonsView(file_path: $file_path) 193 | } 194 | } 195 | } 196 | 197 | struct TimerView: View { 198 | @State private var timeRemaining = options["timeout"] as! Int; 199 | let timer = Timer.publish(every: 1, on: .current, in: .common).autoconnect() 200 | var body: some View { 201 | HStack() { 202 | Text("\(options["timeout_text"] as! String): \(buildRemainingTime(timeRemaining))") 203 | } 204 | .padding(.leading, 5) 205 | .onReceive(timer) { time in 206 | if self.timeRemaining > 0 { 207 | self.timeRemaining -= 1 208 | } 209 | else { 210 | timer.upstream.connect().cancel() 211 | dialogResult = ""; 212 | dialogInteraction("timeout"); 213 | let dummyEvent = NSEvent.otherEvent(with: NSEvent.EventType.applicationDefined, location: NSZeroPoint, modifierFlags: NSEvent.ModifierFlags(rawValue: 0), timestamp: 0, windowNumber: 0, context: nil, subtype:0, data1:0, data2:0)!; 214 | NSApp.postEvent(dummyEvent, atStart: true); 215 | } 216 | } 217 | } 218 | } 219 | 220 | func buildRemainingTime(_ time: Int) -> String { 221 | var remainingTime: Int = time; 222 | var days = 0; 223 | var hours = 0; 224 | var minutes = 0; 225 | var seconds = 0; 226 | 227 | if (remainingTime > 59) { 228 | seconds = remainingTime % 60; 229 | remainingTime = remainingTime / 60; 230 | if (remainingTime > 59) { 231 | minutes = remainingTime % 60; 232 | remainingTime = remainingTime / 60; 233 | if (remainingTime > 23) { 234 | hours = remainingTime % 24; 235 | days = remainingTime / 24; 236 | } 237 | else { 238 | hours = remainingTime; 239 | } 240 | } 241 | else { 242 | minutes = remainingTime; 243 | } 244 | } 245 | else { 246 | seconds = remainingTime; 247 | } 248 | var result = days > 0 ? String(days) + " " : ""; 249 | result += (hours < 10 ? "0" : "") + String(hours); 250 | result += ":" + (minutes < 10 ? "0" : "") + String(minutes); 251 | result += ":" + (seconds < 10 ? "0" : "") + String(seconds); 252 | 253 | return result; 254 | } 255 | 256 | var buttonCount: Int = 0; 257 | func increaseButtonCount() -> Int { 258 | buttonCount += 1; 259 | return buttonCount; 260 | } 261 | struct ButtonsView : View { 262 | @Binding var file_path: String; 263 | 264 | var body: some View { 265 | HStack { 266 | ForEach(options["buttons"] as! [String], id: \.self) { 267 | ButtonView( 268 | file_path: $file_path, 269 | labelText: "\($0)", 270 | actionReturn: String(increaseButtonCount()) 271 | ) 272 | } 273 | } 274 | .frame(maxWidth: .infinity, alignment: .leading) 275 | .environment(\.layoutDirection, .rightToLeft) 276 | .padding(.top, 3) 277 | } 278 | } 279 | struct ButtonView: View { 280 | @Binding var file_path: String; 281 | 282 | var labelText: String = "Button"; 283 | var actionReturn: String = "0"; 284 | 285 | var body: some View { 286 | HStack { 287 | Button(action: buttonClicked, label: { 288 | Text(labelText) 289 | .frame(minWidth: 50) 290 | }) 291 | .padding(.horizontal, 5) 292 | .padding(.vertical, 2) 293 | } 294 | 295 | } 296 | 297 | func buttonClicked() { 298 | dialogInteraction(actionReturn + "\n" + file_path); 299 | // app.stop("button"); 300 | } 301 | } 302 | 303 | /////////////////////// 304 | // DO NOT EDIT BELOW // 305 | /////////////////////// 306 | 307 | func dialogInteraction(_ returnString: String) { 308 | if ((options["async"] as! Bool) && (returnString != "timeout")) { 309 | callbackHolder!((returnString as NSString).utf8String!); 310 | } 311 | else { 312 | dialogResult = returnString; 313 | app.stop(""); 314 | } 315 | } 316 | 317 | let app = NSApplication.shared 318 | let delegate = AppDelegate() 319 | var dialogResult = "-1"; 320 | 321 | class WindowDelegate: NSObject, NSWindowDelegate { 322 | func windowWillClose(_ notification: Notification) { 323 | NSApplication.shared.terminate(0) 324 | } 325 | } 326 | 327 | class CustomWindow: NSWindow { 328 | override var canBecomeKey: Bool { 329 | return true 330 | } 331 | } 332 | 333 | class AppDelegate: NSObject, NSApplicationDelegate { 334 | let window = CustomWindow() 335 | let windowDelegate = WindowDelegate() 336 | 337 | func applicationDidFinishLaunching(_ notification: Notification) { 338 | let mainMenu = NSMenu(title: options["title"] as! String) 339 | if (options["allow_quit"] as! Bool) { 340 | let appMenu = NSMenuItem() 341 | appMenu.submenu = NSMenu() 342 | appMenu.submenu?.addItem(NSMenuItem(title: "Quit", action: #selector(app.stop(_:)), keyEquivalent: "q")) 343 | mainMenu.addItem(appMenu) 344 | } 345 | let editMenu = NSMenuItem() 346 | editMenu.submenu = NSMenu(title: "Edit") 347 | editMenu.submenu?.addItem(NSMenuItem(title: "Undo", action: Selector(("undo:")), keyEquivalent: "z")) 348 | editMenu.submenu?.addItem(NSMenuItem(title: "Redo", action: Selector(("redo:")), keyEquivalent: "y")) 349 | editMenu.submenu?.addItem(NSMenuItem.separator()) 350 | editMenu.submenu?.addItem(NSMenuItem(title: "Cut", action: #selector(NSText.cut(_:)), keyEquivalent: "x")) 351 | editMenu.submenu?.addItem(NSMenuItem(title: "Copy", action: #selector(NSText.copy(_:)), keyEquivalent: "c")) 352 | editMenu.submenu?.addItem(NSMenuItem(title: "Paste", action: #selector(NSText.paste(_:)), keyEquivalent: "v")) 353 | editMenu.submenu?.addItem(NSMenuItem(title: "Select All", action: #selector(NSStandardKeyBindingResponding.selectAll(_:)), keyEquivalent: "a")) 354 | mainMenu.addItem(editMenu) 355 | NSApplication.shared.mainMenu = mainMenu 356 | window.setContentSize(options["window_size"] as! CGSize) 357 | window.delegate = windowDelegate 358 | if (options["title"] as? String ?? "" == "") { 359 | window.styleMask = [] 360 | } 361 | else { 362 | window.styleMask = [.titled] 363 | window.title = options["title"] as! String 364 | } 365 | 366 | let view = NSHostingView(rootView: ContentView()) 367 | view.frame = CGRect(origin: .zero, size: options["window_size"] as! CGSize) 368 | view.autoresizingMask = [.height, .width] 369 | window.contentView!.addSubview(view) 370 | if (options["static"] as! Bool) { 371 | window.isMovableByWindowBackground = false 372 | window.isMovable = false 373 | } 374 | else { 375 | window.isMovableByWindowBackground = true 376 | window.isMovable = true 377 | } 378 | window.center() 379 | window.makeKeyAndOrderFront(window) 380 | 381 | NSApp.setActivationPolicy(.accessory) 382 | if (options["focus"] as! Bool) { 383 | NSApp.activate(ignoringOtherApps: true) // Makes the window take focus 384 | } 385 | } 386 | } 387 | 388 | @_cdecl("swift_run_dialog") 389 | public func run_dialog(callBack:@escaping `func`) -> UnsafePointer { 390 | callbackHolder = callBack; 391 | let delegate = AppDelegate() 392 | app.delegate = delegate 393 | app.run() 394 | return (dialogResult as NSString).utf8String!; 395 | } 396 | 397 | @_cdecl("swift_set_options") 398 | public func set_options(_options: UnsafePointer) -> UnsafePointer { 399 | var result_dict: [String: Any] = [ 400 | "status" as String: "failed" 401 | ]; 402 | 403 | if let options_dict = convertOptionsToDictionary(options: _options) { 404 | if let value = options_dict["type"] as? String { 405 | if gdialog_type != value { 406 | result_dict["error"] = "gDialog type does not match."; 407 | return convertDictToCReturn(dict: result_dict); 408 | } 409 | } 410 | 411 | 412 | if let option_types = options_dict["option_types"] as? [String: String] { 413 | options_dict.forEach { option in 414 | if (options[option.key] != nil) { 415 | switch option_types[option.key] { 416 | case "string": 417 | options[option.key] = option.value as! String; 418 | case "array": 419 | options[option.key] = option.value as! [String]; 420 | case "bool-true": 421 | options[option.key] = true; 422 | case "bool-false": 423 | options[option.key] = false; 424 | case "float": 425 | options[option.key] = option.value as! CGFloat; 426 | case "int": 427 | options[option.key] = option.value as! Int; 428 | case "size": 429 | if let size_value = option.value as? [String: CGFloat] { 430 | options[option.key] = CGSize(width: size_value["width"]! as CGFloat, height: size_value["height"]! as CGFloat) 431 | } 432 | case "image": 433 | options[option.key] = NSImage(contentsOfFile: option.value as! String); 434 | default: 435 | break; 436 | } 437 | } 438 | } 439 | if (options["width"] as? CGFloat ?? 0 != 0) { 440 | options["window_size"] = CGSize(width: options["width"] as! CGFloat, height: (options["window_size"] as! CGSize).height) 441 | } 442 | if (options["height"] as? CGFloat ?? 0 != 0) { 443 | options["window_size"] = CGSize(width: (options["window_size"] as! CGSize).width, height: options["height"] as! CGFloat) 444 | } 445 | } 446 | result_dict["status"] = "success"; 447 | } 448 | 449 | return convertDictToCReturn(dict: result_dict); 450 | } 451 | 452 | 453 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 454 | func convertToDictionary(text: String) -> [String: Any]? { 455 | if let data = text.data(using: .utf8) { 456 | do { 457 | return try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] 458 | } catch {} 459 | } 460 | return nil 461 | } 462 | func convertDictToJsonString(dict: [String: Any]) -> String? { 463 | guard let theJSONData = try? JSONSerialization.data(withJSONObject: dict, options: []) else { 464 | return nil 465 | } 466 | return String(data: theJSONData, encoding: .utf8) 467 | } 468 | func convertDictToCReturn(dict: [String: Any]) -> UnsafePointer { 469 | let return_json = convertDictToJsonString(dict: dict); 470 | return (return_json! as NSString).utf8String!; 471 | } 472 | func convertOptionsToDictionary(options: UnsafePointer) -> [String: Any]? { 473 | return convertToDictionary(text: String(cString: options)); 474 | } 475 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -------------------------------------------------------------------------------- /source/src/htmlbox/Sources/htmlbox/htmlbox.swift: -------------------------------------------------------------------------------- 1 | // swift build --package-path ./src/htmlbox -Xswiftc "-target" -Xswiftc "x86_64-apple-macosx10.15" -Xlinker -rpath -Xlinker @executable_path/Frameworks 2 | 3 | import AppKit 4 | import SwiftUI 5 | import Combine 6 | import WebKit 7 | 8 | // Variables - Defaults 9 | var gdialog_type = "htmlbox"; 10 | public typealias `func` = @convention(c) (UnsafePointer) -> Void 11 | var callbackHolder: `func`? 12 | var options: [String: Any] = [ 13 | "async": false, 14 | "html_b64": "", 15 | "file": "", 16 | "base_path": "", 17 | "url": "", 18 | "fullscreen": false, 19 | "kiosk": false, 20 | "allow_quit": false, 21 | "width": 0, 22 | "height": 0, 23 | "window_size": CGSize(width: 500, height: 150), 24 | "title": "", 25 | "focus": false, 26 | "static": false, 27 | "normal_window": false 28 | ]; 29 | 30 | let html_prefix = """ 31 | 32 | 33 | 39 | 47 | 48 | 49 | """; 50 | let html_suffix = """ 51 | 52 | 53 | """; 54 | 55 | /////////////////////////////// 56 | // TEMPLATE BEGIN - EDIT HERE// 57 | /////////////////////////////// 58 | 59 | // HTML Box UI 60 | // HTML Box UI 61 | class ViewModel: ObservableObject { 62 | var webViewNavigationPublisher = PassthroughSubject() 63 | var showLoader = PassthroughSubject() 64 | var valuePublisher = PassthroughSubject() 65 | } 66 | 67 | enum WebViewNavigation { 68 | case backward, forward 69 | } 70 | 71 | class GDWebView : WKWebView { 72 | override func willOpenMenu(_ menu: NSMenu, with event: NSEvent) { 73 | if (options["kiosk"] as! Bool) { 74 | for menuItem in menu.items { 75 | menuItem.isHidden = true 76 | } 77 | } 78 | } 79 | } 80 | 81 | struct WebView: NSViewRepresentable { 82 | // Viewmodel object 83 | @ObservedObject var viewModel: ViewModel 84 | let scriptHandler = ScriptHandler() 85 | // Make a coordinator to co-ordinate with WKWebView's default delegate functions 86 | func makeCoordinator() -> Coordinator { 87 | Coordinator(self) 88 | } 89 | 90 | func makeNSView(context: NSViewRepresentableContext) -> WKWebView { 91 | // Enable javascript in WKWebView to interact with the web app 92 | // let preferences = WKWebpagePreferences() 93 | // preferences.allowsContentJavaScript = true 94 | 95 | let contentController = WKUserContentController(); 96 | contentController.add(scriptHandler, name: "print") 97 | contentController.add(scriptHandler, name: "quit") 98 | 99 | let configuration = WKWebViewConfiguration() 100 | configuration.userContentController = contentController 101 | // configuration.defaultWebpagePreferences = preferences 102 | 103 | let webView = GDWebView(frame: CGRect.zero, configuration: configuration) 104 | webView.navigationDelegate = context.coordinator 105 | return webView 106 | } 107 | 108 | func updateNSView(_ webView: WKWebView, context: NSViewRepresentableContext) { 109 | if (options["url"] as! String != "") { 110 | webView.load(URLRequest(url: URL(string: options["url"] as! String)!)) 111 | } 112 | else { 113 | webView.loadHTMLString(getHTMLString(), baseURL: (options["base_path"] as! String) != "" ? URL(string: options["base_path"] as! String) : nil) // baseURL is the root path 114 | } 115 | } 116 | 117 | class ScriptHandler: NSObject, WKScriptMessageHandler { 118 | func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { 119 | // print(message) 120 | if message.name == "print" { 121 | if (options["async"] as! Bool) { 122 | dialogInteraction(message.body as! String) 123 | } 124 | else { 125 | print(message.body) 126 | } 127 | } 128 | if message.name == "quit" { 129 | dialogInteraction("0") 130 | // app.stop("button"); 131 | } 132 | } 133 | } 134 | 135 | class Coordinator : NSObject, WKNavigationDelegate { 136 | var parent: WebView 137 | var webViewNavigationSubscriber: AnyCancellable? = nil 138 | 139 | init(_ uiWebView: WebView) { 140 | self.parent = uiWebView 141 | } 142 | 143 | deinit { 144 | webViewNavigationSubscriber?.cancel() 145 | } 146 | 147 | // This function is essential for intercepting every navigation in the webview 148 | func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { 149 | // var urlString = navigationAction.request.url!.absoluteString 150 | // if (urlString != "about:blank") { 151 | if (options["url"] as! String != "") { 152 | decisionHandler(.allow) 153 | return 154 | } 155 | if navigationAction.request.url?.host != nil { 156 | decisionHandler(.cancel) 157 | NSWorkspace.shared.open(navigationAction.request.url!) 158 | return 159 | } 160 | decisionHandler(.allow) 161 | } 162 | } 163 | } 164 | 165 | 166 | 167 | 168 | struct ContentView: View { 169 | @ObservedObject var viewModel = ViewModel() 170 | var body: some View { 171 | VStack(spacing: 0) { 172 | WebView(viewModel: viewModel) 173 | } 174 | } 175 | } 176 | 177 | func getHTMLString() -> String { 178 | do { 179 | if (options["file"] as! String == "") { 180 | return html_prefix + String(data: Data(base64Encoded: options["html_b64"] as! String)!, encoding: .utf8)! + html_suffix; 181 | } 182 | else { 183 | let html_string = try String(contentsOfFile: options["file"] as! String) 184 | return html_prefix + html_string + html_suffix; 185 | } 186 | } 187 | catch { 188 | return ""; 189 | } 190 | } 191 | 192 | /////////////////////// 193 | // DO NOT EDIT BELOW // 194 | /////////////////////// 195 | 196 | func dialogInteraction(_ returnString: String) { 197 | if ((options["async"] as! Bool) && (returnString != "timeout")) { 198 | callbackHolder!((returnString as NSString).utf8String!); 199 | } 200 | else { 201 | dialogResult = returnString; 202 | app.stop(""); 203 | } 204 | } 205 | 206 | let app = NSApplication.shared 207 | let delegate = AppDelegate() 208 | var dialogResult = "-1"; 209 | 210 | class WindowDelegate: NSObject, NSWindowDelegate { 211 | func windowWillClose(_ notification: Notification) { 212 | NSApplication.shared.terminate(0) 213 | } 214 | } 215 | 216 | class CustomWindow: NSWindow { 217 | override var canBecomeKey: Bool { 218 | return true 219 | } 220 | } 221 | 222 | class AppDelegate: NSObject, NSApplicationDelegate { 223 | let window = CustomWindow() 224 | let windowDelegate = WindowDelegate() 225 | 226 | func applicationDidFinishLaunching(_ notification: Notification) { 227 | let screenFrame = NSScreen.main!.frame; 228 | let mainMenu = NSMenu(title: options["title"] as! String) 229 | 230 | if (options["normal_window"] as! Bool) { 231 | window.collectionBehavior = NSWindow.CollectionBehavior.fullScreenPrimary 232 | window.styleMask = [.titled, .closable, .miniaturizable, .resizable] 233 | window.title = options["title"] as! String 234 | NSApp.setActivationPolicy(.regular) 235 | options["allow_quit"] = true 236 | options["kiosk"] = false 237 | } 238 | else { 239 | if (options["title"] as? String ?? "" == "") { 240 | window.styleMask = [] 241 | } 242 | else { 243 | window.styleMask = [.titled] 244 | window.title = options["title"] as! String 245 | } 246 | NSApp.setActivationPolicy(.accessory) 247 | } 248 | 249 | if (options["allow_quit"] as! Bool) { 250 | let appMenu = NSMenuItem() 251 | appMenu.submenu = NSMenu() 252 | appMenu.submenu?.addItem(NSMenuItem(title: "Quit", action: #selector(app.stop(_:)), keyEquivalent: "q")) 253 | // let mainMenu = NSMenu(title: options["title"] as! String) 254 | 255 | mainMenu.addItem(appMenu) 256 | // NSApplication.shared.mainMenu = mainMenu 257 | } 258 | 259 | let editMenu = NSMenuItem() 260 | editMenu.submenu = NSMenu(title: "Edit") 261 | editMenu.submenu?.addItem(NSMenuItem(title: "Undo", action: Selector(("undo:")), keyEquivalent: "z")) 262 | editMenu.submenu?.addItem(NSMenuItem(title: "Redo", action: Selector(("redo:")), keyEquivalent: "y")) 263 | editMenu.submenu?.addItem(NSMenuItem.separator()) 264 | editMenu.submenu?.addItem(NSMenuItem(title: "Cut", action: Selector("cut:"), keyEquivalent: "x")) 265 | editMenu.submenu?.addItem(NSMenuItem(title: "Copy", action: Selector("copy:"), keyEquivalent: "c")) 266 | editMenu.submenu?.addItem(NSMenuItem(title: "Paste", action: Selector("paste:"), keyEquivalent: "v")) 267 | editMenu.submenu?.addItem(NSMenuItem(title: "Select All", action: #selector(NSStandardKeyBindingResponding.selectAll(_:)), keyEquivalent: "a")) 268 | mainMenu.addItem(editMenu) 269 | NSApplication.shared.mainMenu = mainMenu 270 | window.delegate = windowDelegate 271 | 272 | 273 | // if (options["title"] as? String ?? "" == "") { 274 | // window.styleMask = [] 275 | // if (options["normal_window"] as! Bool) { 276 | // window.styleMask = [.titled, .closable, .miniaturizable, .resizable, .fullScreen] 277 | // } 278 | // else { 279 | // window.styleMask = [] 280 | // } 281 | // } 282 | // else { 283 | // if (options["normal_window"] as! Bool) { 284 | // window.styleMask = [.titled, .closable, .miniaturizable, .resizable, .fullScreen] 285 | // } 286 | // else { 287 | // window.styleMask = [.titled] 288 | // } 289 | // window.title = options["title"] as! String 290 | // } 291 | 292 | if ((options["fullscreen"] as! Bool) || (options["kiosk"] as! Bool)) { 293 | window.collectionBehavior = NSWindow.CollectionBehavior.fullScreenPrimary 294 | window.setFrame(screenFrame, display: true) 295 | window.toggleFullScreen(window) 296 | options["window_size"] = CGSize(width: screenFrame.width, height: screenFrame.height) 297 | } 298 | 299 | window.setContentSize(options["window_size"] as! CGSize) 300 | 301 | let view = NSHostingView(rootView: ContentView()) 302 | view.frame = CGRect(origin: .zero, size: options["window_size"] as! CGSize) 303 | view.autoresizingMask = [.height, .width] 304 | window.contentView!.addSubview(view) 305 | window.isMovableByWindowBackground = true 306 | window.isMovable = true 307 | window.center() 308 | window.makeKeyAndOrderFront(window) 309 | 310 | 311 | // NSApp.setActivationPolicy(.accessory) 312 | if (options["focus"] as! Bool) { 313 | NSApp.activate(ignoringOtherApps: true) // Makes the window take focus 314 | } 315 | 316 | if (options["kiosk"] as! Bool) { 317 | let presOptions: NSApplication.PresentationOptions = [ 318 | .disableForceQuit , // Cmd+Opt+Esc panel is disabled 319 | .disableMenuBarTransparency, // Menu Bar's transparent appearance is disabled 320 | // .disableQuit , 321 | .fullScreen , // Application is in fullscreen mode 322 | .hideDock , // Dock is entirely unavailable. Spotlight menu is disabled. 323 | .hideMenuBar , // Menu Bar is Disabled 324 | .disableAppleMenu , // All Apple menu items are disabled. 325 | .disableProcessSwitching , // Cmd+Tab UI is disabled. All Exposé functionality is also disabled. 326 | .disableSessionTermination , // PowerKey panel and Restart/Shut Down/Log Out are disabled. 327 | .disableHideApplication , // Application "Hide" menu item is disabled. 328 | .autoHideToolbar 329 | 330 | ] 331 | let optionsDictionary = [NSView.FullScreenModeOptionKey.fullScreenModeAllScreens : NSNumber(value: presOptions.rawValue)] 332 | // let optionsDictionary = [NSView.FullScreenModeOptionKey.fullScreenModeApplicationPresentationOptions : NSNumber(value: presOptions.rawValue)] 333 | view.enterFullScreenMode(NSScreen.main!, withOptions:optionsDictionary) 334 | } 335 | 336 | } 337 | } 338 | 339 | 340 | 341 | 342 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 343 | 344 | @_cdecl("swift_run_dialog") 345 | public func run_dialog(callBack:@escaping `func`) -> UnsafePointer { 346 | callbackHolder = callBack; 347 | let delegate = AppDelegate() 348 | app.delegate = delegate 349 | app.run() 350 | return (dialogResult as NSString).utf8String!; 351 | } 352 | 353 | @_cdecl("swift_set_options") 354 | public func set_options(_options: UnsafePointer) -> UnsafePointer { 355 | var result_dict: [String: Any] = [ 356 | "status" as String: "failed" 357 | ]; 358 | 359 | if let options_dict = convertOptionsToDictionary(options: _options) { 360 | if let value = options_dict["type"] as? String { 361 | if gdialog_type != value { 362 | result_dict["error"] = "gDialog type does not match."; 363 | return convertDictToCReturn(dict: result_dict); 364 | } 365 | } 366 | 367 | 368 | if let option_types = options_dict["option_types"] as? [String: String] { 369 | options_dict.forEach { option in 370 | if (options[option.key] != nil) { 371 | switch option_types[option.key] { 372 | case "string": 373 | options[option.key] = option.value as! String; 374 | case "array": 375 | options[option.key] = option.value as! [String]; 376 | case "bool-true": 377 | options[option.key] = true; 378 | case "bool-false": 379 | options[option.key] = false; 380 | case "float": 381 | options[option.key] = option.value as! CGFloat; 382 | case "int": 383 | options[option.key] = option.value as! Int; 384 | case "size": 385 | if let size_value = option.value as? [String: CGFloat] { 386 | options[option.key] = CGSize(width: size_value["width"]! as CGFloat, height: size_value["height"]! as CGFloat) 387 | } 388 | case "image": 389 | options[option.key] = NSImage(contentsOfFile: option.value as! String); 390 | default: 391 | break; 392 | } 393 | } 394 | } 395 | if (options["width"] as? CGFloat ?? 0 != 0) { 396 | options["window_size"] = CGSize(width: options["width"] as! CGFloat, height: (options["window_size"] as! CGSize).height) 397 | } 398 | if (options["height"] as? CGFloat ?? 0 != 0) { 399 | options["window_size"] = CGSize(width: (options["window_size"] as! CGSize).width, height: options["height"] as! CGFloat) 400 | } 401 | } 402 | result_dict["status"] = "success"; 403 | } 404 | 405 | return convertDictToCReturn(dict: result_dict); 406 | } 407 | 408 | 409 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 410 | func convertToDictionary(text: String) -> [String: Any]? { 411 | if let data = text.data(using: .utf8) { 412 | do { 413 | return try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] 414 | } catch {} 415 | } 416 | return nil 417 | } 418 | func convertDictToJsonString(dict: [String: Any]) -> String? { 419 | guard let theJSONData = try? JSONSerialization.data(withJSONObject: dict, options: []) else { 420 | return nil 421 | } 422 | return String(data: theJSONData, encoding: .utf8) 423 | } 424 | func convertDictToCReturn(dict: [String: Any]) -> UnsafePointer { 425 | let return_json = convertDictToJsonString(dict: dict); 426 | return (return_json! as NSString).utf8String!; 427 | } 428 | func convertOptionsToDictionary(options: UnsafePointer) -> [String: Any]? { 429 | return convertToDictionary(text: String(cString: options)); 430 | } 431 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -------------------------------------------------------------------------------- /source/src/pickerbox/Sources/pickerbox/pickerbox.swift: -------------------------------------------------------------------------------- 1 | // swift build --package-path ./src/pickerbox -Xswiftc "-target" -Xswiftc "x86_64-apple-macosx10.15" -Xlinker -rpath -Xlinker @executable_path/Frameworks 2 | 3 | import AppKit 4 | import SwiftUI 5 | 6 | 7 | // Variables - Defaults 8 | var gdialog_type = "pickerbox"; 9 | public typealias `func` = @convention(c) (UnsafePointer) -> Void 10 | var callbackHolder: `func`? 11 | var options: [String: Any] = [ 12 | "async": false, 13 | "items": [], 14 | "style": "default", 15 | "header": "", 16 | "allow_quit": false, 17 | "width": 0, 18 | "height": 0, 19 | "window_size": CGSize(width: 500, height: 150), 20 | "icon_file": "", // NSImage 21 | "system_icon": "", 22 | "icon_type": "none", 23 | "title": "", 24 | "text": "", 25 | "buttons": [ "OK" ], 26 | "scrollable_text": false, 27 | "focus": false, 28 | "timeout": 0, 29 | "timeout_text": "Time Remaining", 30 | "static": false 31 | ]; 32 | 33 | // Picker Box UI 34 | struct ContentView: View { 35 | @State private var dropdown_options: [String] = options["items"] as! [String]; 36 | @State private var selected_option = 0; 37 | var body: some View { 38 | VStack(spacing: 0) { 39 | HeaderView() 40 | BodyView(dropdown_options: $dropdown_options, selected_option: $selected_option) 41 | FooterView(dropdown_options: $dropdown_options, selected_option: $selected_option) 42 | } 43 | .padding(5) 44 | } 45 | } 46 | 47 | struct HeaderView: View { 48 | var body: some View { 49 | VStack() { 50 | Text(options["header"] as! String).font(.title) 51 | } 52 | .frame(maxWidth: .infinity) 53 | .padding(.bottom, 3) 54 | } 55 | } 56 | 57 | struct BodyView: View { 58 | @Binding var dropdown_options: [String]; 59 | @Binding var selected_option: Int; 60 | 61 | var body: some View { 62 | HStack(alignment: .top) { 63 | if (options["icon_type"] as! String != "none") { 64 | IconView() 65 | } 66 | MainView(dropdown_options: $dropdown_options, selected_option: $selected_option) 67 | } 68 | .frame(maxHeight: .infinity) 69 | } 70 | } 71 | 72 | struct IconView: View { 73 | var body: some View { 74 | VStack() { 75 | if options["icon_type"] as! String == "file" { 76 | Image(nsImage: options["icon_file"] as! NSImage) 77 | .resizable() 78 | .aspectRatio(contentMode: .fit) 79 | .frame(maxWidth: 75) 80 | } 81 | else { 82 | Image(nsImage: NSImage(named: (options["system_icon"] as! String).starts(with: "NSImageName") ? (options["system_icon"] as! String).replacingOccurrences(of: "NSImageName", with: "NS") : options["system_icon"] as! String)!) 83 | .resizable() 84 | .aspectRatio(contentMode: .fit) 85 | .frame(width: 75) 86 | 87 | } 88 | } 89 | .frame(maxHeight: .infinity) 90 | } 91 | } 92 | 93 | struct MainView: View { 94 | @Binding var dropdown_options: [String]; 95 | @Binding var selected_option: Int; 96 | 97 | var body: some View { 98 | VStack { 99 | if (options["scrollable_text"] as! Bool) { 100 | ScrollView { 101 | TextView() 102 | } 103 | } 104 | else { 105 | TextView() 106 | } 107 | PickerView(dropdown_options: $dropdown_options, selected_option: $selected_option) 108 | } 109 | .frame(maxHeight: .infinity, alignment: .top) 110 | .padding(.leading, 5) 111 | } 112 | } 113 | 114 | 115 | struct TextView: View { 116 | var body: some View { 117 | VStack(alignment: .leading) { 118 | Text(options["text"] as! String) 119 | .font(.body) 120 | .frame(maxWidth: .infinity, alignment: .leading) 121 | .multilineTextAlignment(.leading) 122 | } 123 | } 124 | } 125 | 126 | struct PickerView: View { 127 | @Binding var dropdown_options: [String]; 128 | @Binding var selected_option: Int; 129 | var styleOption = options["style"] as? String ?? "default"; 130 | 131 | var body: some View { 132 | VStack { 133 | if (styleOption == "radio") { 134 | Picker("", selection: $selected_option) { 135 | ForEach(0 ..< dropdown_options.count) { 136 | Text(self.dropdown_options[$0]) 137 | .font(.body) 138 | } 139 | } 140 | .labelsHidden() 141 | .frame(maxWidth: .infinity, alignment: .leading) 142 | .pickerStyle(RadioGroupPickerStyle()) 143 | } 144 | else if (styleOption == "segmented") { 145 | Picker("", selection: $selected_option) { 146 | ForEach(0 ..< dropdown_options.count) { 147 | Text(self.dropdown_options[$0]) 148 | .font(.body) 149 | } 150 | } 151 | .labelsHidden() 152 | .frame(maxWidth: .infinity, alignment: .leading) 153 | .pickerStyle(SegmentedPickerStyle()) 154 | } 155 | else { 156 | Picker("", selection: $selected_option) { 157 | ForEach(0 ..< dropdown_options.count) { 158 | Text(self.dropdown_options[$0]) 159 | .font(.body) 160 | } 161 | } 162 | .labelsHidden() 163 | .frame(maxWidth: .infinity, alignment: .leading) 164 | .pickerStyle(DefaultPickerStyle()) 165 | } 166 | 167 | } 168 | 169 | } 170 | } 171 | 172 | struct FooterView: View { 173 | @Binding var dropdown_options: [String]; 174 | @Binding var selected_option: Int; 175 | 176 | var body: some View { 177 | HStack() { 178 | if (options["timeout"] as! Int > 0) { 179 | TimerView() 180 | } 181 | ButtonsView(dropdown_options: $dropdown_options, selected_option: $selected_option) 182 | } 183 | } 184 | } 185 | 186 | struct TimerView: View { 187 | @State private var timeRemaining = options["timeout"] as! Int; 188 | let timer = Timer.publish(every: 1, on: .current, in: .common).autoconnect() 189 | var body: some View { 190 | HStack() { 191 | Text("\(options["timeout_text"] as! String): \(buildRemainingTime(timeRemaining))") 192 | } 193 | .padding(.leading, 5) 194 | .onReceive(timer) { time in 195 | if self.timeRemaining > 0 { 196 | self.timeRemaining -= 1 197 | } 198 | else { 199 | timer.upstream.connect().cancel() 200 | dialogResult = ""; 201 | dialogInteraction("timeout"); 202 | let dummyEvent = NSEvent.otherEvent(with: NSEvent.EventType.applicationDefined, location: NSZeroPoint, modifierFlags: NSEvent.ModifierFlags(rawValue: 0), timestamp: 0, windowNumber: 0, context: nil, subtype:0, data1:0, data2:0)!; 203 | NSApp.postEvent(dummyEvent, atStart: true); 204 | } 205 | } 206 | } 207 | } 208 | 209 | func buildRemainingTime(_ time: Int) -> String { 210 | var remainingTime: Int = time; 211 | var days = 0; 212 | var hours = 0; 213 | var minutes = 0; 214 | var seconds = 0; 215 | 216 | if (remainingTime > 59) { 217 | seconds = remainingTime % 60; 218 | remainingTime = remainingTime / 60; 219 | if (remainingTime > 59) { 220 | minutes = remainingTime % 60; 221 | remainingTime = remainingTime / 60; 222 | if (remainingTime > 23) { 223 | hours = remainingTime % 24; 224 | days = remainingTime / 24; 225 | } 226 | else { 227 | hours = remainingTime; 228 | } 229 | } 230 | else { 231 | minutes = remainingTime; 232 | } 233 | } 234 | else { 235 | seconds = remainingTime; 236 | } 237 | var result = days > 0 ? String(days) + " " : ""; 238 | result += (hours < 10 ? "0" : "") + String(hours); 239 | result += ":" + (minutes < 10 ? "0" : "") + String(minutes); 240 | result += ":" + (seconds < 10 ? "0" : "") + String(seconds); 241 | 242 | return result; 243 | } 244 | 245 | var buttonCount: Int = 0; 246 | func increaseButtonCount() -> Int { 247 | buttonCount += 1; 248 | return buttonCount; 249 | } 250 | struct ButtonsView : View { 251 | @Binding var dropdown_options: [String]; 252 | @Binding var selected_option: Int; 253 | 254 | var body: some View { 255 | HStack { 256 | ForEach(options["buttons"] as! [String], id: \.self) { 257 | ButtonView( 258 | dropdown_options: $dropdown_options, 259 | selected_option: $selected_option, 260 | labelText: "\($0)", 261 | actionReturn: String(increaseButtonCount()) 262 | ) 263 | } 264 | } 265 | .frame(maxWidth: .infinity, alignment: .leading) 266 | .environment(\.layoutDirection, .rightToLeft) 267 | .padding(.top, 3) 268 | } 269 | } 270 | struct ButtonView: View { 271 | @Binding var dropdown_options: [String]; 272 | @Binding var selected_option: Int; 273 | 274 | var labelText: String = "Button"; 275 | var actionReturn: String = "0"; 276 | 277 | var body: some View { 278 | HStack { 279 | Button(action: buttonClicked, label: { 280 | Text(labelText) 281 | .frame(minWidth: 50) 282 | }) 283 | .padding(.horizontal, 5) 284 | .padding(.vertical, 2) 285 | } 286 | 287 | } 288 | 289 | func buttonClicked() { 290 | dialogInteraction(actionReturn + "\n" + String(selected_option + 1)); 291 | // app.stop("button"); 292 | } 293 | } 294 | 295 | /////////////////////// 296 | // DO NOT EDIT BELOW // 297 | /////////////////////// 298 | 299 | func dialogInteraction(_ returnString: String) { 300 | if ((options["async"] as! Bool) && (returnString != "timeout")) { 301 | callbackHolder!((returnString as NSString).utf8String!); 302 | } 303 | else { 304 | dialogResult = returnString; 305 | app.stop(""); 306 | } 307 | } 308 | 309 | let app = NSApplication.shared 310 | let delegate = AppDelegate() 311 | var dialogResult = "-1"; 312 | 313 | class WindowDelegate: NSObject, NSWindowDelegate { 314 | func windowWillClose(_ notification: Notification) { 315 | NSApplication.shared.terminate(0) 316 | } 317 | } 318 | 319 | class CustomWindow: NSWindow { 320 | override var canBecomeKey: Bool { 321 | return true 322 | } 323 | } 324 | 325 | class AppDelegate: NSObject, NSApplicationDelegate { 326 | let window = CustomWindow() 327 | let windowDelegate = WindowDelegate() 328 | 329 | func applicationDidFinishLaunching(_ notification: Notification) { 330 | let mainMenu = NSMenu(title: options["title"] as! String) 331 | if (options["allow_quit"] as! Bool) { 332 | let appMenu = NSMenuItem() 333 | appMenu.submenu = NSMenu() 334 | appMenu.submenu?.addItem(NSMenuItem(title: "Quit", action: #selector(app.stop(_:)), keyEquivalent: "q")) 335 | mainMenu.addItem(appMenu) 336 | } 337 | let editMenu = NSMenuItem() 338 | editMenu.submenu = NSMenu(title: "Edit") 339 | editMenu.submenu?.addItem(NSMenuItem(title: "Undo", action: Selector(("undo:")), keyEquivalent: "z")) 340 | editMenu.submenu?.addItem(NSMenuItem(title: "Redo", action: Selector(("redo:")), keyEquivalent: "y")) 341 | editMenu.submenu?.addItem(NSMenuItem.separator()) 342 | editMenu.submenu?.addItem(NSMenuItem(title: "Cut", action: #selector(NSText.cut(_:)), keyEquivalent: "x")) 343 | editMenu.submenu?.addItem(NSMenuItem(title: "Copy", action: #selector(NSText.copy(_:)), keyEquivalent: "c")) 344 | editMenu.submenu?.addItem(NSMenuItem(title: "Paste", action: #selector(NSText.paste(_:)), keyEquivalent: "v")) 345 | editMenu.submenu?.addItem(NSMenuItem(title: "Select All", action: #selector(NSStandardKeyBindingResponding.selectAll(_:)), keyEquivalent: "a")) 346 | mainMenu.addItem(editMenu) 347 | NSApplication.shared.mainMenu = mainMenu 348 | window.setContentSize(options["window_size"] as! CGSize) 349 | window.delegate = windowDelegate 350 | if (options["title"] as? String ?? "" == "") { 351 | window.styleMask = [] 352 | } 353 | else { 354 | window.styleMask = [.titled] 355 | window.title = options["title"] as! String 356 | } 357 | 358 | let view = NSHostingView(rootView: ContentView()) 359 | view.frame = CGRect(origin: .zero, size: options["window_size"] as! CGSize) 360 | view.autoresizingMask = [.height, .width] 361 | window.contentView!.addSubview(view) 362 | if (options["static"] as! Bool) { 363 | window.isMovableByWindowBackground = false 364 | window.isMovable = false 365 | } 366 | else { 367 | window.isMovableByWindowBackground = true 368 | window.isMovable = true 369 | } 370 | window.center() 371 | window.makeKeyAndOrderFront(window) 372 | 373 | NSApp.setActivationPolicy(.accessory) 374 | if (options["focus"] as! Bool) { 375 | NSApp.activate(ignoringOtherApps: true) // Makes the window take focus 376 | } 377 | } 378 | } 379 | 380 | @_cdecl("swift_run_dialog") 381 | public func run_dialog(callBack:@escaping `func`) -> UnsafePointer { 382 | callbackHolder = callBack; 383 | let delegate = AppDelegate() 384 | app.delegate = delegate 385 | app.run() 386 | return (dialogResult as NSString).utf8String!; 387 | } 388 | 389 | @_cdecl("swift_set_options") 390 | public func set_options(_options: UnsafePointer) -> UnsafePointer { 391 | var result_dict: [String: Any] = [ 392 | "status" as String: "failed" 393 | ]; 394 | if let options_dict = convertOptionsToDictionary(options: _options) { 395 | if let value = options_dict["type"] as? String { 396 | if gdialog_type != value { 397 | result_dict["error"] = "gDialog type does not match."; 398 | return convertDictToCReturn(dict: result_dict); 399 | } 400 | } 401 | 402 | 403 | if let option_types = options_dict["option_types"] as? [String: String] { 404 | options_dict.forEach { option in 405 | if (options[option.key] != nil) { 406 | switch option_types[option.key] { 407 | case "string": 408 | options[option.key] = option.value as! String; 409 | case "array": 410 | options[option.key] = option.value as! [String]; 411 | case "bool-true": 412 | options[option.key] = true; 413 | case "bool-false": 414 | options[option.key] = false; 415 | case "float": 416 | options[option.key] = option.value as! CGFloat; 417 | case "int": 418 | options[option.key] = option.value as! Int; 419 | case "size": 420 | if let size_value = option.value as? [String: CGFloat] { 421 | options[option.key] = CGSize(width: size_value["width"]! as CGFloat, height: size_value["height"]! as CGFloat) 422 | } 423 | case "image": 424 | options[option.key] = NSImage(contentsOfFile: option.value as! String); 425 | default: 426 | break; 427 | } 428 | } 429 | } 430 | if (options["width"] as? CGFloat ?? 0 != 0) { 431 | options["window_size"] = CGSize(width: options["width"] as! CGFloat, height: (options["window_size"] as! CGSize).height) 432 | } 433 | if (options["height"] as? CGFloat ?? 0 != 0) { 434 | options["window_size"] = CGSize(width: (options["window_size"] as! CGSize).width, height: options["height"] as! CGFloat) 435 | } 436 | } 437 | result_dict["status"] = "success"; 438 | } 439 | 440 | return convertDictToCReturn(dict: result_dict); 441 | } 442 | 443 | 444 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 445 | func convertToDictionary(text: String) -> [String: Any]? { 446 | if let data = text.data(using: .utf8) { 447 | do { 448 | return try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] 449 | } catch {} 450 | } 451 | return nil 452 | } 453 | func convertDictToJsonString(dict: [String: Any]) -> String? { 454 | guard let theJSONData = try? JSONSerialization.data(withJSONObject: dict, options: []) else { 455 | return nil 456 | } 457 | return String(data: theJSONData, encoding: .utf8) 458 | } 459 | func convertDictToCReturn(dict: [String: Any]) -> UnsafePointer { 460 | let return_json = convertDictToJsonString(dict: dict); 461 | return (return_json! as NSString).utf8String!; 462 | } 463 | func convertOptionsToDictionary(options: UnsafePointer) -> [String: Any]? { 464 | return convertToDictionary(text: String(cString: options)); 465 | } 466 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -------------------------------------------------------------------------------- /source/src/bannerbox/Sources/bannerbox/bannerbox.swift: -------------------------------------------------------------------------------- 1 | // swift build --package-path ./src/bannerbox -Xswiftc "-target" -Xswiftc "x86_64-apple-macosx10.15" -Xlinker -rpath -Xlinker @executable_path/Frameworks 2 | 3 | import AppKit 4 | import SwiftUI 5 | 6 | 7 | // Variables - Defaults 8 | var gdialog_type = "bannerbox"; 9 | public typealias `func` = @convention(c) (UnsafePointer) -> Void 10 | var callbackHolder: `func`? 11 | var options: [String: Any] = [ 12 | "async": false, 13 | "background_color": "", 14 | "header": "", 15 | "allow_quit": false, 16 | "width": 0, 17 | "height": 0, 18 | "window_size": CGSize(width: 340, height: 60), 19 | "icon_file": "", // NSImage 20 | "system_icon": "", 21 | "icon_type": "none", 22 | "text": "", 23 | "buttons": [ ], 24 | "scrollable_text": false, 25 | "focus": false, 26 | "timeout": 10, 27 | "no_timeout": false 28 | ]; 29 | 30 | // Banner Box UI 31 | struct ContentView: View { 32 | @State private var isDialogHovered: Bool = false; 33 | @State private var bgColor: Color = (options["background_color"] as! String == "") ? Color(NSColor.windowBackgroundColor) : Color(hex: options["background_color"] as! String); 34 | 35 | @State private var timeRemaining = options["timeout"] as! Int; 36 | let timer = Timer.publish(every: 1, on: .current, in: .common).autoconnect() 37 | 38 | var body: some View { 39 | HStack(spacing: 0) { 40 | BodyView() 41 | if ((options["buttons"] as! [String]).count > 0) { 42 | ButtonsView(bgColor: $bgColor, isDialogHovered: $isDialogHovered) 43 | } 44 | } 45 | .frame(maxHeight: .infinity) 46 | .foregroundColor(Color(NSColor.windowFrameTextColor)) 47 | .background(bgColor) 48 | // .opacity(isDialogHovered ? 1.0 : 0.8) 49 | .opacity(0.9) 50 | .cornerRadius(8) 51 | .onReceive(timer) { time in 52 | if (options["no_timeout"] as! Bool == false) { 53 | if self.timeRemaining > 0 { 54 | self.timeRemaining -= 1 55 | } 56 | else { 57 | timer.upstream.connect().cancel() 58 | dialogResult = ""; 59 | dialogInteraction("timeout"); 60 | let dummyEvent = NSEvent.otherEvent(with: NSEvent.EventType.applicationDefined, location: NSZeroPoint, modifierFlags: NSEvent.ModifierFlags(rawValue: 0), timestamp: 0, windowNumber: 0, context: nil, subtype:0, data1:0, data2:0)!; 61 | NSApp.postEvent(dummyEvent, atStart: true); 62 | } 63 | } 64 | } 65 | .onTapGesture { 66 | if ((options["buttons"] as! [String]).count == 0) { 67 | dialogInteraction("0"); 68 | // app.stop("tap"); 69 | } 70 | } 71 | // .onHover { _ in 72 | // self.isDialogHovered.toggle(); 73 | // } 74 | .animation(.default) 75 | } 76 | } 77 | 78 | 79 | // https://stackoverflow.com/a/56874327 80 | extension Color { 81 | init(hex: String) { 82 | let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) 83 | var int: UInt64 = 0 84 | Scanner(string: hex).scanHexInt64(&int) 85 | let a, r, g, b: UInt64 86 | switch hex.count { 87 | case 3: // RGB (12-bit) 88 | (a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17) 89 | case 6: // RGB (24-bit) 90 | (a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF) 91 | case 8: // ARGB (32-bit) 92 | (a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF) 93 | default: 94 | (a, r, g, b) = (1, 1, 1, 0) 95 | } 96 | 97 | self.init( 98 | .sRGB, 99 | red: Double(r) / 255, 100 | green: Double(g) / 255, 101 | blue: Double(b) / 255, 102 | opacity: Double(a) / 255 103 | ) 104 | } 105 | } 106 | 107 | 108 | 109 | 110 | struct BodyView: View { 111 | var body: some View { 112 | HStack(alignment: .top, spacing: 0) { 113 | if (options["icon_type"] as! String != "none") { 114 | IconView() 115 | } 116 | MainView() 117 | .padding(.leading, 5) 118 | .padding(.top, 5) 119 | } 120 | .padding(.leading, 5) 121 | .frame(maxHeight: .infinity) 122 | } 123 | } 124 | 125 | struct IconView: View { 126 | var body: some View { 127 | VStack() { 128 | if options["icon_type"] as! String == "file" { 129 | Image(nsImage: options["icon_file"] as! NSImage) 130 | .resizable() 131 | .aspectRatio(contentMode: .fit) 132 | .frame(maxWidth: 50) 133 | } 134 | else { 135 | Image(nsImage: NSImage(named: (options["system_icon"] as! String).starts(with: "NSImageName") ? (options["system_icon"] as! String).replacingOccurrences(of: "NSImageName", with: "NS") : options["system_icon"] as! String)!) 136 | .resizable() 137 | .aspectRatio(contentMode: .fit) 138 | .frame(width: 50) 139 | 140 | } 141 | } 142 | .frame(maxHeight: .infinity) 143 | } 144 | } 145 | 146 | struct MainView: View { 147 | var body: some View { 148 | VStack(spacing: 0) { 149 | if (options["header"] as! String != "") { 150 | HeaderView() 151 | } 152 | if (options["scrollable_text"] as! Bool) { 153 | ScrollView { 154 | TextView() 155 | } 156 | } 157 | else { 158 | TextView() 159 | } 160 | } 161 | .frame(maxHeight: .infinity, alignment: .top) 162 | .frame(maxWidth: .infinity, alignment: .leading) 163 | } 164 | } 165 | 166 | struct HeaderView: View { 167 | var body: some View { 168 | VStack() { 169 | Text(options["header"] as! String).font(.headline) 170 | } 171 | .frame(maxWidth: .infinity, alignment: .leading) 172 | .padding(.bottom, 3) 173 | } 174 | } 175 | 176 | struct TextView: View { 177 | var body: some View { 178 | VStack(alignment: .leading) { 179 | Text(options["text"] as! String) 180 | .font(.body) 181 | .frame(maxWidth: .infinity, alignment: .leading) 182 | .multilineTextAlignment(.leading) 183 | } 184 | } 185 | } 186 | 187 | struct ButtonsView : View { 188 | let buttons = options["buttons"] as! [String]; 189 | @State var maximumSubViewWidth: CGFloat = 0 190 | @Binding var bgColor: Color; 191 | @Binding var isDialogHovered: Bool; 192 | 193 | let borderColor: Color = Color(NSColor.separatorColor); 194 | var body: some View { 195 | VStack() { 196 | ButtonView( 197 | labelText: buttons[0], 198 | actionReturn: "1", 199 | maximumSubViewWidth: $maximumSubViewWidth, 200 | bgColor: $bgColor, 201 | isDialogHovered: $isDialogHovered 202 | ) 203 | .border(width: 1, edges: [.leading], color: borderColor) 204 | if (buttons.count > 1) { 205 | ButtonView( 206 | labelText: buttons[1], 207 | actionReturn: "2", 208 | maximumSubViewWidth: $maximumSubViewWidth, 209 | bgColor: $bgColor, 210 | isDialogHovered: $isDialogHovered 211 | ) 212 | .border(width: 1, edges: [.leading, .top], color: borderColor) 213 | } 214 | } 215 | .frame(maxHeight: .infinity) 216 | .onPreferenceChange(DetermineWidth.Key.self) { 217 | maximumSubViewWidth = $0 218 | } 219 | } 220 | } 221 | struct ButtonView: View { 222 | var labelText: String = "Button"; 223 | var actionReturn: String = "0"; 224 | @Binding var maximumSubViewWidth: CGFloat; 225 | @Binding var bgColor: Color; 226 | @Binding var isDialogHovered: Bool; 227 | @State private var isHovering: Bool = false; 228 | @State private var isPressed: Bool = false; 229 | 230 | var body: some View { 231 | HStack { 232 | Text(labelText) 233 | .padding(.leading, 10) 234 | .padding(.trailing, 10) 235 | 236 | } 237 | .frame(maxHeight: .infinity) 238 | .frame(minWidth: maximumSubViewWidth) 239 | .overlay(DetermineWidth()) 240 | .background(isPressed ? Color.accentColor : isHovering ? Color.secondary : bgColor) 241 | .contentShape(Rectangle()) // Makes whole view tappable 242 | // .opacity(isDialogHovered ? 1.0 : 0.8) 243 | .opacity(0.9) 244 | .onTapGesture { 245 | buttonClicked(); 246 | } 247 | .onHover { _ in 248 | self.isHovering.toggle(); 249 | } 250 | .onLongPressGesture(minimumDuration: 10, pressing: { _ in 251 | self.isPressed.toggle(); 252 | }) {} 253 | 254 | } 255 | 256 | func buttonClicked() { 257 | dialogInteraction(actionReturn); 258 | // app.stop("button"); 259 | } 260 | } 261 | 262 | var buttons_width: CGFloat = 0; 263 | struct DetermineWidth: View { 264 | typealias Key = MaximumWidthPreferenceKey 265 | var body: some View { 266 | GeometryReader { 267 | proxy in 268 | Color.clear 269 | .anchorPreference(key: Key.self, value: .bounds) { 270 | anchor in proxy[anchor].size.width 271 | } 272 | } 273 | } 274 | } 275 | 276 | struct MaximumWidthPreferenceKey: PreferenceKey { 277 | static var defaultValue: CGFloat = 0 278 | static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { 279 | value = max(value, nextValue()) 280 | buttons_width = value 281 | } 282 | } 283 | 284 | 285 | // Border with specific edge / side https://stackoverflow.com/a/58632759 286 | extension View { 287 | func border(width: CGFloat, edges: [Edge], color: Color) -> some View { 288 | overlay(EdgeBorder(width: width, edges: edges).foregroundColor(color)) 289 | } 290 | } 291 | 292 | struct EdgeBorder: Shape { 293 | var width: CGFloat 294 | var edges: [Edge] 295 | 296 | func path(in rect: CGRect) -> Path { 297 | var path = Path() 298 | for edge in edges { 299 | var x: CGFloat { 300 | switch edge { 301 | case .top, .bottom, .leading: return rect.minX 302 | case .trailing: return rect.maxX - width 303 | } 304 | } 305 | 306 | var y: CGFloat { 307 | switch edge { 308 | case .top, .leading, .trailing: return rect.minY 309 | case .bottom: return rect.maxY - width 310 | } 311 | } 312 | 313 | var w: CGFloat { 314 | switch edge { 315 | case .top, .bottom: return rect.width 316 | case .leading, .trailing: return self.width 317 | } 318 | } 319 | 320 | var h: CGFloat { 321 | switch edge { 322 | case .top, .bottom: return self.width 323 | case .leading, .trailing: return rect.height 324 | } 325 | } 326 | path.addPath(Path(CGRect(x: x, y: y, width: w, height: h))) 327 | } 328 | return path 329 | } 330 | } 331 | 332 | /////////////////////// 333 | // DO NOT EDIT BELOW // 334 | /////////////////////// 335 | 336 | func dialogInteraction(_ returnString: String) { 337 | if ((options["async"] as! Bool) && (returnString != "timeout")) { 338 | callbackHolder!((returnString as NSString).utf8String!); 339 | } 340 | else { 341 | dialogResult = returnString; 342 | app.stop(""); 343 | } 344 | } 345 | 346 | let app = NSApplication.shared 347 | let delegate = AppDelegate() 348 | var dialogResult = "-1"; 349 | 350 | class WindowDelegate: NSObject, NSWindowDelegate { 351 | func windowWillClose(_ notification: Notification) { 352 | NSApplication.shared.terminate(0) 353 | } 354 | } 355 | 356 | class CustomWindow: NSWindow { 357 | override var canBecomeKey: Bool { 358 | return true 359 | } 360 | } 361 | 362 | class AppDelegate: NSObject, NSApplicationDelegate { 363 | let window = CustomWindow() 364 | let windowDelegate = WindowDelegate() 365 | 366 | func applicationDidFinishLaunching(_ notification: Notification) { 367 | let screenFrame = NSScreen.main!.frame; 368 | if (options["allow_quit"] as! Bool) { 369 | let appMenu = NSMenuItem() 370 | appMenu.submenu = NSMenu() 371 | appMenu.submenu?.addItem(NSMenuItem(title: "Quit", action: #selector(app.stop(_:)), keyEquivalent: "q")) 372 | let mainMenu = NSMenu(title: "") 373 | mainMenu.addItem(appMenu) 374 | NSApplication.shared.mainMenu = mainMenu 375 | } 376 | window.setContentSize(options["window_size"] as! CGSize) 377 | window.delegate = windowDelegate 378 | window.styleMask = [] 379 | window.alphaValue = 0; 380 | window.backgroundColor = NSColor.clear; 381 | let view = NSHostingView(rootView: ContentView()) 382 | view.frame = CGRect(origin: .zero, size: options["window_size"] as! CGSize) 383 | view.autoresizingMask = [.height, .width] 384 | window.contentView!.addSubview(view) 385 | window.isMovableByWindowBackground = false 386 | window.isMovable = false 387 | window.orderFrontRegardless() 388 | window.makeKeyAndOrderFront(window) 389 | let size = options["window_size"] as! CGSize; 390 | window.setFrameOrigin(NSMakePoint(NSWidth(screenFrame) - size.width, NSHeight(screenFrame) - size.height)) 391 | NSAnimationContext.runAnimationGroup({ context in 392 | context.duration = 1.0 393 | context.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.default) 394 | window.animator().alphaValue = 1 395 | }, completionHandler: nil) 396 | 397 | NSApp.setActivationPolicy(.accessory) 398 | 399 | if (options["focus"] as! Bool) { 400 | NSApp.activate(ignoringOtherApps: true) // Makes the window take focus 401 | } 402 | } 403 | } 404 | 405 | @_cdecl("swift_run_dialog") 406 | public func run_dialog(callBack:@escaping `func`) -> UnsafePointer { 407 | callbackHolder = callBack; 408 | let delegate = AppDelegate() 409 | app.delegate = delegate 410 | app.run() 411 | return (dialogResult as NSString).utf8String!; 412 | } 413 | 414 | @_cdecl("swift_set_options") 415 | public func set_options(_options: UnsafePointer) -> UnsafePointer { 416 | var result_dict: [String: Any] = [ 417 | "status" as String: "failed" 418 | ]; 419 | 420 | if let options_dict = convertOptionsToDictionary(options: _options) { 421 | if let value = options_dict["type"] as? String { 422 | if gdialog_type != value { 423 | result_dict["error"] = "gDialog type does not match."; 424 | return convertDictToCReturn(dict: result_dict); 425 | } 426 | } 427 | 428 | 429 | if let option_types = options_dict["option_types"] as? [String: String] { 430 | options_dict.forEach { option in 431 | if (options[option.key] != nil) { 432 | switch option_types[option.key] { 433 | case "string": 434 | options[option.key] = option.value as! String; 435 | case "array": 436 | options[option.key] = option.value as! [String]; 437 | case "bool-true": 438 | options[option.key] = true; 439 | case "bool-false": 440 | options[option.key] = false; 441 | case "float": 442 | options[option.key] = option.value as! CGFloat; 443 | case "int": 444 | options[option.key] = option.value as! Int; 445 | case "size": 446 | if let size_value = option.value as? [String: CGFloat] { 447 | options[option.key] = CGSize(width: size_value["width"]! as CGFloat, height: size_value["height"]! as CGFloat) 448 | } 449 | case "image": 450 | options[option.key] = NSImage(contentsOfFile: option.value as! String); 451 | default: 452 | break; 453 | } 454 | } 455 | } 456 | if (options["width"] as? CGFloat ?? 0 != 0) { 457 | options["window_size"] = CGSize(width: options["width"] as! CGFloat, height: (options["window_size"] as! CGSize).height) 458 | } 459 | if (options["height"] as? CGFloat ?? 0 != 0) { 460 | options["window_size"] = CGSize(width: (options["window_size"] as! CGSize).width, height: options["height"] as! CGFloat) 461 | } 462 | } 463 | result_dict["status"] = "success"; 464 | } 465 | 466 | return convertDictToCReturn(dict: result_dict); 467 | } 468 | 469 | 470 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 471 | func convertToDictionary(text: String) -> [String: Any]? { 472 | if let data = text.data(using: .utf8) { 473 | do { 474 | return try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] 475 | } catch {} 476 | } 477 | return nil 478 | } 479 | func convertDictToJsonString(dict: [String: Any]) -> String? { 480 | guard let theJSONData = try? JSONSerialization.data(withJSONObject: dict, options: []) else { 481 | return nil 482 | } 483 | return String(data: theJSONData, encoding: .utf8) 484 | } 485 | func convertDictToCReturn(dict: [String: Any]) -> UnsafePointer { 486 | let return_json = convertDictToJsonString(dict: dict); 487 | return (return_json! as NSString).utf8String!; 488 | } 489 | func convertOptionsToDictionary(options: UnsafePointer) -> [String: Any]? { 490 | return convertToDictionary(text: String(cString: options)); 491 | } 492 | //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// --------------------------------------------------------------------------------