1 // Copyright (c) 2017 Matthew Brennan Jones <matthew.brennan.jones@gmail.com>
2 // Boost Software License - Version 1.0
3 // A simple message box for the D programming language
4 // https://github.com/workhorsy/d-message-box
5 
6 
7 /++
8 A simple message box for the D programming language
9 
10 It should work without requiring any 3rd party GUI toolkits. But will work with what
11 it can find on your OS at runtime.
12 
13 It tries to use the following:
14 
15 * DlangUI (win32 on Windows or SDL2 on Linux)
16 
17 * SDL_ShowSimpleMessageBox (Derelict SDL2)
18 
19 * MessageBoxW (Windows)
20 
21 * Zenity (Gtk/Gnome)
22 
23 * Kdialog (KDE)
24 
25 * gxmessage (X11)
26 
27 Home page:
28 $(LINK https://github.com/workhorsy/d-message-box)
29 
30 Version: 0.3.0
31 
32 License:
33 Boost Software License - Version 1.0
34 
35 Examples:
36 ----
37 import std.stdio : stdout, stderr;
38 import message_box : MessageBox, IconType;
39 
40 int main(string[] args) {
41 	// Create the message box
42 	auto dialog = new MessageBox("Party Time", "The roof is on fire!", IconType.Warning);
43 
44 	// Set the error handler
45 	dialog.onError((Throwable err) {
46 		stderr.writefln("Failed to show message box: %s", err);
47 	});
48 
49 	// Show the message box
50 	dialog.show();
51 
52 	return 0;
53 }
54 ----
55 +/
56 
57 module message_box;
58 
59 bool is_sdl2_loadable = false;
60 bool use_log = false;
61 
62 static this() {
63 	import std.stdio : stdout;
64 
65 	// Figure out if the SDL2 libraries can be loaded
66 	version (Have_derelict_sdl2) {
67 		import derelict.sdl2.sdl : DerelictSDL2, SharedLibVersion, SharedLibLoadException;
68 		try {
69 			DerelictSDL2.load(SharedLibVersion(2, 0, 2));
70 			is_sdl2_loadable = true;
71 			stdout.writefln("SDL was found ...");
72 		} catch (SharedLibLoadException) {
73 			stdout.writefln("SDL was NOT found ...");
74 		}
75 	}
76 }
77 
78 
79 /++
80 If true will print output of external program to console.
81 Params:
82  is_logging = If true will print to output
83 +/
84 public void setUseLog(bool is_logging) {
85 	use_log = is_logging;
86 }
87 
88 /++
89 Returns if external program logging is on or off.
90 +/
91 public bool getUseLog() {
92 	return use_log;
93 }
94 
95 /++
96 The type of icon to show in the message box. Some message boxes will not show
97 the icon.
98 
99 ----
100 enum IconType {
101 	None,
102 	Information,
103 	Error,
104 	Warning,
105 }
106 ----
107 +/
108 
109 enum IconType {
110 	None,
111 	Information,
112 	Error,
113 	Warning,
114 }
115 
116 abstract class MessageBoxBase {
117 	this(string title, string message, IconType icon_type) {
118 		_title = title;
119 		_message = message;
120 		_icon_type = icon_type;
121 	}
122 
123 	void onError(void delegate(Throwable err) cb) {
124 		_on_error_cb = cb;
125 	}
126 
127 	void fireOnError(Throwable err) {
128 		// Remove the callback before use to prevent infinite recursion
129 		auto old_cb = _on_error_cb;
130 		_on_error_cb = null;
131 
132 		// Fire the callback
133 		if (old_cb) old_cb(err);
134 
135 		// Restore the callback
136 		_on_error_cb = old_cb;
137 	}
138 
139 	void show();
140 
141 	string _title;
142 	string _message;
143 	IconType _icon_type;
144 	void delegate(Throwable err) _on_error_cb;
145 }
146 
147 /++
148 The MessageBox class
149 +/
150 class MessageBox {
151 	import compressed_file : CompressedFile;
152 	import message_box_dlangui : MessageBoxDlangUI;
153 	import message_box_sdl : MessageBoxSDL;
154 /*
155 	import message_box_win32 : MessageBoxWin32;
156 */
157 	import message_box_zenity : MessageBoxZenity;
158 	import message_box_kdialog : MessageBoxKdialog;
159 	import message_box_gxmessage : MessageBoxGxmessage;
160 
161 	/++
162 	Sets up the message box with the desired title, message, and icon. Does not
163 	show it until the show method is called.
164 	Params:
165 	 title = The string to show in the message box title
166 	 message = The string to show in the message box body
167 	 icon = The type of icon to show in the message box
168 	Throws:
169 	 If it fails to find any programs or libraries to make a message box with.
170 	+/
171 	this(string title, string message, IconType icon_type) {
172 		assertInitialized();
173 		if (MessageBoxDlangUI.isSupported()) {
174 			_dialog = new MessageBoxDlangUI(_temp_dir, title, message, icon_type);
175 		} else if (MessageBoxSDL.isSupported()) {
176 			_dialog = new MessageBoxSDL(title, message, icon_type);
177 /*
178 		} else if (MessageBoxWin32.isSupported()) {
179 			_dialog = new MessageBoxWin32(title, message, icon_type);
180 */
181 		} else if (MessageBoxZenity.isSupported()) {
182 			_dialog = new MessageBoxZenity(title, message, icon_type);
183 		} else if (MessageBoxKdialog.isSupported()) {
184 			_dialog = new MessageBoxKdialog(title, message, icon_type);
185 		} else if (MessageBoxGxmessage.isSupported()) {
186 			_dialog = new MessageBoxGxmessage(title, message, icon_type);
187 		} else {
188 			throw new Exception("Failed to find a way to make a message box.");
189 		}
190 	}
191 
192 	/++
193 	This method is called if there is an error when showing the message box.
194 	Params:
195 	 cb = The call back to fire when there is an error.
196 	+/
197 	void onError(void delegate(Throwable err) cb) {
198 		assertInitialized();
199 		_dialog._on_error_cb = cb;
200 	}
201 
202 	/++
203 	Shows the message box. Will block until it is closed.
204 	+/
205 	void show() {
206 		assertInitialized();
207 		_dialog.show();
208 	}
209 
210 	public static void init() {
211 		import data : compressed_files;
212 		import extract : extractFiles;
213 		import std.file : tempDir, exists, mkdir;
214 		import std.path : buildPath;
215 		import std.algorithm : map;
216 		import std.ascii : letters;
217 		import std.random : uniform;
218 		import std.range : iota, array;
219 		import std.conv : to;
220 
221 		// Generate a random unused temporary directory name
222 		do {
223 			string dir_name = iota(10).map!((_) => letters[uniform(0, $)]).array.to!string;
224 			_temp_dir = buildPath(tempDir(), dir_name);
225 		} while(exists(_temp_dir));
226 
227 		// Create the directory
228 		mkdir(_temp_dir);
229 
230 		// Extract the files into the directory
231 		extractFiles(_temp_dir, compressed_files, delegate(int percent) {
232 			//dialog.setPercent(percent);
233 		});
234 	}
235 
236 	public static void cleanup()  {
237 		import std.file : exists, rmdirRecurse, FileException;
238 		import std.stdio : stderr;
239 
240 		// Remove the temporary directory
241 		if (_temp_dir && exists(_temp_dir)) {
242 			try {
243 				rmdirRecurse(_temp_dir);
244 			} catch (FileException err) {
245 				stderr.writefln(`Failed to remove the directory "%s"`, _temp_dir);
246 			}
247 		}
248 	}
249 
250 	private static void assertInitialized() {
251 		// Just return if initialized
252 		if (_temp_dir) return;
253 
254 		throw new Exception("MessageBox.init should be called before use.");
255 	}
256 
257 	static string _temp_dir = null;
258 	MessageBoxBase _dialog;
259 }