1 /* Gui_messages.cpp
2  *
3  * Copyright (C) 1992-2018,2020,2021 Paul Boersma,
4  *               2008 Stefan de Konink, 2010 Franz Brausse, 2013 Tom Naughton
5  *
6  * This code is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2 of the License, or (at
9  * your option) any later version.
10  *
11  * This code is distributed in the hope that it will be useful, but
12  * WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
14  * See the GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this work. If not, see <http://www.gnu.org/licenses/>.
18  */
19 
20 #include <time.h>
21 
22 #include <assert.h>
23 #ifdef _WIN32
24 	#include <windows.h>
25 #endif
26 
27 #include "melder.h"
28 #include "Graphics.h"
29 #include "Gui.h"
30 
31 /********** Exported variable. **********/
32 
33 static GuiWindow Melder_topShell;
34 
35 /********** PROGRESS **********/
36 
37 static bool theProgressCancelled = false;
38 
39 static bool waitWhileProgress (double progress, conststring32 message, GuiDialog dia,
40 	GuiProgressBar scale, GuiLabel label1, GuiLabel label2, GuiButton cancelButton)
41 {
42 	#if gtk
43 		(void) cancelButton;   // the Interrupt button has its own callback
44 		// Wait for all pending events to be processed. If anybody knows how to inspect GTK's
45 		// event queue for specific events, dump the code here, please.
46 		// Until then, the button click attaches a g_object data key named "pressed" to the cancelButton
47 		// which this function reads out in order to tell whether interruption has occurred
48 		while (gtk_events_pending ()) {
49 			trace (U"event pending");
50 			gtk_main_iteration ();
51 		}
52 	#elif motif
53 		XEvent event;
54 		while (PeekMessage (& event, 0, 0, 0, PM_REMOVE)) {
55 			if (event. message == WM_KEYDOWN) {
56 				/*
57 				 * Ignore all key-down messages, except Escape.
58 				 */
59 				if (LOWORD (event. wParam) == VK_ESCAPE) {
60 					XtUnmanageChild (dia -> d_widget);
61 					return false;   // don't continue
62 				}
63 			} else if (event. message == WM_LBUTTONDOWN) {
64 				/*
65 				 * Ignore all mouse-down messages, except click in Interrupt button.
66 				 */
67 				GuiObject me = (GuiObject) GetWindowLongPtr (event. hwnd, GWLP_USERDATA);
68 				if (me == cancelButton -> d_widget) {
69 					XtUnmanageChild (dia -> d_widget);
70 					return false;   // don't continue
71 				}
72 			} else if (event. message != WM_SYSKEYDOWN) {
73 				/*
74 				 * Process paint messages etc.
75 				 */
76 				DispatchMessage (& event);
77 			}
78 		}
79 	#elif cocoa
80 		(void) cancelButton;   // the Interrupt button has its own callback
81 		while (NSEvent *nsEvent = [NSApp
82 			nextEventMatchingMask: NSAnyEventMask
83 			untilDate: [NSDate distantPast]
84 			inMode: NSDefaultRunLoopMode
85 			dequeue: YES
86 			])
87 		{
88 			NSUInteger nsEventType = [nsEvent type];
89 			if (nsEventType == NSKeyDown)
90 				NSBeep ();
91 			[[nsEvent window]  sendEvent: nsEvent];
92 		}
93 	#endif
94 	if (progress >= 1.0) {
95 		GuiThing_hide (dia);
96 	} else {
97 		if (progress <= 0.0)
98 			progress = 0.0;
99 		GuiThing_show (dia);   // TODO: prevent raising to the front
100 		const char32 *newline = str32chr (message, U'\n');
101 		if (newline) {
102 			static MelderString buffer;
103 			MelderString_copy (& buffer, message);
104 			buffer.string [newline - message] = U'\0';
105 			GuiLabel_setText (label1, buffer.string);
106 			buffer.string [newline - message] = U'\n';
107 			GuiLabel_setText (label2, buffer.string + (newline - message) + 1);
108 		} else {
109 			GuiLabel_setText (label1, message);
110 			GuiLabel_setText (label2, U"");
111 		}
112 		#if gtk
113 			trace (U"update the progress bar");
114 			GuiProgressBar_setValue (scale, progress);
115 			while (gtk_events_pending ()) {
116 				trace (U"event pending");
117 				gtk_main_iteration ();
118 			}
119 			trace (U"check whether the cancel button has the \"pressed\" key set");
120 			if (g_object_steal_data (G_OBJECT (cancelButton -> d_widget), "pressed")) {
121 				trace (U"the cancel button has been pressed");
122 				return false;   // don't continue
123 			}
124 		#elif motif
125 			GuiProgressBar_setValue (scale, progress);
126 			GdiFlush ();
127 		#elif cocoa
128 			GuiProgressBar_setValue (scale, progress);
129 			//[scale -> d_cocoaProgressBar   displayIfNeeded];
130 			if (theProgressCancelled) {
131 				theProgressCancelled = false;
132 				return false;
133 			}
134 		#endif
135 	}
136 	trace (U"continue");
137 	return true;
138 }
139 
140 static GuiButton theProgressCancelButton = nullptr;
141 
142 #if gtk || cocoa
143 	static void progress_dia_close (Thing /* boss */) {
144 		theProgressCancelled = true;
145 		#if gtk
146 			g_object_set_data (G_OBJECT (theProgressCancelButton -> d_widget), "pressed", (gpointer) 1);
147 		#endif
148 	}
149 	static void progress_cancel_btn_press (Thing /* boss */, GuiButtonEvent /* event */) {
150 		theProgressCancelled = true;
ExecOpenIndices(ResultRelInfo * resultRelInfo,bool speculative)151 		#if gtk
152 			g_object_set_data (G_OBJECT (theProgressCancelButton -> d_widget), "pressed", (gpointer) 1);
153 		#endif
154 	}
155 #endif
156 
157 static void _Melder_dia_init (GuiDialog *dia, GuiProgressBar *scale, GuiLabel *label1, GuiLabel *label2, GuiButton *cancelButton, bool hasMonitor) {
158 	trace (U"creating the dialog");
159 	*dia = GuiDialog_create (Melder_topShell, 200, 100, 400, hasMonitor ? 430 : 200, U"Work in progress",
160 		#if gtk || cocoa
161 			progress_dia_close, nullptr,
162 		#else
163 			nullptr, nullptr,
164 		#endif
165 		0);
166 
167 	trace (U"creating the labels");
168 	*label1 = GuiLabel_createShown (*dia, 3, 403, 0, Gui_LABEL_HEIGHT, U"label1", 0);
169 	*label2 = GuiLabel_createShown (*dia, 3, 403, 30, 30 + Gui_LABEL_HEIGHT, U"label2", 0);
170 
171 	trace (U"creating the scale");
172 	*scale = GuiProgressBar_createShown (*dia, 3, -3, 70, 110, 0);
173 
174 	trace (U"creating the cancel button");
175 	*cancelButton = GuiButton_createShown (*dia, 0, 400, 170, 170 + Gui_PUSHBUTTON_HEIGHT,
176 		U"Interrupt",
177 		#if gtk
178 			progress_cancel_btn_press, nullptr,
179 		#elif cocoa
180 			progress_cancel_btn_press, nullptr,
181 		#else
182 			nullptr, nullptr,
183 		#endif
184 		0);
185 	trace (U"end");
186 }
187 
188 static void gui_progress (double progress, conststring32 message) {
189 	static clock_t lastTime;
190 	static GuiDialog dia = nullptr;
191 	static GuiProgressBar scale = nullptr;
192 	static GuiLabel label1 = nullptr, label2 = nullptr;
193 	clock_t now = clock ();
194 	if (progress <= 0.0 || progress >= 1.0 ||
195 		now - lastTime > CLOCKS_PER_SEC / 4)   // this time step must be much longer than the null-event waiting time
196 	{
197 		if (! dia)
198 			_Melder_dia_init (& dia, & scale, & label1, & label2, & theProgressCancelButton, false);
199 		if (! waitWhileProgress (progress, message, dia, scale, label1, label2, theProgressCancelButton))
200 			Melder_throw (U"Interrupted!");
201 		lastTime = now;
202 	}
203 }
204 
205 static autoGraphics graphics;
206 
207 static void gui_drawingarea_cb_expose (Thing /* boss */, GuiDrawingArea_ExposeEvent /* event */) {
208 	if (! graphics)
209 		return;
210 	Graphics_play (graphics.get(), graphics.get());
211 }
212 
213 static void * gui_monitor (double progress, conststring32 message) {
214 	static clock_t lastTime;
215 	static GuiDialog dia = nullptr;
216 	static GuiProgressBar scale = nullptr;
217 	static GuiDrawingArea drawingArea = nullptr;
218 	static GuiButton cancelButton = nullptr;
219 	static GuiLabel label1 = nullptr, label2 = nullptr;
220 	clock_t now = clock ();
221 	if (progress <= 0.0 || progress >= 1.0 ||
222 		now - lastTime > CLOCKS_PER_SEC / 4)   // this time step must be much longer than the null-event waiting time
223 	{
224 		if (! dia) {
225 			_Melder_dia_init (& dia, & scale, & label1, & label2, & cancelButton, true);
ExecCloseIndices(ResultRelInfo * resultRelInfo)226 			drawingArea = GuiDrawingArea_createShown (dia, 0, 400, 230, 430,
227 					gui_drawingarea_cb_expose, nullptr, nullptr, nullptr, nullptr, 0);
228 			GuiThing_show (dia);
229 			graphics = Graphics_create_xmdrawingarea (drawingArea);
230 		}
231 		if (progress <= 0.0 && graphics) {
232 			Graphics_clearRecording (graphics.get());
233 			Graphics_startRecording (graphics.get());
234 			Graphics_clearWs (graphics.get());
235 		}
236 		if (! waitWhileProgress (progress, message, dia, scale, label1, label2, cancelButton))
237 			Melder_throw (U"Interrupted!");
238 		lastTime = now;
239 		if (progress == 0.0)
240 			return graphics.get();
241 	}
242 	return nullptr;
243 }
244 
245 #if cocoa
246 	static void mac_message (NSAlertStyle macAlertType, conststring32 message32) {
247 		static char16 message16 [4000];
248 		const integer messageLength = str32len (message32);
249 		uinteger j = 0;
250 		for (int i = 0; i < messageLength && j <= 4000 - 3; i ++) {
251 			char32 kar = message32 [i];
252 			if (kar <= 0x00'FFFF) {
253 				message16 [j ++] = (char16) kar;
254 			} else if (kar <= 0x10'FFFF) {
255 				kar -= 0x01'0000;
256 				message16 [j ++] = (char16) (0x00'D800 | (kar >> 10));
257 				message16 [j ++] = (char16) (0x00'DC00 | (kar & 0x00'03FF));
258 			}
259 		}
260 		message16 [j] = u'\0';   // append null byte because we are going to search this string
261 
262 		/*
263 			Split up the message between header (will appear in bold) and rest.
264 			The split is done at the first line break, except if the first line ends in a colon,
265 			in which case the split is done at the second line break.
266 		*/
267 		const char16 *lineBreak = & message16 [0];
268 		for (; *lineBreak != u'\0'; lineBreak ++)
269 			if (*lineBreak == u'\n')
270 				break;
271 		if (*lineBreak == u'\n' && lineBreak - message16 > 0 && lineBreak [-1] == u':')
272 			for (lineBreak ++; *lineBreak != u'\0'; lineBreak ++)
ExecInsertIndexTuples(TupleTableSlot * slot,EState * estate,bool noDupErr,bool * specConflict,List * arbiterIndexes)273 				if (*lineBreak == u'\n')
274 					break;
275 		uinteger lengthOfFirstSentence = (uinteger) (lineBreak - message16);
276 		/*
277 			Create an alert dialog with an icon that is appropriate for the level.
278 		*/
279 		NSAlert *alert = [[NSAlert alloc] init];
280 		[alert setAlertStyle: macAlertType];
281 		if (macAlertType == NSCriticalAlertStyle)
282 			[alert addButtonWithTitle: @"Crash"];
283 		/*
284 			Add the header in bold.
285 		*/
286 		NSString *header = [[NSString alloc] initWithCharacters: (const unichar *) & message16 [0]   length: lengthOfFirstSentence];   // note: init can change the object pointer!
287 		if (header) {   // make this very safe, because we can be at error time or at fatal time
288 			[alert setMessageText: header];
289 			[header release];
290 		}
291 		/*
292 			Add the rest of the message in small type.
293 		*/
294 		if (lengthOfFirstSentence < j) {
295 			NSString *rest = [[NSString alloc] initWithCharacters: (const unichar *) & lineBreak [1]   length: j - 1 - lengthOfFirstSentence];
296 			if (rest) {   // make this very safe, because we can be at error time or at crash time
297 				[alert setInformativeText: rest];
298 				[rest release];
299 			}
300 		}
301 		/*
302 			Display the alert dialog and synchronously wait for the user to click OK.
303 			But: it is not impossible that the program crashes during `runModal`,
304 			especially if `runModal` is called at expose time.
305 			Write the message to stdout just in case.
306 		*/
307 		Melder_casual (message32);
308 		[alert runModal];
309 		[alert release];
310 	}
311 #endif
312 
313 #define theMessageFund_SIZE  100'000
314 static char * theMessageFund = nullptr;
315 
316 static void gui_fatal (conststring32 message) {
317 	free (theMessageFund);
318 	#if gtk
319 		GuiObject dialog = gtk_message_dialog_new (GTK_WINDOW (Melder_topShell -> d_gtkWindow), GTK_DIALOG_DESTROY_WITH_PARENT,
320 			GTK_MESSAGE_ERROR, GTK_BUTTONS_NONE, "%s", Melder_peek32to8 (message));
321 		gtk_dialog_add_buttons (GTK_DIALOG (dialog), "Crash", GTK_RESPONSE_OK, nullptr);
322 		gtk_dialog_run (GTK_DIALOG (dialog));
323 		gtk_widget_destroy (GTK_WIDGET (dialog));
324 	#elif motif
325 		MessageBox (nullptr, Melder_peek32toW (message), L"Crashing bug", MB_OK | MB_TOPMOST | MB_ICONSTOP);
326 	#elif cocoa
327 		mac_message (NSCriticalAlertStyle, message);
328 		SysError (11);
329 	#endif
330 }
331 
332 static void gui_error (conststring32 message) {
333 	const bool memoryIsLow = str32str (message, U"Out of memory");
334 	if (memoryIsLow)
335 		free (theMessageFund);
336 	#if gtk
337 		trace (U"create dialog");
338 		GuiObject dialog = gtk_message_dialog_new (GTK_WINDOW (Melder_topShell -> d_gtkWindow), GTK_DIALOG_DESTROY_WITH_PARENT,
339 			GTK_MESSAGE_WARNING, GTK_BUTTONS_OK, "%s", Melder_peek32to8 (message));
340 		trace (U"run dialog");
341 		gtk_dialog_run (GTK_DIALOG (dialog));
342 		trace (U"destroy dialog");
343 		gtk_widget_destroy (GTK_WIDGET (dialog));
344 	#elif motif
345 		MessageBox (nullptr, Melder_peek32toW (message), L"Message", MB_OK | MB_TOPMOST | MB_ICONWARNING);   // or (HWND) XtWindow ((GuiObject) Melder_topShell)
346 	#elif cocoa
347 		mac_message (NSWarningAlertStyle, message);
348 	#endif
349 	if (memoryIsLow) {
350 		theMessageFund = (char *) malloc (theMessageFund_SIZE);
351 		if (! theMessageFund) {
352 			#if gtk
353 				GuiObject dialog = gtk_message_dialog_new (GTK_WINDOW (Melder_topShell -> d_gtkWindow), GTK_DIALOG_DESTROY_WITH_PARENT,
354 					GTK_MESSAGE_ERROR, GTK_BUTTONS_OK, "Praat is very low on memory.\nSave your work and quit Praat.\nIf you don't do that, Praat may crash.");
355 				gtk_dialog_run (GTK_DIALOG (dialog));
356 				gtk_widget_destroy (GTK_WIDGET (dialog));
357 			#elif motif
358 				MessageBox (nullptr, L"Praat is very low on memory.\nSave your work and quit Praat.\nIf you don't do that, Praat may crash.", L"Message", MB_OK);
359 			#elif cocoa
360 				mac_message (NSCriticalAlertStyle, U"Praat is very low on memory.\nSave your work and quit Praat.\nIf you don't do that, Praat may crash.");
361 			#endif
362 		}
363 	}
364 }
365 
366 static void gui_warning (conststring32 message) {
367 	#if gtk
368 		GuiObject dialog = gtk_message_dialog_new (GTK_WINDOW (Melder_topShell -> d_gtkWindow), GTK_DIALOG_DESTROY_WITH_PARENT,
369 			GTK_MESSAGE_INFO, GTK_BUTTONS_OK, "%s", Melder_peek32to8 (message));
370 		gtk_dialog_run (GTK_DIALOG (dialog));
371 		gtk_widget_destroy (GTK_WIDGET (dialog));
372 	#elif motif
373 		MessageBox (nullptr, Melder_peek32toW (message), L"Warning", MB_OK | MB_TOPMOST | MB_ICONINFORMATION);
374 	#elif cocoa
375 		mac_message (NSInformationalAlertStyle, message);
376 	#endif
377 }
378 
379 void Gui_injectMessageProcs (GuiWindow parent) {
380 	theMessageFund = (char *) malloc (theMessageFund_SIZE);
381 	assert (theMessageFund);
382 	Melder_topShell = parent;
383 	Melder_setCrashProc (gui_fatal);
384 	Melder_setErrorProc (gui_error);
385 	Melder_setWarningProc (gui_warning);
386 	Melder_setProgressProc (gui_progress);
387 	Melder_setMonitorProc (gui_monitor);
388 }
389 
390 /* End of file Gui_messages.cpp */
391