1 // ----------------------------------------------------------------------------
2 //      notify.cxx
3 //
4 // Copyright (C) 2009-2010
5 //              Stelios Bounanos, M0GLD
6 //
7 // Generic notifier
8 //
9 //
10 // This file is part of fldigi.
11 //
12 // fldigi is free software; you can redistribute it and/or modify
13 // it under the terms of the GNU General Public License as published by
14 // the Free Software Foundation; either version 3 of the License, or
15 // (at your option) any later version.
16 //
17 // fldigi is distributed in the hope that it will be useful,
18 // but WITHOUT ANY WARRANTY; without even the implied warranty of
19 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20 // GNU General Public License for more details.
21 //
22 // You should have received a copy of the GNU General Public License
23 // along with this program.  If not, see <http://www.gnu.org/licenses/>.
24 // ----------------------------------------------------------------------------
25 
26 #include <config.h>
27 
28 #include <string>
29 #include <vector>
30 #include <list>
31 #include <algorithm>
32 #include <cstring>
33 #include <cctype>
34 #include <cstdlib>
35 
36 #include <sstream>
37 
38 #include "timeops.h"
39 
40 #if HAVE_STD_HASH
41 #	define MAP_TYPE std::unordered_map
42 #	define HASH_TYPE std::hash
43 #	include <unordered_map>
44 #else
45 #	if (__GNUC__ > 4) || (__GNUC__ == 4 && __GNUC_MINOR__ >= 1)
46 #		define MAP_TYPE std::tr1::unordered_map
47 #	include <tr1/unordered_map>
48 #	else
49 // use the non-standard gnu hash_map on gcc <= 4.0.x,
50 // which has a broken tr1::unordered_map::operator=
51 #	define MAP_TYPE __gnu_cxx::hash_map
52 #	include <ext/hash_map>
53 	namespace __gnu_cxx {
54 		// define the missing hash specialisation for std::string
55 		// using the 'const char*' hash function
56 		template<> struct hash<std::string> {
operator ()__gnu_cxx::hash57 			size_t operator()(const std::string& s) const { return __stl_hash_string(s.c_str()); }
58 		};
59 	}
60 #	endif
61 #endif
62 
63 #include <unistd.h>
64 #include <time.h>
65 #include <sys/time.h>
66 
67 #ifdef __MINGW32__
68 #  include <windows.h>
69 #endif
70 
71 #include <FL/Fl_Menu_Item.H>
72 #include <FL/Fl_Preferences.H>
73 #include <FL/Fl_Pixmap.H>
74 #include <FL/Fl.H>
75 #include <FL/Enumerations.H>
76 #include <FL/Fl_Button.H>
77 
78 #include "flinput2.h"
79 #include "flmisc.h"
80 #include "macros.h"
81 #include "debug.h"
82 #include "dxcc.h"
83 #include "spot.h"
84 #include "pskrep.h"
85 #include "logsupport.h"
86 #include "re.h"
87 #include "fileselect.h"
88 #include "icons.h"
89 #include "configuration.h"
90 #include "macroedit.h"
91 #include "main.h"
92 #include "fl_digi.h"
93 #include "waterfall.h"
94 #include "globals.h"
95 #include "trx.h"
96 #include "rsid.h"
97 #include "gettext.h"
98 #include "notifydialog.h"
99 #include "notify.h"
100 #include "qrunner.h"
101 
102 using namespace std;
103 
104 struct notify_action_t
105 {
106 	string alert;
107 	string rx_marker;
108 	string macro;
109 	string program;
110 	time_t alert_timeout;
111 	time_t trigger_limit;
112 };
113 
114 enum notify_filter_match_t { NOTIFY_FILTER_CALLSIGN, NOTIFY_FILTER_DXCC };
115 typedef MAP_TYPE<string, bool> notify_filter_dxcc_t;
116 struct notify_filter_t
117 {
118 	notify_filter_match_t match;
119 	string callsign;
120 	notify_filter_dxcc_t dxcc;
121 	string dxcc_last;
122 	bool nwb, lotw, eqsl;
123 };
124 
125 struct notify_dup_t
126 {
127 	time_t when;
128 	band_t band;
129 	trx_mode mode;
130 	long long freq;
131 };
132 
133 typedef MAP_TYPE<string, notify_dup_t> notify_seen_t;
134 enum notify_event_t { NOTIFY_EVENT_MYCALL, NOTIFY_EVENT_STATION, NOTIFY_EVENT_CUSTOM, NOTIFY_EVENT_RSID };
135 struct notify_t
136 {
137 	notify_event_t event;
138 	time_t last_trigger;
139 	string re;
140 	bool enabled;
141 	int afreq;
142 	long long rfreq;
143 	trx_mode mode;
144 
145 	const char* match_string;
146 	const regmatch_t* submatch_offsets;
147 	size_t submatch_length;
148 
149 	notify_dup_t dup;
150 	bool dup_ignore;
151 	size_t dup_ref;
152 	notify_seen_t last_seen;
153 
154 	notify_action_t action;
155 	notify_filter_t filter;
156 };
157 typedef list<notify_t> notify_list_t;
158 
159 
160 static void notify_init_window(void);
161 static void notify_save(void);
162 static void notify_load(void);
163 static void notify_register(notify_t& n);
164 static void notify_unregister(const notify_t& n);
165 static void notify_set_qsodb_cache(void);
166 
167 static void notify_event_cb(Fl_Widget* w, void* arg);
168 static void notify_select_cb(Fl_Widget* w, void* arg);
169 static void notify_dxcc_browse_cb(Fl_Widget* w, void* arg);
170 static void notify_add_cb(Fl_Widget* w, void* arg);
171 static void notify_remove_cb(Fl_Widget* w, void* arg);
172 static void notify_update_cb(Fl_Widget* w, void* arg);
173 static void notify_dialog_default_cb(Fl_Widget* w, void* arg);
174 static void notify_rx_default_cb(Fl_Widget* w, void* arg);
175 static void notify_macro_edit_cb(Fl_Widget* w, void* arg);
176 static void notify_program_select_cb(Fl_Widget* w, void* arg);
177 static void notify_dxcc_check_cb(Fl_Widget* w, void* arg);
178 static void notify_test_cb(Fl_Widget* w, void* arg);
179 static void notify_filter_dxcc_select_cb(Fl_Widget* w, void* arg);
180 static void notify_filter_dxcc_search(Fl_Widget* w, void* arg);
181 static void notify_dup_ignore_cb(Fl_Widget* w, void* arg);
182 static void notify_re_cb(Fl_Widget* w, void* arg);
183 
184 static void notify_recv(trx_mode mode, int afreq, const char* str, const regmatch_t* sub, size_t len, void* data);
185 static void notify_table_append(const notify_t& n);
186 
187 ////////////////////////////////////////////////////////////////////////////////
188 
189 struct event_regex_t {
190 	const char* regex;
191 	size_t index;
192 };
193 
194 static Fl_Menu_Item notify_event_menu[] = {
195 	{ _("My callsign de CALL") },
196 	{ _("Station heard twice") },
197 	{ _("Custom text search") },
198 	{ _("RSID reception") },
199 	{ 0 }
200 };
201 
202 #define NOTIFY_SET_DUP_MENU (void*)1
203 
204 enum { NOTIFY_LIST_MENU_TOGGLE, NOTIFY_LIST_MENU_UPDATE, NOTIFY_LIST_MENU_REMOVE };
205 static Fl_Menu_Item notify_list_context_menu[] = {
206 	{ icons::make_icon_label(_("Toggle"), shutdown_icon), 0, notify_update_cb, (void*)NOTIFY_LIST_MENU_TOGGLE },
207 	{ icons::make_icon_label(_("Update"), refresh_icon), 0, notify_update_cb, (void*)NOTIFY_LIST_MENU_UPDATE },
208 	{ icons::make_icon_label(_("Remove"), minus_icon), 0, notify_remove_cb, (void*)NOTIFY_LIST_MENU_REMOVE },
209 	{ 0 }
210 };
211 
212 enum {
213 	NOTIFY_DXCC_SELECT_CONT, NOTIFY_DXCC_SELECT_ITU,
214 	NOTIFY_DXCC_SELECT_CQ, NOTIFY_DXCC_SELECT_ALL,
215 	NOTIFY_DXCC_DESELECT_CONT, NOTIFY_DXCC_DESELECT_ITU,
216 	NOTIFY_DXCC_DESELECT_CQ, NOTIFY_DXCC_DESELECT_ALL
217 };
218 static Fl_Menu_Item notify_dxcc_context_menu[] = {
219 	{ _("Select"), 0, 0, 0, FL_SUBMENU },
220 		{ _("Continent"), 0, notify_filter_dxcc_select_cb, (void*)NOTIFY_DXCC_SELECT_CONT },
221 		{ _("ITU zone"), 0, notify_filter_dxcc_select_cb, (void*)NOTIFY_DXCC_SELECT_ITU },
222 		{ _("CQ zone"), 0, notify_filter_dxcc_select_cb, (void*)NOTIFY_DXCC_SELECT_CQ, FL_MENU_DIVIDER },
223 		{ _("All"), 0, notify_filter_dxcc_select_cb, (void*)NOTIFY_DXCC_SELECT_ALL },
224 		{ 0 },
225 	{ _("Deselect"), 0, 0, 0, FL_SUBMENU },
226 		{ _("Continent"), 0, notify_filter_dxcc_select_cb, (void*)NOTIFY_DXCC_DESELECT_CONT },
227 		{ _("ITU zone"), 0, notify_filter_dxcc_select_cb, (void*)NOTIFY_DXCC_DESELECT_ITU },
228 		{ _("CQ zone"), 0, notify_filter_dxcc_select_cb, (void*)NOTIFY_DXCC_DESELECT_CQ, FL_MENU_DIVIDER },
229 		{ _("All"), 0, notify_filter_dxcc_select_cb, (void*)NOTIFY_DXCC_DESELECT_ALL },
230 		{ 0 },
231 	{ 0 }
232 };
233 
234 static event_regex_t event_regex[] = {
235 	{ "<MYCALL>.+de[[:space:]]+(<CALLSIGN_RE>)", 1 },
236 	{ PSKREP_RE, PSKREP_RE_INDEX },
237 	{ "", 0 },
238 	{ "", 1 }
239 };
240 
241 static const char* default_alert_text[] = {
242 	"$CALLSIGN is calling you\n    $TEXT\nTime: %X %Z (%z)\nMode: $MODEM @ $RF_KHZ KHz",
243 	"Heard $CALLSIGN ($COUNTRY)\n    $TEXT\nTime: %X %Z (%z)\nMode: $MODEM @ $RF_KHZ KHz",
244 	"",
245 	"RSID received\nMode: $MODEM @ $RF_KHZ KHz\nTime: %X %Z (%z)",
246 };
247 
248 static Fl_Menu_Item notify_dup_callsign_menu[] = {
249 	{ "" }, { "" }, { "" }, { "" }, { "" }, { "" }, { "" }, { "" }, { "" }, { "" }, { 0 }
250 };
251 
252 static Fl_Menu_Item notify_dup_refs_menu[] = {
253 	{ "Substring \\0" }, { "Substring \\1" }, { "Substring \\2" }, { "Substring \\3" },
254 	{ "Substring \\4" }, { "Substring \\5" }, { "Substring \\6" }, { "Substring \\7" },
255 	{ "Substring \\8" }, { "Substring \\9" }, { 0 }
256 };
257 
258 enum {
259 	NOTIFY_DXCC_COL_SEL, NOTIFY_DXCC_COL_CN, NOTIFY_DXCC_COL_CT,
260 	NOTIFY_DXCC_COL_ITU, NOTIFY_DXCC_COL_CQ, NOTIFY_DXCC_NUMCOL
261 };
262 
263 
advli(T & i,int n)264 template <typename T, typename U> static T& advli(T& i, int n) { advance(i, n); return i; }
advli(T i,U n)265 template <typename T, typename U> static T advli(T i, U n) { advance(i, n); return i; }
266 
267 
268 static notify_list_t notify_list;
269 static notify_t notify_tmp;
270 static const vector<dxcc*>* dxcc_list;
271 
272 Fl_Double_Window* notify_window;
273 Fl_Double_Window* dxcc_window;
274 
275 
276 ////////////////////////////////////////////////////////////////////////////////
277 // public interface
278 ////////////////////////////////////////////////////////////////////////////////
279 
notify_start(void)280 void notify_start(void)
281 {
282 	if (!notify_window)
283 		notify_init_window();
284 
285 	notify_load();
286 	if (!notify_list.empty()) {
287 		for (notify_list_t::iterator i = notify_list.begin(); i != notify_list.end(); ++i)
288 			if (i->enabled)
289 				notify_register(*i);
290 		tblNotifyList->value(0);
291 		tblNotifyList->do_callback();
292 		notify_set_qsodb_cache();
293 	}
294 }
295 
notify_stop(void)296 void notify_stop(void)
297 {
298 	for (notify_list_t::iterator i = notify_list.begin(); i != notify_list.end(); ++i)
299 		notify_unregister(*i);
300 	notify_list.clear();
301 	notify_set_qsodb_cache();
302 	tblNotifyList->clear();
303 }
304 
notify_show(void)305 void notify_show(void)
306 {
307 	if (!notify_window)
308 		notify_window = make_notify_window();
309 	notify_window->show();
310 }
311 
312 // display the dxcc window
notify_dxcc_show(bool readonly)313 void notify_dxcc_show(bool readonly)
314 {
315 	if (!dxcc_list)
316 		return;
317 	if (readonly) {
318 		btnNotifyDXCCSelect->hide();
319 		btnNotifyDXCCDeselect->hide();
320 		tblNotifyFilterDXCC->callback(Fl_Widget::default_callback);
321 		tblNotifyFilterDXCC->menu(0);
322 		btnNotifyDXCCDeselect->do_callback(); // deselect all
323 		if (dxcc_window->shown())
324 			dxcc_window->hide();
325 		if (dxcc_window->modal())
326 			dxcc_window->set_non_modal();
327 	}
328 	else {
329 		btnNotifyDXCCSelect->show();
330 		btnNotifyDXCCDeselect->show();
331 		tblNotifyFilterDXCC->callback(notify_dxcc_check_cb);
332 		tblNotifyFilterDXCC->menu(notify_dxcc_context_menu);
333 		dxcc_window->set_modal();
334 	}
335 	dxcc_window->show();
336 }
337 
338 // called by the myCall callback when the operator callsign is changed
notify_change_callsign(void)339 void notify_change_callsign(void)
340 {
341 	for (notify_list_t::iterator i = notify_list.begin(); i != notify_list.end(); ++i) {
342 		if (i->event == NOTIFY_EVENT_MYCALL) { // re-register
343 			notify_unregister(*i);
344 			notify_register(*i);
345 		}
346 	}
347 }
348 
349 // called by the RSID decoder
notify_rsid(trx_mode mode,int afreq)350 void notify_rsid(trx_mode mode, int afreq)
351 {
352 	const char* mode_name = mode_info[mode].name;
353 	regmatch_t sub[2] = { { 0, (regoff_t)strlen(mode_name) } };
354 	sub[1] = sub[0];
355 	for (notify_list_t::iterator i = notify_list.begin(); i != notify_list.end(); ++i)
356 		if (i->event == NOTIFY_EVENT_RSID)
357 			notify_recv(mode, afreq, mode_name, sub, 2, &*i);
358 }
359 
360 // called by the config dialog when the "notifications only"
361 // rsid option is selected
notify_create_rsid_event(bool val)362 void notify_create_rsid_event(bool val)
363 {
364 	if (!val)
365 		return;
366 	for (notify_list_t::iterator i = notify_list.begin(); i != notify_list.end(); ++i)
367 		if (i->event == NOTIFY_EVENT_RSID)
368 			return;
369 
370 	notify_t rsid_event = {
371 		NOTIFY_EVENT_RSID, 0, "", true, 0, 0LL, NUM_MODES, NULL, NULL, 0,
372 		{ 0, NUM_BANDS, NUM_MODES, 0LL }, false, 0
373 	};
374 	notify_action_t rsid_action = { default_alert_text[NOTIFY_EVENT_RSID], "", "", "", 30, 1 };
375 	rsid_event.action = rsid_action;
376 
377 	notify_list.push_back(rsid_event);
378 	notify_table_append(notify_list.back());
379 	tblNotifyList->do_callback();
380 	notify_save();
381 }
382 
383 
384 ////////////////////////////////////////////////////////////////////////////////
385 // misc utility functions
386 ////////////////////////////////////////////////////////////////////////////////
387 
388 static void notify_set_event_dup(const notify_t& n);
389 static void notify_set_event_dup_menu(const char* re);
390 static bool notify_dxcc_row_checked(int i);
391 
392 // return actual regular expression for event n
notify_get_re(const notify_t & n)393 static string notify_get_re(const notify_t& n)
394 {
395 	string::size_type pos;
396 	string s = n.re;
397 
398 	struct { const char* str; const char* rep; } subst[] = {
399 		{ "<MYCALL>", progdefaults.myCall.c_str() },
400 		{ "<CALLSIGN_RE>", CALLSIGN_RE }
401 	};
402 	for (size_t i = 0; i < sizeof(subst)/sizeof(*subst); i++)
403 		if ((pos = s.find(subst[i].str)) != string::npos)
404 			s.replace(pos, strlen(subst[i].str), subst[i].rep);
405 
406 	return s;
407 }
408 
409 // set the widget values using event n
notify_event_to_gui(const notify_t & n)410 static void notify_event_to_gui(const notify_t& n)
411 {
412 	// event
413 	mnuNotifyEvent->value(n.event);
414 	notify_event_cb(mnuNotifyEvent, 0);
415 	if (!n.re.empty() && inpNotifyRE->visible())
416 		inpNotifyRE->value(n.re.c_str());
417 	btnNotifyEnabled->value(n.enabled);
418 
419 	// action
420 	inpNotifyActionDialog->value(n.action.alert.c_str());
421 	inpNotifyActionRXMarker->value(n.action.rx_marker.c_str());
422 	inpNotifyActionMacro->value(n.action.macro.c_str());
423 	inpNotifyActionProgram->value(n.action.program.c_str());
424 	cntNotifyActionLimit->value(n.action.trigger_limit);
425 	cntNotifyActionDialogTimeout->value(n.action.alert_timeout);
426 
427 	// dup
428 	chkNotifyDupIgnore->value(n.dup_ignore);
429 	chkNotifyDupIgnore->do_callback();
430 	notify_set_event_dup(n);
431 	cntNotifyDupTime->value(n.dup.when);
432 	chkNotifyDupBand->value(n.dup.band);
433 	chkNotifyDupMode->value(n.dup.mode);
434 
435 	// filter
436 	btnNotifyDXCCDeselect->do_callback(); // deselect all
437 	if (!grpNotifyFilter->active())
438 		return;
439 	chkNotifyFilterCall->value(0);
440 	inpNotifyFilterCall->hide();
441 	chkNotifyFilterDXCC->value(0);
442 	btnNotifyFilterDXCC->hide();
443 	inpNotifyFilterCall->value(0);
444 
445 	if (n.filter.match == NOTIFY_FILTER_CALLSIGN) {
446 		chkNotifyFilterCall->value(1);
447 		inpNotifyFilterCall->show();
448 		inpNotifyFilterCall->value(n.filter.callsign.c_str());
449 	}
450 	else if (n.filter.match == NOTIFY_FILTER_DXCC) {
451 		chkNotifyFilterDXCC->value(1);
452 		btnNotifyFilterDXCC->show();
453 	}
454 	chkNotifyFilterNWB->value(n.filter.nwb);
455 	chkNotifyFilterLOTW->value(n.filter.lotw);
456 	chkNotifyFilterEQSL->value(n.filter.eqsl);
457 }
458 
459 // copy widget values to event n
notify_gui_to_event(notify_t & n)460 static void notify_gui_to_event(notify_t& n)
461 {
462 	// event
463 	n.event = static_cast<notify_event_t>(mnuNotifyEvent->value());
464 	n.last_trigger = 0;
465 	if (n.event == NOTIFY_EVENT_CUSTOM)
466 		n.re = inpNotifyRE->value();
467 	else
468 		n.re = event_regex[n.event].regex;
469 	n.enabled = btnNotifyEnabled->value();
470 	n.afreq = 0;
471 	n.rfreq = 0;
472 	n.match_string = 0;
473 	n.submatch_offsets = 0;
474 	n.submatch_length = 0;
475 
476 	// action
477 	n.action.alert = inpNotifyActionDialog->value();
478 	n.action.rx_marker = inpNotifyActionRXMarker->value();
479 	n.action.macro = inpNotifyActionMacro->value();
480 	n.action.program = inpNotifyActionProgram->value();
481 	n.action.trigger_limit = static_cast<time_t>(cntNotifyActionLimit->value());
482 	n.action.alert_timeout = static_cast<time_t>(cntNotifyActionDialogTimeout->value());
483 
484 	// filter
485 	if (chkNotifyFilterCall->value()) {
486 		n.filter.callsign = inpNotifyFilterCall->value();
487 		n.filter.match = NOTIFY_FILTER_CALLSIGN;
488 	}
489 	else if (chkNotifyFilterDXCC->value()) {
490 		n.filter.match = NOTIFY_FILTER_DXCC;
491 
492 		n.filter.dxcc.clear();
493 		for (int i = 0; i < tblNotifyFilterDXCC->rows(); i++) {
494 			if (notify_dxcc_row_checked(i))
495 				n.filter.dxcc[tblNotifyFilterDXCC->valueAt(i, NOTIFY_DXCC_COL_CN)] = true;
496 		}
497 	}
498 	n.filter.nwb = chkNotifyFilterNWB->value();
499 	n.filter.lotw = chkNotifyFilterLOTW->value();
500 	n.filter.eqsl = chkNotifyFilterEQSL->value();
501 
502 	// dup
503 	n.dup_ignore = chkNotifyDupIgnore->value();
504 	n.dup_ref = mnuNotifyDupWhich->value();
505 	n.dup.when = static_cast<time_t>(cntNotifyDupTime->value());
506 	n.dup.band = chkNotifyDupBand->value() ? NUM_BANDS : static_cast<band_t>(0);
507 	n.dup.mode = chkNotifyDupMode->value() ? NUM_MODES : static_cast<trx_mode>(0);
508 }
509 
510 // initialise the notifications window
notify_init_window(void)511 static void notify_init_window(void)
512 {
513 	notify_window = make_notify_window();
514 	notify_window->xclass(PACKAGE_TARNAME);
515 	dxcc_window = make_dxcc_window();
516 	dxcc_window->xclass(PACKAGE_TARNAME);
517 
518 	struct { Fl_Button* button; const char* label; } buttons[] = {
519 		{ btnNotifyAdd, icons::make_icon_label(_("Add"), plus_icon) },
520 		{ btnNotifyRemove, icons::make_icon_label(_("Remove"), minus_icon) },
521 		{ btnNotifyUpdate, icons::make_icon_label(_("Update"), refresh_icon) },
522 		{ btnNotifyTest, icons::make_icon_label(_("Test..."), applications_system_icon) },
523 		{ btnNotifyClose, icons::make_icon_label(_("Close"), close_icon) },
524 
525 		{ btnNotifyDXCCSelect, icons::make_icon_label(_("Select All"), edit_select_all_icon) },
526 		{ btnNotifyDXCCDeselect, icons::make_icon_label(_("Clear All"), edit_clear_icon) },
527 		{ btnNotifyDXCCClose, icons::make_icon_label(_("Close"), close_icon) },
528 	};
529 	for (size_t i = 0; i < sizeof(buttons)/sizeof(*buttons); i++) {
530 		buttons[i].button->label(buttons[i].label);
531 		icons::set_icon_label(buttons[i].button);
532 		buttons[i].button->align(FL_ALIGN_LEFT | FL_ALIGN_INSIDE);
533 	}
534 	struct { Fl_Button* button; const char** icon; } buttons2[] = {
535 		{ btnNotifyFilterDXCC, text_editor_icon },
536 		{ btnNotifyActionDialogDefault, text_icon },
537 		{ btnNotifyActionMarkerDefault, text_icon },
538 		{ btnNotifyActionMacro, text_editor_icon },
539 		{ btnNotifyActionProgram, folder_open_icon }
540 	};
541 	for (size_t i = 0; i < sizeof(buttons2)/sizeof(*buttons2); i++)
542 		buttons2[i].button->image(new Fl_Pixmap(buttons2[i].icon));
543 	inpNotifyRE->hide();
544 	grpNotifyFilter->deactivate();
545 	mnuNotifyEvent->menu(notify_event_menu);
546 	btnNotifyEnabled->value(1);
547 	inpNotifyRE->callback(notify_re_cb);
548 
549 	int w = (tblNotifyList->w() - Fl::box_dw(tblNotifyList->box())) / 4;
550 	struct col_info_t {
551 		const char* label;
552 		int width;
553 	};
554 	col_info_t ncols[] = {
555 		{ "Event", w + 30 }, { "Filter", w + 30 },
556 		{ "Action", w - 30 }, { "Enabled", w - 30 },
557 	};
558 	for (size_t i = 0; i < sizeof(ncols)/sizeof(*ncols); i++) {
559 		tblNotifyList->addColumn(ncols[i].label, ncols[i].width,
560 					 static_cast<Fl_Align>(FL_ALIGN_CENTER | FL_ALIGN_CLIP));
561 	}
562 	tblNotifyList->rowSize(FL_NORMAL_SIZE);
563 	tblNotifyList->headerSize(FL_NORMAL_SIZE);
564 	tblNotifyList->allowSort(false);
565 	tblNotifyList->when(FL_WHEN_CHANGED | FL_WHEN_NOT_CHANGED);
566 	tblNotifyList->menu(notify_list_context_menu);
567 	for (int i = 0; i < notify_list_context_menu->size(); i++)
568 		icons::set_icon_label(&notify_list_context_menu[i]);
569 
570 	w = (tblNotifyFilterDXCC->w() - Fl::box_dw(tblNotifyFilterDXCC->box()) -
571 	     tblNotifyFilterDXCC->scrollbSize()) / NOTIFY_DXCC_NUMCOL;
572 	col_info_t dcols[NOTIFY_DXCC_NUMCOL] = {
573 		{ "", 25 }, { _("Country"), w + w - 25 + 2*(w - 45) },
574 		{ _("Continent"), w }, { "ITU", 45 }, { "CQ", 45 }
575 	};
576 	for (size_t i = 0; i < NOTIFY_DXCC_NUMCOL; i++) {
577 		tblNotifyFilterDXCC->addColumn(dcols[i].label, dcols[i].width,
578 					       static_cast<Fl_Align>(FL_ALIGN_CENTER | FL_ALIGN_CLIP), strcmp);
579 	}
580 	tblNotifyFilterDXCC->columnAlign(NOTIFY_DXCC_COL_CN, static_cast<Fl_Align>(FL_ALIGN_LEFT | FL_ALIGN_CLIP));
581 	tblNotifyFilterDXCC->rowSize(FL_NORMAL_SIZE);
582 	tblNotifyFilterDXCC->headerSize(FL_NORMAL_SIZE);
583 	tblNotifyFilterDXCC->when(FL_WHEN_CHANGED | FL_WHEN_NOT_CHANGED);
584 	tblNotifyFilterDXCC->menu(notify_dxcc_context_menu);
585 	btnNotifyDXCCSelect->callback(notify_filter_dxcc_select_cb, (void*)NOTIFY_DXCC_SELECT_ALL);
586 	btnNotifyDXCCDeselect->callback(notify_filter_dxcc_select_cb, (void*)NOTIFY_DXCC_DESELECT_ALL);
587 	inpNotifyDXCCSearchCountry->callback(notify_filter_dxcc_search);
588 	inpNotifyDXCCSearchCountry->when(FL_WHEN_CHANGED | FL_WHEN_NOT_CHANGED | FL_WHEN_ENTER_KEY);
589 	inpNotifyDXCCSearchCallsign->callback(notify_filter_dxcc_search);
590 	inpNotifyDXCCSearchCallsign->when(FL_WHEN_CHANGED);
591 	inpNotifyDXCCSearchCallsign->textfont(FL_HELVETICA);
592 
593 	inpNotifyActionDialog->textfont(FL_HELVETICA);
594 	inpNotifyActionRXMarker->textfont(FL_HELVETICA);
595 	inpNotifyActionMacro->textfont(FL_HELVETICA);
596 	inpNotifyActionProgram->textfont(FL_HELVETICA);
597 	inpNotifyRE->textfont(FL_HELVETICA);
598 	inpNotifyFilterCall->textfont(FL_HELVETICA);
599 
600 	tblNotifyList->callback(notify_select_cb);
601 	mnuNotifyEvent->callback(notify_event_cb, NOTIFY_SET_DUP_MENU);
602 	btnNotifyFilterDXCC->callback(notify_dxcc_browse_cb);
603 	btnNotifyAdd->callback(notify_add_cb);
604 	btnNotifyRemove->callback(notify_remove_cb);
605 	btnNotifyUpdate->callback(notify_update_cb, (void*)NOTIFY_LIST_MENU_UPDATE);
606 	btnNotifyActionDialogDefault->callback(notify_dialog_default_cb);
607 	btnNotifyActionMarkerDefault->callback(notify_rx_default_cb);
608 	btnNotifyActionProgram->callback(notify_program_select_cb);
609 	btnNotifyActionMacro->callback(notify_macro_edit_cb);
610 	btnNotifyTest->callback(notify_test_cb);
611 	tblNotifyFilterDXCC->callback(notify_dxcc_check_cb);
612 	chkNotifyDupIgnore->callback(notify_dup_ignore_cb);
613 	mnuNotifyDupWhich->menu(notify_dup_refs_menu);
614 
615 	chkNotifyFilterCall->value(0);
616 	chkNotifyFilterDXCC->value(0);
617 	inpNotifyFilterCall->hide();
618 	btnNotifyFilterDXCC->hide();
619 
620 	chkNotifyDupIgnore->value(1);
621 	chkNotifyDupIgnore->do_callback();
622 	chkNotifyDupBand->value(1);
623 	chkNotifyDupMode->value(1);
624 	cntNotifyDupTime->value(3600);
625 	mnuNotifyEvent->do_callback(); // for the dup menu
626 
627 	dxcc_list = dxcc_entity_list();
628 	if (dxcc_list) {
629 		char cq[5], itu[5];
630 		for (vector<dxcc*>::const_iterator i = dxcc_list->begin(); i != dxcc_list->end(); ++i) {
631 			snprintf(itu, sizeof(itu), "%02d", (*i)->itu_zone);
632 			snprintf(cq, sizeof(cq), "%02d", (*i)->cq_zone);
633 			tblNotifyFilterDXCC->addRow(NOTIFY_DXCC_NUMCOL, "[x]", (*i)->country,
634 						    (*i)->continent, itu, cq);
635 		}
636 	}
637 	else {
638 		chkNotifyFilterDXCC->deactivate();
639 		btnNotifyFilterDXCC->deactivate();
640 	}
641 
642 	unsigned char q = qsl_is_open();
643 	if (!(q & (1 << QSL_LOTW)))
644 		chkNotifyFilterLOTW->deactivate();
645 	if (!(q & (1 << QSL_EQSL)))
646 		chkNotifyFilterEQSL->deactivate();
647 
648 }
649 
650 // append event n to the table widget
notify_table_append(const notify_t & n)651 static void notify_table_append(const notify_t& n)
652 {
653 	// add to table
654 	string fcol, acol;
655 	if (n.event == NOTIFY_EVENT_MYCALL)
656 		fcol = "My callsign";
657 	else if (n.event == NOTIFY_EVENT_STATION) {
658 		if (n.filter.match == NOTIFY_FILTER_CALLSIGN)
659 			fcol += "Callsign";
660 		else if (n.filter.match == NOTIFY_FILTER_DXCC)
661 			fcol += "DXCC";
662 	}
663 	else if (n.event == NOTIFY_EVENT_CUSTOM)
664 		fcol = n.re;
665 	if (n.filter.nwb)
666 		fcol += ", N";
667 	if (n.filter.lotw)
668 		fcol += ", L";
669 	if (n.filter.eqsl)
670 		fcol += ", E";
671 
672 	if (!n.action.alert.empty())
673 		acol = "A";
674 	if (!n.action.rx_marker.empty()) {
675 		if (!acol.empty())
676 			acol += ", ";
677 		acol += "RX";
678 	}
679 	if (!n.action.macro.empty()) {
680 		if (!acol.empty())
681 			acol += ", ";
682 		acol += "TX";
683 	}
684 	if (!n.action.program.empty()) {
685 		if (!acol.empty())
686 			acol += ", ";
687 		acol += "P";
688 	}
689 	tblNotifyList->addRow(4, notify_event_menu[n.event].label(),
690 			      fcol.c_str(), acol.c_str(), n.enabled ? "Y" : "N");
691 	tblNotifyList->value(tblNotifyList->rows() - 1);
692 	tblNotifyList->redraw();
693 }
694 
695 // clear and reload the event table
notify_table_reload(void)696 static void notify_table_reload(void)
697 {
698 	tblNotifyList->clear();
699 	for (notify_list_t::const_iterator i = notify_list.begin(); i != notify_list.end(); ++i)
700 		notify_table_append(*i);
701 }
702 
703 ////////////////////////////////////////////////////////////////////////////////
704 // spotter and notification functions
705 ////////////////////////////////////////////////////////////////////////////////
706 
707 static notify_dialog *alert_window = 0;
708 
notify_goto_freq_cb(Fl_Widget * w,void * arg)709 static void notify_goto_freq_cb(Fl_Widget* w, void* arg)
710 {
711 	const notify_t* n = static_cast<notify_t*>(arg);
712 	if (progdefaults.rsid_mark) // mark current modem & freq
713 		REQ(note_qrg, false, "\nBefore RSID: ", "\n",
714 			active_modem->get_mode(), 0LL, active_modem->get_freq());
715 	if (active_modem->get_mode() != n->mode)
716 		init_modem_sync(n->mode);
717 	qsy(n->rfreq, n->afreq);
718 	if (n->event == NOTIFY_EVENT_RSID && btnRSID->value() &&
719 	    progdefaults.rsid_auto_disable && progdefaults.rsid_notify_only)
720 		toggleRSID();
721 	w->parent()->hide();
722 }
723 
notify_alert_window_cb(Fl_Widget * w,void * arg)724 static void notify_alert_window_cb(Fl_Widget* w, void* arg)
725 {
726 	delete static_cast<notify_t*>(arg);
727 	w->hide();
728 }
729 
notify_show_alert(const notify_t & n,const char * msg)730 static void notify_show_alert(const notify_t& n, const char* msg)
731 {
732 	if (!alert_window) alert_window = new notify_dialog;
733 	if (alert_window->visible())
734 		return;
735 
736 	Fl_Button* goto_freq = alert_window->make_button(120);
737 	if (goto_freq) {
738 		char label[32];
739 		snprintf(label, sizeof(label), "Go to %d Hz", n.afreq);
740 		goto_freq->copy_label(label);
741 		goto_freq->callback(notify_goto_freq_cb, new notify_t(n));
742 		alert_window->callback(notify_alert_window_cb, goto_freq->user_data());
743 	}
744 
745 	alert_window->notify(msg, n.action.alert_timeout);
746 	REQ(show_notifier, alert_window);
747 }
748 
749 struct replace_refs
750 {
751 	enum {
752 		REF_MODEM, REF_DF_HZ, REF_RF_HZ, REF_RF_KHZ, REF_AF_HZ,
753 		REF_LF_KHZ, REF_CALLSIGN, REF_COUNTRY, REF_MATCHED_TEXT, REF_TEXT
754 	};
replace_varreplace_refs755 	static void replace_var(const notify_t& n, int t, char* str, size_t len)
756 	{
757 		switch (t) {
758 		case REF_MODEM:
759 			strncpy(str, mode_info[n.mode].sname, len-1);
760 			str[len - 1] = '\0';
761 			break;
762 		case REF_DF_HZ:
763 			snprintf(str, len, "%lld", wf->rfcarrier());
764 			break;
765 		case REF_RF_HZ: case REF_RF_KHZ: {
766 			long long hz = wf->rfcarrier() + (wf->USB() ? n.afreq : -n.afreq);
767 			if (t == REF_RF_HZ)
768 				snprintf(str, len, "%lld", hz);
769 			else // REF_RF_KHZ
770 				snprintf(str, len, "%.3f", (double)hz / 1000.0);
771 			break;
772 		}
773 		case REF_AF_HZ:
774 			snprintf(str, len, "%d", n.afreq);
775 			break;
776 		case REF_LF_KHZ:
777 			strncpy(str, inpFreq->value(), len-1);
778 			str[len - 1] = '\0';
779 			break;
780 		case REF_CALLSIGN:
781 			if (n.event == NOTIFY_EVENT_MYCALL || n.event == NOTIFY_EVENT_STATION) {
782 				stringstream info;
783 				info << "\\" << event_regex[n.event].index;
784 				strncpy(str, info.str().c_str(), len-1);
785 			}
786 			break;
787 		case REF_COUNTRY:
788 			if (n.event == NOTIFY_EVENT_STATION) {
789 				strncpy(str, n.filter.dxcc_last.c_str(), len-1);
790 				str[len - 1] = '\0';
791 			}
792 			break;
793 		case REF_MATCHED_TEXT:
794 			snprintf(str, len, "\\0");
795 			break;
796 		case REF_TEXT:
797 			strncpy(str, n.match_string, len-1);
798 			str[len - 1] = '\0';
799 			break;
800 		}
801 	}
802 
operator ()replace_refs803 	void operator()(const notify_t& n, string& edit)
804 	{
805 		char buf[128];
806 		string::size_type p;
807 		// replace $VARIABLES
808 		const char* vars[] = {
809 			"$MODEM", "$DF_HZ", "$RF_HZ", "$RF_KHZ", "$AF_HZ",
810 			"$LF_KHZ", "$CALLSIGN", "$COUNTRY", "$MATCHED_TEXT", "$TEXT"
811 		};
812 		for (size_t i = 0; i < sizeof(vars)/sizeof(*vars); i++) {
813 			if ((p = edit.find(vars[i])) != string::npos) {
814 				replace_var(n, i, buf, sizeof(buf));
815 				edit.replace(p, strlen(vars[i]), buf);
816 			}
817 		}
818 		// replace \X refs with regex substrings
819 		strcpy(buf, "\\0");
820 		for (size_t i = 0; i < n.submatch_length; i++, buf[1]++)
821 			for (p = 0; (p = edit.find(buf, p)) != string::npos; p = 0)
822 				edit.replace(p, 2, n.match_string + n.submatch_offsets[i].rm_so,
823 					     n.submatch_offsets[i].rm_eo - n.submatch_offsets[i].rm_so);
824 	}
825 };
826 
827 // perform the actions for event n
notify_notify(const notify_t & n)828 static void notify_notify(const notify_t& n)
829 {
830 	// show alert window with timeout
831 	if (!n.action.alert.empty()) {
832 		string alert = n.action.alert;
833 		replace_refs()(n, alert);
834 		if (alert.find('%') == string::npos)
835 			notify_show_alert(n, alert.c_str());
836 		else { // treat alert text as strftime format string
837 			size_t len = alert.length() + 256;
838 			char* buf = new char[len];
839 			time_t t = time(NULL);
840 			struct tm ts;
841 			if (localtime_r(&t, &ts) && strftime(buf, len, alert.c_str(), &ts))
842 				notify_show_alert(n, buf);
843 			delete [] buf;
844 		}
845 	}
846 
847 	// append to receive text
848 	if (!n.action.rx_marker.empty()) {
849 		string text = n.action.rx_marker;
850 		replace_refs()(n, text);
851 		string::size_type p;
852 		if ((p = text.find("$RX_MARKER")) != string::npos) {
853 			text[p] = '\0';
854 			note_qrg(false, text.c_str(), text.c_str() + p + strlen("$RX_MARKER"), n.mode, 0LL, n.afreq);
855 		}
856 		else
857 			ReceiveText->addstr(text);
858 	}
859 
860 	// expand macros and append to transmit text
861 	if (!n.action.macro.empty()) {
862 		MACROTEXT m;
863 		m.text[0] = n.action.macro;
864 		replace_refs()(n, m.text[0]);
865 		m.execute(0);
866 	}
867 
868 	// define substring & macro variables and run program
869 	if (!n.action.program.empty()) {
870 #ifndef __MINGW32__
871 		switch (fork()) {
872 		case -1:
873 			LOG_PERROR("fork");
874 			// fall through
875 		default:
876 			break;
877 		case 0:
878 #endif
879 			char var[] = "FLDIGI_NOTIFY_STR_0";
880 			string val;
881 			for (size_t i = 0; i < n.submatch_length; i++, var[sizeof(var) - 2]++) {
882 				val.assign(n.match_string + n.submatch_offsets[i].rm_so,
883 					   n.submatch_offsets[i].rm_eo - n.submatch_offsets[i].rm_so);
884 				setenv(var, val.c_str(), 1);
885 				if (i == event_regex[n.event].index)
886 					setenv("FLDIGI_NOTIFY_CALLSIGN", val.c_str(), 1);
887 			}
888 			setenv("FLDIGI_NOTIFY_TEXT", n.match_string, 1);
889 
890 			unsigned int un = n.submatch_length;
891 			snprintf(var, sizeof(var), "%u", un);
892 
893 			setenv("FLDIGI_NOTIFY_STR_NUM", var, 1);
894 			snprintf(var, sizeof(var), "%d", n.afreq);
895 			setenv("FLDIGI_NOTIFY_AUDIO_FREQUENCY", var, 1);
896 			snprintf(var, sizeof(var), "%u", (unsigned)n.event);
897 			setenv("FLDIGI_NOTIFY_EVENT", var, 1);
898 			if (n.event == NOTIFY_EVENT_STATION)
899 				setenv("FLDIGI_NOTIFY_COUNTRY", n.filter.dxcc_last.c_str(), 1);
900 
901 			set_macro_env(); // also set macro variables
902 #ifdef __MINGW32__
903 			char* cmd = strdup(n.action.program.c_str());
904 			STARTUPINFO si;
905 			PROCESS_INFORMATION pi;
906 			memset(&si, 0, sizeof(si));
907 			si.cb = sizeof(si);
908 			memset(&pi, 0, sizeof(pi));
909 			if (!CreateProcess(NULL, cmd, NULL, NULL, FALSE, CREATE_NO_WINDOW, NULL, NULL, &si, &pi))
910 				LOG_ERROR("CreateProcess failed with error code %ld", GetLastError());
911 			CloseHandle(pi.hProcess);
912 			CloseHandle(pi.hThread);
913 			free(cmd);
914 #else
915 			execl("/bin/sh", "sh", "-c", n.action.program.c_str(), (char *)NULL);
916 			perror("execl");
917 			exit(EXIT_FAILURE);
918 		}
919 #endif
920 	}
921 }
922 
923 // return true if the event n is a dup
notify_is_dup(notify_t & n,const char * str,const regmatch_t * sub,size_t len,time_t now,int afreq,trx_mode mode)924 static bool notify_is_dup(notify_t& n, const char* str, const regmatch_t* sub, size_t len,
925 			  time_t now, int afreq, trx_mode mode)
926 {
927 	if (!n.dup_ignore)
928 		return false;
929 
930 	if (n.dup_ref == 0 || n.dup_ref >= len) {
931 		stringstream info;
932 		info << "Bad dup_ref: " << n.dup_ref << " >= " << len;
933 		LOG_ERROR("%s", info.str().c_str());
934 		return false;
935 	}
936 	const regmatch_t& subidx = sub[n.dup_ref];
937 	string dupstr(subidx.rm_eo - subidx.rm_so, '\0');
938 	transform(str + subidx.rm_so, str + subidx.rm_eo, dupstr.begin(), static_cast<int (*)(int)>(toupper));
939 
940 	notify_dup_t cur = { now, band(wf->rfcarrier()), mode };
941 	if (n.event == NOTIFY_EVENT_RSID)
942 		cur.freq = wf->rfcarrier() + (wf->USB() ? afreq : -afreq);
943 
944 	notify_seen_t::iterator i;
945 	bool is_dup = false;
946 	if ((i = n.last_seen.find(dupstr)) != n.last_seen.end()) {
947 		const notify_dup_t& prev = i->second;
948 		is_dup = (cur.when - prev.when < n.dup.when);
949 		if (n.event == NOTIFY_EVENT_RSID)
950 			is_dup = is_dup && ::llabs(cur.freq - prev.freq) <= ceil(RSID_PRECISION);
951 		if (n.dup.band)
952 			is_dup = is_dup && cur.band == prev.band;
953 		if (n.dup.mode)
954 			is_dup = is_dup && cur.mode == prev.mode;
955 	}
956 	if (is_dup)
957 		return true;
958 
959 	n.last_seen[dupstr] = cur;
960 
961 	if (n.last_seen.size() > 1) { // remove old data
962 		for (i = n.last_seen.begin(); i != n.last_seen.end();) {
963 			if (now - i->second.when > n.dup.when)
964 				n.last_seen.erase(i++);
965 			else
966 				++i;
967 		}
968 	}
969 
970 	return false;
971 }
972 
973 static fre_t notify_filter_call_re("", REG_EXTENDED | REG_NOSUB | REG_ICASE);
974 
975 // Called by the spotter when an event's regular expression matches the decoded text.
976 // Also called by notify_rsid for RSID recepction with: the frequency in afreq,
977 // the mode name in str, the str bounds in the sub array, and len = 1.
notify_recv(trx_mode mode,int afreq,const char * str,const regmatch_t * sub,size_t len,void * data)978 static void notify_recv(trx_mode mode, int afreq, const char* str, const regmatch_t* sub, size_t len, void* data)
979 {
980 	notify_t* n = reinterpret_cast<notify_t*>(data);
981 	time_t now = time(0);
982 
983 	// check if we may trigger this event
984 	if (!n->enabled || now - n->last_trigger < n->action.trigger_limit)
985 		return;
986 
987 	switch (n->event) {
988 	case NOTIFY_EVENT_MYCALL: case NOTIFY_EVENT_CUSTOM: case NOTIFY_EVENT_RSID:
989 		break;
990 	case NOTIFY_EVENT_STATION:
991 		size_t re_idx = event_regex[n->event].index;
992 		string call(str + sub[re_idx].rm_so, sub[re_idx].rm_eo - sub[re_idx].rm_so);
993 		const dxcc* e = dxcc_lookup(call.c_str());
994 
995 		if (n->filter.match == NOTIFY_FILTER_CALLSIGN) {
996 			if (e) // remember the country name we found
997 				n->filter.dxcc_last = e->country;
998 			if (!n->filter.callsign.empty()) { // the callsign must match
999 				if (notify_filter_call_re.re() != n->filter.callsign) // compile new re
1000 					notify_filter_call_re.recompile(n->filter.callsign.c_str());
1001 				if (!notify_filter_call_re.match(call.c_str()))
1002 					return;
1003 			}
1004 			else { // check for nwb, lotw, eqsl
1005 				if (n->filter.nwb && SearchLog(call.c_str()))
1006 					return;
1007 				if ((n->filter.lotw || n->filter.eqsl) && qsl_is_open() &&
1008 				    !(qsl_lookup(call.c_str()) & (1 << QSL_LOTW | 1 << QSL_EQSL)))
1009 					return;
1010 			}
1011 		}
1012 		else if (n->filter.match == NOTIFY_FILTER_DXCC) {
1013 			if (e) {
1014 				n->filter.dxcc_last = e->country;
1015 
1016 				// if the dxcc filter is not empty, it must contain the country for call
1017 				if (!n->filter.dxcc.empty() && n->filter.dxcc.find(e->country) == n->filter.dxcc.end())
1018 					return;
1019 				if (n->filter.nwb && qsodb_dxcc_entity_find(e->country))
1020 					return;
1021 			}
1022 
1023 			if ((n->filter.lotw || n->filter.eqsl) && qsl_is_open() &&
1024 			    !(qsl_lookup(call.c_str()) & (1 << QSL_LOTW | 1 << QSL_EQSL)))
1025 				return;
1026 		}
1027 		break;
1028 	}
1029 
1030 	if (!notify_is_dup(*n, str, sub, len, now, afreq, mode)) {
1031 		n->last_trigger = now;
1032 		n->afreq = afreq;
1033 		n->rfreq = wf->rfcarrier();
1034 		n->mode = mode;
1035 		n->match_string = str;
1036 		n->submatch_offsets = sub;
1037 		n->submatch_length = min(len, (size_t)10); // whole string + up to 9 user-specified backrefs
1038 
1039 //std::cout << "trigger: " << n->last_trigger << "\n" <<
1040 //"audio freq: " << n->afreq << "\n" <<
1041 //"rf carrier: " << n->rfreq << "\n" <<
1042 //"mode #: " << n->mode << "\n" <<
1043 //"mode: " << mode_info[n->mode].name << "\n" <<
1044 //"match: " << n->match_string << "\n" <<
1045 //"offsets: " << n->submatch_offsets << "\n" <<
1046 //"length: " << n->submatch_length << std::endl;
1047 
1048 		notify_notify(*n);
1049 	}
1050 }
1051 
notify_set_qsodb_cache(void)1052 static void notify_set_qsodb_cache(void)
1053 {
1054 	bool v = false;
1055 	for (notify_list_t::iterator i = notify_list.begin(); i != notify_list.end(); ++i) {
1056 		if (i->event == NOTIFY_EVENT_STATION && i->filter.match == NOTIFY_FILTER_DXCC && i->enabled) {
1057 			v = true;
1058 			break;
1059 		}
1060 	}
1061 	dxcc_entity_cache_enable(v);
1062 }
1063 
1064 // register event n with the spotter
notify_register(notify_t & n)1065 static void notify_register(notify_t& n)
1066 {
1067 	if (n.event != NOTIFY_EVENT_RSID)
1068 		spot_register_recv(notify_recv, &n, notify_get_re(n).c_str(), REG_EXTENDED | REG_ICASE);
1069 }
1070 
1071 // unregister event n
notify_unregister(const notify_t & n)1072 static void notify_unregister(const notify_t& n)
1073 {
1074 	if (n.event != NOTIFY_EVENT_RSID)
1075 		spot_unregister_recv(notify_recv, &n);
1076 }
1077 
1078 ////////////////////////////////////////////////////////////////////////////////
1079 // callbacks
1080 ////////////////////////////////////////////////////////////////////////////////
1081 
1082 // the event table callback
notify_select_cb(Fl_Widget * w,void * arg)1083 static void notify_select_cb(Fl_Widget* w, void* arg)
1084 {
1085 	int v = tblNotifyList->value();
1086 	if (v >= 0)
1087 		notify_event_to_gui(notify_tmp = *advli(notify_list.begin(), v));
1088 }
1089 
1090 // the remove button/menu item callback
notify_remove_cb(Fl_Widget * w,void *)1091 static void notify_remove_cb(Fl_Widget* w, void*)
1092 {
1093 	int v = tblNotifyList->value();
1094 	if (v < 0)
1095 		return;
1096 
1097 	// unregister
1098 	notify_unregister(*advli(notify_list.begin(), v));
1099 	// remove from list
1100 	notify_list.erase(advli(notify_list.begin(), v));
1101 	// remove from table
1102 	tblNotifyList->removeRow(tblNotifyList->value());
1103 	// select next row
1104 	if (w == btnNotifyRemove) {
1105 		if (v >= tblNotifyList->rows())
1106 			v = tblNotifyList->rows() - 1;
1107 		if (v >= 0) {
1108 			tblNotifyList->value(v);
1109 			notify_select_cb(tblNotifyList, 0);
1110 		}
1111 	}
1112 
1113 	notify_save();
1114 }
1115 
1116 enum {
1117 	NOTIFY_CHECK_CUSTOM_RE_EMPTY = 1 << 0,
1118 	NOTIFY_CHECK_CUSTOM_RE_VALID = 1 << 1,
1119 	NOTIFY_CHECK_MYCALL_NOT_EMPTY = 1 << 2
1120 };
1121 // do some sanity checks on the widget values before adding/updating an event
notify_check(unsigned check)1122 static bool notify_check(unsigned check)
1123 {
1124 	if (mnuNotifyEvent->value() == NOTIFY_EVENT_CUSTOM) {
1125 		if (check & NOTIFY_CHECK_CUSTOM_RE_EMPTY) {
1126 			if (!inpNotifyRE->size()) {
1127 				fl_alert2(_("The regular expression field must not be empty."));
1128 				return false;
1129 			}
1130 		}
1131 		if (check & NOTIFY_CHECK_CUSTOM_RE_VALID) {
1132 			if (!fre_t(inpNotifyRE->value(), REG_EXTENDED | REG_ICASE)) {
1133 				fl_alert2(_("The regular expression must be valid."));
1134 				return false;
1135 			}
1136 		}
1137 	}
1138 	if ((check & NOTIFY_CHECK_MYCALL_NOT_EMPTY) && mnuNotifyEvent->value() == NOTIFY_EVENT_MYCALL) {
1139 		if (progdefaults.myCall.empty()) {
1140 			fl_alert2(_("Please set your callsign first."));
1141 			return false;
1142 		}
1143 	}
1144 	// ...
1145 
1146 	return true;
1147 }
1148 
1149 // the add button callback
notify_add_cb(Fl_Widget * w,void * arg)1150 static void notify_add_cb(Fl_Widget* w, void* arg)
1151 {
1152 	if (!notify_check(~0))
1153 		return;
1154 
1155 	notify_gui_to_event(notify_tmp);
1156 	// add to list
1157 	notify_list.push_back(notify_tmp);
1158 	// add to table
1159 	notify_table_append(notify_list.back());
1160 	// register with spotter
1161 	notify_register(notify_list.back());
1162 	// save file
1163 	notify_save();
1164 }
1165 
1166 // the update button/menu item callback
notify_update_cb(Fl_Widget * w,void * arg)1167 static void notify_update_cb(Fl_Widget* w, void* arg)
1168 {
1169 	int v = tblNotifyList->value();
1170 	if (v < 0)
1171 		return;
1172 	if (!notify_check(~0))
1173 		return;
1174 	notify_t& nv = *advli(notify_list.begin(), v);
1175 
1176 	if ((intptr_t)arg != NOTIFY_LIST_MENU_TOGGLE) {
1177 		notify_gui_to_event(notify_tmp);
1178 		nv = notify_tmp;
1179 		if (nv.dup_ignore)
1180 			nv.last_seen.clear();
1181 	}
1182 	else { // only toggle the enabled status
1183 		nv.enabled = !nv.enabled;
1184 		btnNotifyEnabled->value(nv.enabled);
1185 	}
1186 	if (!nv.enabled)
1187 		notify_unregister(nv);
1188 	else {
1189 		notify_unregister(nv);
1190 		notify_register(nv);
1191 	}
1192 
1193 	notify_table_reload();
1194 	tblNotifyList->value(v);
1195 	notify_save();
1196 }
1197 
1198 // the event selection menu callback
notify_event_cb(Fl_Widget * w,void * arg)1199 static void notify_event_cb(Fl_Widget* w, void* arg)
1200 {
1201 	notify_event_t e = static_cast<notify_event_t>(reinterpret_cast<Fl_Choice*>(w)->value());
1202 	switch (e) {
1203 	case NOTIFY_EVENT_MYCALL:
1204 		mnuNotifyDupWhich->activate();
1205 		chkNotifyDupBand->activate();
1206 		chkNotifyDupMode->activate();
1207 		grpNotifyFilter->deactivate();
1208 		chkNotifyFilterCall->value(0);
1209 		inpNotifyFilterCall->hide();
1210 		chkNotifyFilterDXCC->value(0);
1211 		btnNotifyFilterDXCC->hide();
1212 		chkNotifyFilterNWB->value(0);
1213 		chkNotifyFilterLOTW->value(0);
1214 		chkNotifyFilterEQSL->value(0);
1215 		inpNotifyRE->value(0);
1216 		inpNotifyRE->hide();
1217 		btnNotifyActionDialogDefault->show();
1218 		break;
1219 	case NOTIFY_EVENT_STATION:
1220 		mnuNotifyDupWhich->activate();
1221 		chkNotifyDupBand->activate();
1222 		chkNotifyDupMode->activate();
1223 		grpNotifyFilter->activate();
1224 		if (!chkNotifyFilterCall->value() && !chkNotifyFilterDXCC->value()) {
1225 			chkNotifyFilterCall->value(1);
1226 			inpNotifyFilterCall->show();
1227 		}
1228 		inpNotifyRE->value(0);
1229 		inpNotifyRE->hide();
1230 		btnNotifyActionDialogDefault->show();
1231 		break;
1232 	case NOTIFY_EVENT_CUSTOM:
1233 		mnuNotifyDupWhich->activate();
1234 		chkNotifyDupBand->activate();
1235 		chkNotifyDupMode->activate();
1236 		grpNotifyFilter->deactivate();
1237 		chkNotifyFilterCall->value(0);
1238 		inpNotifyFilterCall->hide();
1239 		chkNotifyFilterDXCC->value(0);
1240 		btnNotifyFilterDXCC->hide();
1241 		chkNotifyFilterNWB->value(0);
1242 		chkNotifyFilterLOTW->value(0);
1243 		chkNotifyFilterEQSL->value(0);
1244 		inpNotifyRE->show();
1245 		break;
1246 	case NOTIFY_EVENT_RSID:
1247 		grpNotifyFilter->deactivate();
1248 		chkNotifyFilterCall->value(0);
1249 		inpNotifyFilterCall->hide();
1250 		chkNotifyFilterDXCC->value(0);
1251 		btnNotifyFilterDXCC->hide();
1252 		chkNotifyFilterNWB->value(0);
1253 		chkNotifyFilterLOTW->value(0);
1254 		chkNotifyFilterEQSL->value(0);
1255 		inpNotifyRE->value(0);
1256 		inpNotifyRE->hide();
1257 		btnNotifyActionDialogDefault->show();
1258 		// limited dup handling
1259 		mnuNotifyDupWhich->deactivate();
1260 		chkNotifyDupBand->value(0);
1261 		chkNotifyDupBand->deactivate();
1262 		chkNotifyDupMode->value(1);
1263 		chkNotifyDupMode->deactivate();
1264 		break;
1265 	}
1266 
1267 	if (arg == NOTIFY_SET_DUP_MENU) {
1268 		notify_gui_to_event(notify_tmp);
1269 		notify_set_event_dup(notify_tmp);
1270 	}
1271 	w->parent()->redraw();
1272 }
1273 
1274 // the program filesystem browse button callback
notify_program_select_cb(Fl_Widget * w,void * arg)1275 static void notify_program_select_cb(Fl_Widget* w, void* arg)
1276 {
1277 	const char* fn = FSEL::select(_("Run program"), "", 0, 0);
1278 	if (!fn) return;
1279 	if (!*fn) return;
1280 
1281 	inpNotifyActionProgram->value(fn);
1282 	// quote program path
1283 	inpNotifyActionProgram->position(0);
1284 	inpNotifyActionProgram->insert("\"", 1);
1285 	inpNotifyActionProgram->position(inpNotifyActionProgram->size());
1286 	inpNotifyActionProgram->insert("\"", 1);
1287 
1288 }
1289 
1290 // the test button callback
notify_test_cb(Fl_Widget * w,void * arg)1291 static void notify_test_cb(Fl_Widget* w, void* arg)
1292 {
1293 	notify_gui_to_event(notify_tmp);
1294 
1295 	if (notify_tmp.event == NOTIFY_EVENT_RSID) {
1296 		notify_tmp.mode = active_modem->get_mode();
1297 		regmatch_t sub[2] = { { 0, (regoff_t)strlen(mode_info[notify_tmp.mode].name) } };
1298 		sub[1] = sub[0];
1299 		notify_recv(notify_tmp.mode, active_modem->get_freq(),
1300 			    mode_info[notify_tmp.mode].name, sub, 2, &notify_tmp);
1301 		return;
1302 	}
1303 
1304 	string test_strings[3];
1305 	test_strings[NOTIFY_EVENT_MYCALL].assign(progdefaults.myCall).append(" de n0call");
1306 	test_strings[NOTIFY_EVENT_STATION] = "cq de n0call n0call ";
1307 	static string test;
1308 	if (test.empty())
1309 		test = test_strings[notify_tmp.event];
1310 
1311 	string msg;
1312 	msg.assign(_("Default test string is:\n  \"")).append(test_strings[notify_tmp.event]).append("\"\n")
1313 		.append(_("Enter test string or leave blank for default:"));
1314         const char* s = fl_input2("%s", msg.c_str(), test.c_str());
1315 	if (s) {
1316 		if (test.assign(s).empty()) // empty input
1317 			test = test_strings[notify_tmp.event];
1318 	}
1319 	else // cancelled
1320 		return;
1321 
1322 	fre_t re(notify_get_re(notify_tmp).c_str(), REG_EXTENDED | REG_ICASE);
1323 	if (!re)
1324 		fl_alert2(_("This event's regular expression is invalid."));
1325 	else if (re.match(test.c_str())) {
1326 		const vector<regmatch_t>& o = re.suboff();
1327 		notify_recv(active_modem->get_mode(), active_modem->get_freq(),
1328 			    test.c_str(), &o[0], o.size(), &notify_tmp);
1329 	}
1330 	else
1331 		fl_message2(_("The test string did not match this event's search pattern."));
1332 }
1333 
1334 // the macro editor button callback
notify_macro_edit_cb(Fl_Widget * w,void * arg)1335 static void notify_macro_edit_cb(Fl_Widget* w, void* arg)
1336 {
1337 	editMacro(0, MACRO_EDIT_INPUT, inpNotifyActionMacro);
1338 }
1339 
1340 // the insert default alert text button callback
notify_dialog_default_cb(Fl_Widget * w,void * arg)1341 static void notify_dialog_default_cb(Fl_Widget* w, void* arg)
1342 {
1343 	size_t i = CLAMP((size_t)mnuNotifyEvent->value(), 0,
1344 			 sizeof(default_alert_text)/sizeof(*default_alert_text) - 1);
1345 	string s = default_alert_text[i];
1346 	if (s.empty()) { // custom search; count and list refs
1347 		size_t nsub = min(fre_t(inpNotifyRE->value(), REG_EXTENDED | REG_ICASE).nsub(), (size_t)9);
1348 		if (nsub) {
1349 			s.assign(_("Available substrings")).append(":\n\\0\n");
1350 			char ref[] = "\\1";
1351 			for (size_t i = 1; i < nsub; i++, ref[1]++)
1352 				s.append(ref).append("\n");
1353 		}
1354 	}
1355 	inpNotifyActionDialog->value(s.c_str());
1356 }
1357 
1358 // the insert default RX text button callback
notify_rx_default_cb(Fl_Widget * w,void * arg)1359 static void notify_rx_default_cb(Fl_Widget* w, void* arg)
1360 {
1361 	if (mnuNotifyEvent->value() == NOTIFY_EVENT_RSID)
1362 		inpNotifyActionRXMarker->value("\nRSID: $RX_MARKER\n");
1363 	else
1364 		inpNotifyActionRXMarker->value("\n$RX_MARKER\n");
1365 }
1366 
1367 // the ignore duplicates check button callback
notify_dup_ignore_cb(Fl_Widget * w,void * arg)1368 void notify_dup_ignore_cb(Fl_Widget* w, void* arg)
1369 {
1370 	if (static_cast<Fl_Check_Button*>(w)->value()) {
1371 		mnuNotifyDupWhich->show();
1372 		cntNotifyDupTime->show();
1373 		chkNotifyDupBand->show();
1374 		chkNotifyDupMode->show();
1375 	}
1376 	else {
1377 		mnuNotifyDupWhich->hide();
1378 		cntNotifyDupTime->hide();
1379 		chkNotifyDupBand->hide();
1380 		chkNotifyDupMode->hide();
1381 	}
1382 	w->parent()->redraw();
1383 }
1384 
1385 // the custom re field callback
notify_re_cb(Fl_Widget * w,void * arg)1386 static void notify_re_cb(Fl_Widget* w, void* arg)
1387 {
1388 	notify_set_event_dup_menu(static_cast<Fl_Input*>(w)->value());
1389 }
1390 
1391 ////////////////////////////////////////////////////////////////////////////////
1392 // dup widget handling
1393 ////////////////////////////////////////////////////////////////////////////////
1394 
1395 // set the dup menu substrings for regular epxression string re
notify_set_event_dup_menu(const char * re)1396 static void notify_set_event_dup_menu(const char* re)
1397 {
1398 	int v = mnuNotifyDupWhich->value();
1399 	size_t nref = fre_t(re, REG_EXTENDED).nsub();
1400 	for (size_t i = 0; i < sizeof(notify_dup_refs_menu)/sizeof(*notify_dup_refs_menu); i++)
1401 		notify_dup_refs_menu[i].hide();
1402 	if (nref) {
1403 		size_t i;
1404 		for (i = 1; i < nref; i++)
1405 			notify_dup_refs_menu[i].show();
1406 		if ((size_t)v == nref)
1407 			v = mnuNotifyDupWhich->size() - 1;
1408 		else
1409 			v = 1;
1410 	}
1411 	else
1412 		v = 0;
1413 	mnuNotifyDupWhich->value(v);
1414 	mnuNotifyDupWhich->redraw();
1415 }
1416 
1417 // set the dup group widgets for event n
notify_set_event_dup(const notify_t & n)1418 static void notify_set_event_dup(const notify_t& n)
1419 {
1420 	size_t i;
1421 	for (i = 0; i < sizeof(notify_dup_refs_menu)/sizeof(*notify_dup_refs_menu); i++)
1422 		notify_dup_refs_menu[i].hide();
1423 
1424 	switch (n.event) {
1425 	case NOTIFY_EVENT_MYCALL: case NOTIFY_EVENT_STATION: case NOTIFY_EVENT_RSID:
1426 		i = event_regex[n.event].index;
1427 		mnuNotifyDupWhich->menu(notify_dup_callsign_menu);
1428 		for (size_t j = 0; j < sizeof(notify_dup_callsign_menu)/sizeof(*notify_dup_callsign_menu) - 1; j++)
1429 			notify_dup_callsign_menu[j].hide();
1430 		notify_dup_callsign_menu[i].show();
1431 		if (n.event == NOTIFY_EVENT_RSID)
1432 			notify_dup_callsign_menu[i].label(_("Frequency"));
1433 		else
1434 			notify_dup_callsign_menu[i].label(_("Callsign"));
1435 		mnuNotifyDupWhich->value(i);
1436 		break;
1437 	case NOTIFY_EVENT_CUSTOM:
1438 		mnuNotifyDupWhich->menu(notify_dup_refs_menu);
1439 		notify_set_event_dup_menu(notify_get_re(n).c_str());
1440 		break;
1441 	}
1442 }
1443 
1444 ////////////////////////////////////////////////////////////////////////////////
1445 // DXCC list
1446 ////////////////////////////////////////////////////////////////////////////////
1447 
1448 // set the toggle status of row i to cond
notify_dxcc_row_check(int i,bool cond=true)1449 static void notify_dxcc_row_check(int i, bool cond = true)
1450 {
1451 	*tblNotifyFilterDXCC->valueAt(i, NOTIFY_DXCC_COL_SEL) = "["[!cond];
1452 }
1453 
1454 // toggle the checked status of row i, return new status
notify_dxcc_row_toggle(int i)1455 static bool notify_dxcc_row_toggle(int i)
1456 {
1457 	char* p = tblNotifyFilterDXCC->valueAt(i, NOTIFY_DXCC_COL_SEL);
1458 	return (*p = "["[!!*p]);
1459 }
1460 
1461 // return checked status of row i
notify_dxcc_row_checked(int i)1462 static bool notify_dxcc_row_checked(int i)
1463 {
1464 	return *tblNotifyFilterDXCC->valueAt(i, NOTIFY_DXCC_COL_SEL);
1465 }
1466 
1467 // the dxcc list select/deselect callback
notify_filter_dxcc_select_cb(Fl_Widget * w,void * arg)1468 static void notify_filter_dxcc_select_cb(Fl_Widget* w, void* arg)
1469 {
1470 
1471 	bool val;
1472 	const char* str;
1473 	int row = tblNotifyFilterDXCC->value();
1474 	int col;
1475 
1476 	intptr_t iarg = (intptr_t)arg;
1477 	switch (iarg) {
1478 	case NOTIFY_DXCC_SELECT_CONT: case NOTIFY_DXCC_DESELECT_CONT:
1479 		str = tblNotifyFilterDXCC->valueAt(row, col = NOTIFY_DXCC_COL_CT);
1480 		val = (iarg == NOTIFY_DXCC_SELECT_CONT);
1481 		break;
1482 	case NOTIFY_DXCC_SELECT_ITU: case NOTIFY_DXCC_DESELECT_ITU:
1483 		str = tblNotifyFilterDXCC->valueAt(row, col = NOTIFY_DXCC_COL_ITU);
1484 		val = (iarg == NOTIFY_DXCC_SELECT_ITU);
1485 		break;
1486 	case NOTIFY_DXCC_SELECT_CQ: case NOTIFY_DXCC_DESELECT_CQ:
1487 		str = tblNotifyFilterDXCC->valueAt(row, col = NOTIFY_DXCC_COL_CQ);
1488 		val = (iarg == NOTIFY_DXCC_SELECT_CQ);
1489 		break;
1490 
1491 	case NOTIFY_DXCC_SELECT_ALL:
1492 		for (int i = 0; i < tblNotifyFilterDXCC->rows(); i++)
1493 			notify_dxcc_row_check(i);
1494 		goto redraw;
1495 	case NOTIFY_DXCC_DESELECT_ALL:
1496 		for (int i = 0; i < tblNotifyFilterDXCC->rows(); i++)
1497 			notify_dxcc_row_check(i, false);
1498 		goto redraw;
1499 
1500 	default:
1501 		return;
1502 	}
1503 
1504 	for (int i = 0; i < tblNotifyFilterDXCC->rows(); i++) {
1505 		if (!strcmp(tblNotifyFilterDXCC->valueAt(i, col), str))
1506 			notify_dxcc_row_check(i, val);
1507 	}
1508 
1509 redraw:
1510 	tblNotifyFilterDXCC->redraw();
1511 }
1512 
1513 // the dxcc search field callback
notify_filter_dxcc_search(Fl_Widget * w,void * arg)1514 static void notify_filter_dxcc_search(Fl_Widget* w, void* arg)
1515 {
1516 	if (w == inpNotifyDXCCSearchCallsign) {
1517 		const dxcc* e = dxcc_lookup(inpNotifyDXCCSearchCallsign->value());
1518 		if (e) {
1519 			inpNotifyDXCCSearchCountry->value(e->country);
1520 			inpNotifyDXCCSearchCountry->do_callback();
1521 			inpNotifyDXCCSearchCountry->position(0);
1522 		}
1523 		return;
1524 	}
1525 
1526 	if (unlikely(!inpNotifyDXCCSearchCountry->size()))
1527 		return;
1528 	int col = 1, row = tblNotifyFilterDXCC->value() + 1;
1529 	row = WCLAMP(row, 0, tblNotifyFilterDXCC->rows() - 1);
1530 	if (tblNotifyFilterDXCC->search(row, col, false, inpNotifyDXCCSearchCountry->value())) {
1531 		int when = tblNotifyFilterDXCC->when();
1532 		tblNotifyFilterDXCC->when(FL_WHEN_NEVER);
1533 		tblNotifyFilterDXCC->GotoRow(row);
1534 		tblNotifyFilterDXCC->when(when);
1535 		inpNotifyDXCCSearchCountry->textcolor(FL_FOREGROUND_COLOR);
1536 	}
1537 	else
1538 		inpNotifyDXCCSearchCountry->textcolor(FL_RED);
1539 	inpNotifyDXCCSearchCountry->redraw();
1540 }
1541 
1542 // the dxcc filter selection button callback
notify_dxcc_browse_cb(Fl_Widget * w,void * arg)1543 static void notify_dxcc_browse_cb(Fl_Widget* w, void* arg)
1544 {
1545 	int v = tblNotifyList->value();
1546 	if (v < 0) // no selection, uncheck all rows
1547 		btnNotifyDXCCDeselect->do_callback();
1548 	else {
1549 		const notify_t& n = *advli(notify_list.begin(), v);
1550 		for (int i = 0; i < tblNotifyFilterDXCC->rows(); i++)
1551 			notify_dxcc_row_check(i, n.filter.dxcc.find(tblNotifyFilterDXCC->valueAt(i, NOTIFY_DXCC_COL_CN))
1552 					      != n.filter.dxcc.end());
1553 	}
1554 	notify_dxcc_show(false);
1555 }
1556 
1557 // the dxcc table callback
notify_dxcc_check_cb(Fl_Widget * w,void * arg)1558 static void notify_dxcc_check_cb(Fl_Widget* w, void* arg)
1559 {
1560 	if (Fl::event_button() != FL_LEFT_MOUSE || (Fl::event() == FL_KEYDOWN && !Fl::event_shift()))
1561 		return;
1562 	int sel = tblNotifyFilterDXCC->value();
1563 	const char* country = tblNotifyFilterDXCC->valueAt(sel, NOTIFY_DXCC_COL_CN);
1564 	if (notify_dxcc_row_toggle(sel))
1565 		notify_tmp.filter.dxcc[country] = true;
1566 	else
1567 		notify_tmp.filter.dxcc.erase(country);
1568 }
1569 
1570 ////////////////////////////////////////////////////////////////////////////////
1571 // storage functions
1572 ////////////////////////////////////////////////////////////////////////////////
1573 
1574 // save the event list
notify_save(void)1575 static void notify_save(void)
1576 {
1577 	notify_set_qsodb_cache();
1578 
1579 	remove(string(HomeDir).append("/").append("notify.prefs").c_str());
1580 	Fl_Preferences ndata(HomeDir.c_str(), PACKAGE_TARNAME, "notify");
1581 	ndata.set("items", static_cast<int>(notify_list.size()));
1582 
1583 	size_t num = 0;
1584 	stringstream group;
1585 	for (notify_list_t::iterator i = notify_list.begin(); i != notify_list.end(); ++i) {
1586 		group << "item" << num++;
1587 
1588 		ndata.set(Fl_Preferences::Name("%s/event", group.str().c_str()), i->event);
1589 		if (i->event >= NOTIFY_EVENT_CUSTOM)
1590 			ndata.set(Fl_Preferences::Name("%s/re", group.str().c_str()), i->re.c_str());
1591 		ndata.set(Fl_Preferences::Name("%s/enabled", group.str().c_str()), i->enabled);
1592 
1593 		ndata.set(Fl_Preferences::Name("%s/action/alert", group.str().c_str()), i->action.alert.c_str());
1594 		ndata.set(Fl_Preferences::Name("%s/action/rx_marker", group.str().c_str()), i->action.rx_marker.c_str());
1595 		ndata.set(Fl_Preferences::Name("%s/action/macro", group.str().c_str()), i->action.macro.c_str());
1596 		ndata.set(Fl_Preferences::Name("%s/action/program", group.str().c_str()), i->action.program.c_str());
1597 		ndata.set(Fl_Preferences::Name("%s/action/trigger_limit", group.str().c_str()),
1598 			  static_cast<int>(i->action.trigger_limit));
1599 		ndata.set(Fl_Preferences::Name("%s/action/alert_timeout", group.str().c_str()),
1600 			  static_cast<int>(i->action.alert_timeout));
1601 
1602 		ndata.set(Fl_Preferences::Name("%s/filter/match", group.str().c_str()), i->filter.match);
1603 		ndata.set(Fl_Preferences::Name("%s/filter/callsign", group.str().c_str()), i->filter.callsign.c_str());
1604 		ndata.set(Fl_Preferences::Name("%s/filter/nwb", group.str().c_str()), i->filter.nwb);
1605 		ndata.set(Fl_Preferences::Name("%s/filter/lotw", group.str().c_str()), i->filter.lotw);
1606 		ndata.set(Fl_Preferences::Name("%s/filter/eqsl", group.str().c_str()), i->filter.eqsl);
1607 		int k = 0;
1608 		for (notify_filter_dxcc_t::const_iterator j = i->filter.dxcc.begin();
1609 		     j != i->filter.dxcc.end() && j->second; ++j) {
1610 			ndata.set(Fl_Preferences::Name("%s/filter/dxcc/%d", group.str().c_str(), k++), j->first.c_str());
1611 		}
1612 
1613 		ndata.set(Fl_Preferences::Name("%s/dup/ignore", group.str().c_str()), i->dup_ignore);
1614 		ndata.set(Fl_Preferences::Name("%s/dup/ref", group.str().c_str()), static_cast<int>(i->dup_ref));
1615 		ndata.set(Fl_Preferences::Name("%s/dup/when", group.str().c_str()), static_cast<int>(i->dup.when));
1616 		ndata.set(Fl_Preferences::Name("%s/dup/band", group.str().c_str()), i->dup.band);
1617 		ndata.set(Fl_Preferences::Name("%s/dup/mode", group.str().c_str()), static_cast<int>(i->dup.mode));
1618 	}
1619 }
1620 
1621 // load the event list
notify_load(void)1622 static void notify_load(void)
1623 {
1624 	int x;
1625 	char s[512];
1626 	s[sizeof(s)-1] = '\0';
1627 
1628 	Fl_Preferences ndata(HomeDir.c_str(), PACKAGE_TARNAME, "notify");
1629 	if (!ndata.get("items", x, 0) || x <= 0)
1630 		return;
1631 	size_t n = static_cast<size_t>(x);
1632 
1633 	stringstream group;
1634 	for (size_t i = 0; i < n; i++) {
1635 		notify_t nitem;
1636 		group << "item" << i;
1637 
1638 		ndata.get(Fl_Preferences::Name("%s/event", group.str().c_str()), x, 0);
1639 		nitem.event = static_cast<notify_event_t>(x);
1640 		nitem.last_trigger = 0;
1641 		if (nitem.event >= NOTIFY_EVENT_CUSTOM) {
1642 			ndata.get(Fl_Preferences::Name("%s/re", group.str().c_str()), s, "", sizeof(s)-1);
1643 			nitem.re = s;
1644 		}
1645 		else
1646 			nitem.re = event_regex[nitem.event].regex;
1647 		ndata.get(Fl_Preferences::Name("%s/enabled", group.str().c_str()), x, 1);
1648 		nitem.enabled = x;
1649 
1650 		ndata.get(Fl_Preferences::Name("%s/action/alert", group.str().c_str()), s, "", sizeof(s)-1);
1651 		nitem.action.alert = s;
1652 		ndata.get(Fl_Preferences::Name("%s/action/macro", group.str().c_str()), s, "", sizeof(s)-1);
1653 		nitem.action.macro = s;
1654 		ndata.get(Fl_Preferences::Name("%s/action/rx_marker", group.str().c_str()), s, "", sizeof(s)-1);
1655 		nitem.action.rx_marker = s;
1656 		ndata.get(Fl_Preferences::Name("%s/action/program", group.str().c_str()), s, "", sizeof(s)-1);
1657 		nitem.action.program = s;
1658 		ndata.get(Fl_Preferences::Name("%s/action/trigger_limit", group.str().c_str()), x, 0);
1659 		nitem.action.trigger_limit = x;
1660 		ndata.get(Fl_Preferences::Name("%s/action/alert_timeout", group.str().c_str()), x, 5);
1661 		nitem.action.alert_timeout = x;
1662 
1663 		ndata.get(Fl_Preferences::Name("%s/filter/match", group.str().c_str()), x, 0);
1664 		nitem.filter.match = static_cast<notify_filter_match_t>(x);
1665 		ndata.get(Fl_Preferences::Name("%s/filter/callsign", group.str().c_str()), s, "", sizeof(s)-1);
1666 		nitem.filter.callsign = s;
1667 		ndata.get(Fl_Preferences::Name("%s/filter/nwb", group.str().c_str()), x, 0);
1668 		nitem.filter.nwb = x;
1669 		ndata.get(Fl_Preferences::Name("%s/filter/lotw", group.str().c_str()), x, 0);
1670 		nitem.filter.lotw = x;
1671 		ndata.get(Fl_Preferences::Name("%s/filter/eqsl", group.str().c_str()), x, 0);
1672 		nitem.filter.eqsl = x;
1673 		for (int k = 0; ndata.get(Fl_Preferences::Name("%s/filter/dxcc/%d", group.str().c_str(), k), s, "", sizeof(s)-1); k++)
1674 			nitem.filter.dxcc[s] = true;
1675 
1676 		ndata.get(Fl_Preferences::Name("%s/dup/ignore", group.str().c_str()), x, 0);
1677 		nitem.dup_ignore = x;
1678 		ndata.get(Fl_Preferences::Name("%s/dup/ref", group.str().c_str()), x, 0);
1679 		nitem.dup_ref = x;
1680 		ndata.get(Fl_Preferences::Name("%s/dup/when", group.str().c_str()), x, 600);
1681 		nitem.dup.when = x;
1682 		ndata.get(Fl_Preferences::Name("%s/dup/band", group.str().c_str()), x, 0);
1683 		nitem.dup.band = static_cast<band_t>(x);
1684 		ndata.get(Fl_Preferences::Name("%s/dup/mode", group.str().c_str()), x, 0);
1685 		nitem.dup.mode = static_cast<trx_mode>(x);
1686 
1687 		notify_list.push_back(nitem);
1688 	}
1689 
1690 	notify_table_reload();
1691 }
1692