├── .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 | ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
--------------------------------------------------------------------------------