Keep maps of wxTreeListItem to Cinema/Screen.
[dcpomatic.git] / src / wx / screens_panel.cc
1 /*
2     Copyright (C) 2015-2022 Carl Hetherington <cth@carlh.net>
3
4     This file is part of DCP-o-matic.
5
6     DCP-o-matic 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
9     (at your option) any later version.
10
11     DCP-o-matic is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU General Public License for more details.
15
16     You should have received a copy of the GNU General Public License
17     along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
18
19 */
20
21
22 #include "cinema_dialog.h"
23 #include "dcpomatic_button.h"
24 #include "screen_dialog.h"
25 #include "screens_panel.h"
26 #include "wx_util.h"
27 #include "lib/cinema.h"
28 #include "lib/config.h"
29 #include "lib/screen.h"
30 #include <unicode/putil.h>
31 #include <unicode/ucol.h>
32 #include <unicode/uiter.h>
33 #include <unicode/utypes.h>
34 #include <unicode/ustring.h>
35
36
37 using std::cout;
38 using std::make_pair;
39 using std::make_shared;
40 using std::map;
41 using std::pair;
42 using std::shared_ptr;
43 using std::string;
44 using std::vector;
45 using boost::optional;
46 using namespace dcpomatic;
47
48
49 ScreensPanel::ScreensPanel (wxWindow* parent)
50         : wxPanel (parent, wxID_ANY)
51         , _ignore_selection_change (false)
52 {
53         auto sizer = new wxBoxSizer (wxVERTICAL);
54
55         _search = new wxSearchCtrl (this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(200, search_ctrl_height()));
56 #ifndef __WXGTK3__
57         /* The cancel button seems to be strangely broken in GTK3; clicking on it twice sometimes works */
58         _search->ShowCancelButton (true);
59 #endif
60         sizer->Add (_search, 0, wxBOTTOM, DCPOMATIC_SIZER_GAP);
61
62         auto targets = new wxBoxSizer (wxHORIZONTAL);
63         _targets = new wxTreeListCtrl (this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTL_MULTIPLE | wxTL_3STATE | wxTL_NO_HEADER);
64         _targets->AppendColumn (wxT("foo"));
65         _targets->SetSortColumn (0);
66         _targets->SetItemComparator (&_comparator);
67
68         targets->Add (_targets, 1, wxEXPAND | wxRIGHT, DCPOMATIC_SIZER_GAP);
69
70         add_cinemas ();
71
72         auto target_buttons = new wxBoxSizer (wxVERTICAL);
73
74         _add_cinema = new Button (this, _("Add Cinema..."));
75         target_buttons->Add (_add_cinema, 1, wxEXPAND | wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
76         _edit_cinema = new Button (this, _("Edit Cinema..."));
77         target_buttons->Add (_edit_cinema, 1, wxEXPAND | wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
78         _remove_cinema = new Button (this, _("Remove Cinema"));
79         target_buttons->Add (_remove_cinema, 1, wxEXPAND | wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
80         _add_screen = new Button (this, _("Add Screen..."));
81         target_buttons->Add (_add_screen, 1, wxEXPAND | wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
82         _edit_screen = new Button (this, _("Edit Screen..."));
83         target_buttons->Add (_edit_screen, 1, wxEXPAND | wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
84         _remove_screen = new Button (this, _("Remove Screen"));
85         target_buttons->Add (_remove_screen, 1, wxEXPAND | wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
86
87         targets->Add (target_buttons, 0, 0);
88
89         sizer->Add (targets, 1, wxEXPAND);
90
91         _search->Bind        (wxEVT_TEXT, boost::bind (&ScreensPanel::search_changed, this));
92         _targets->Bind       (wxEVT_TREELIST_SELECTION_CHANGED, &ScreensPanel::selection_changed_shim, this);
93         _targets->Bind       (wxEVT_TREELIST_ITEM_CHECKED, &ScreensPanel::checkbox_changed, this);
94
95         _add_cinema->Bind    (wxEVT_BUTTON, boost::bind (&ScreensPanel::add_cinema_clicked, this));
96         _edit_cinema->Bind   (wxEVT_BUTTON, boost::bind (&ScreensPanel::edit_cinema_clicked, this));
97         _remove_cinema->Bind (wxEVT_BUTTON, boost::bind (&ScreensPanel::remove_cinema_clicked, this));
98
99         _add_screen->Bind    (wxEVT_BUTTON, boost::bind (&ScreensPanel::add_screen_clicked, this));
100         _edit_screen->Bind   (wxEVT_BUTTON, boost::bind (&ScreensPanel::edit_screen_clicked, this));
101         _remove_screen->Bind (wxEVT_BUTTON, boost::bind (&ScreensPanel::remove_screen_clicked, this));
102
103         SetSizer (sizer);
104 }
105
106
107 ScreensPanel::~ScreensPanel ()
108 {
109         _targets->Unbind (wxEVT_TREELIST_SELECTION_CHANGED, &ScreensPanel::selection_changed_shim, this);
110         _targets->Unbind (wxEVT_TREELIST_ITEM_CHECKED, &ScreensPanel::checkbox_changed, this);
111 }
112
113
114 void
115 ScreensPanel::setup_sensitivity ()
116 {
117         bool const sc = _selected_cinemas.size() == 1;
118         bool const ss = _selected_screens.size() == 1;
119
120         _edit_cinema->Enable (sc || ss);
121         _remove_cinema->Enable (_selected_cinemas.size() >= 1);
122
123         _add_screen->Enable (sc || ss);
124         _edit_screen->Enable (ss);
125         _remove_screen->Enable (_selected_screens.size() >= 1);
126 }
127
128
129 optional<wxTreeListItem>
130 ScreensPanel::add_cinema (shared_ptr<Cinema> cinema)
131 {
132         auto search = wx_to_std (_search->GetValue ());
133         transform (search.begin(), search.end(), search.begin(), ::tolower);
134
135         if (!search.empty ()) {
136                 auto name = cinema->name;
137                 transform (name.begin(), name.end(), name.begin(), ::tolower);
138                 if (name.find (search) == string::npos) {
139                         return {};
140                 }
141         }
142
143         auto id = _targets->AppendItem(_targets->GetRootItem(), std_to_wx(cinema->name));
144
145         _cinemas.push_back(make_pair(id, cinema));
146         _item_to_cinema[id] = cinema;
147
148         for (auto screen: cinema->screens()) {
149                 add_screen (cinema, screen);
150         }
151
152         return id;
153 }
154
155
156 optional<wxTreeListItem>
157 ScreensPanel::add_screen (shared_ptr<Cinema> cinema, shared_ptr<Screen> screen)
158 {
159         auto cinema_iter = _cinemas.begin();
160         while (cinema_iter != _cinemas.end() && cinema_iter->second != cinema) {
161                 ++cinema_iter;
162         }
163
164         if (cinema_iter == _cinemas.end()) {
165                 return {};
166         }
167
168         auto id = _targets->AppendItem(cinema_iter->first, std_to_wx(screen->name));
169
170         _screens.push_back(make_pair(id, screen));
171         _item_to_screen[id] = screen;
172
173         return cinema_iter->first;
174 }
175
176
177 void
178 ScreensPanel::add_cinema_clicked ()
179 {
180         auto d = new CinemaDialog (GetParent(), _("Add Cinema"));
181         if (d->ShowModal () == wxID_OK) {
182                 auto cinema = make_shared<Cinema>(d->name(), d->emails(), d->notes(), d->utc_offset_hour(), d->utc_offset_minute());
183                 Config::instance()->add_cinema (cinema);
184                 auto id = add_cinema (cinema);
185                 if (id) {
186                         _targets->UnselectAll ();
187                         _targets->Select (*id);
188                 }
189         }
190
191         d->Destroy ();
192 }
193
194
195 optional<pair<wxTreeListItem, shared_ptr<Cinema>>>
196 ScreensPanel::cinema_for_operation () const
197 {
198         if (_selected_cinemas.size() == 1) {
199                 return make_pair(_selected_cinemas.begin()->first, _selected_cinemas.begin()->second);
200         } else if (_selected_screens.size() == 1) {
201                 return make_pair(_targets->GetItemParent(_selected_screens.begin()->first), _selected_screens.begin()->second->cinema);
202         }
203
204         return {};
205 }
206
207
208 void
209 ScreensPanel::edit_cinema_clicked ()
210 {
211         auto cinema = cinema_for_operation ();
212         if (!cinema) {
213                 return;
214         }
215
216         auto d = new CinemaDialog (
217                 GetParent(), _("Edit cinema"), cinema->second->name, cinema->second->emails, cinema->second->notes, cinema->second->utc_offset_hour(), cinema->second->utc_offset_minute()
218                 );
219
220         if (d->ShowModal() == wxID_OK) {
221                 cinema->second->name = d->name ();
222                 cinema->second->emails = d->emails ();
223                 cinema->second->notes = d->notes ();
224                 cinema->second->set_utc_offset_hour (d->utc_offset_hour ());
225                 cinema->second->set_utc_offset_minute (d->utc_offset_minute ());
226                 _targets->SetItemText (cinema->first, std_to_wx(d->name()));
227                 Config::instance()->changed (Config::CINEMAS);
228         }
229
230         d->Destroy ();
231 }
232
233
234 void
235 ScreensPanel::remove_cinema_clicked ()
236 {
237         if (_selected_cinemas.size() == 1) {
238                 if (!confirm_dialog(this, wxString::Format(_("Are you sure you want to remove the cinema '%s'?"), std_to_wx(_selected_cinemas.begin()->second->name)))) {
239                         return;
240                 }
241         } else {
242                 if (!confirm_dialog(this, wxString::Format(_("Are you sure you want to remove %d cinemas?"), int(_selected_cinemas.size())))) {
243                         return;
244                 }
245         }
246
247         for (auto const& i: _selected_cinemas) {
248                 Config::instance()->remove_cinema (i.second);
249                 _targets->DeleteItem (i.first);
250         }
251
252         selection_changed ();
253 }
254
255
256 void
257 ScreensPanel::add_screen_clicked ()
258 {
259         auto cinema = cinema_for_operation ();
260         if (!cinema) {
261                 return;
262         }
263
264         auto d = new ScreenDialog (GetParent(), _("Add Screen"));
265         if (d->ShowModal () != wxID_OK) {
266                 d->Destroy ();
267                 return;
268         }
269
270         for (auto screen: cinema->second->screens()) {
271                 if (screen->name == d->name()) {
272                         error_dialog (
273                                 GetParent(),
274                                 wxString::Format (
275                                         _("You cannot add a screen called '%s' as the cinema already has a screen with this name."),
276                                         std_to_wx(d->name()).data()
277                                         )
278                                 );
279                         return;
280                 }
281         }
282
283         auto screen = std::make_shared<Screen>(d->name(), d->notes(), d->recipient(), d->recipient_file(), d->trusted_devices());
284         cinema->second->add_screen (screen);
285         auto id = add_screen (cinema->second, screen);
286         if (id) {
287                 _targets->Expand (id.get ());
288         }
289
290         Config::instance()->changed (Config::CINEMAS);
291
292         d->Destroy ();
293 }
294
295
296 void
297 ScreensPanel::edit_screen_clicked ()
298 {
299         if (_selected_screens.size() != 1) {
300                 return;
301         }
302
303         auto edit_screen = *_selected_screens.begin();
304
305         auto d = new ScreenDialog (
306                 GetParent(), _("Edit screen"),
307                 edit_screen.second->name,
308                 edit_screen.second->notes,
309                 edit_screen.second->recipient,
310                 edit_screen.second->recipient_file,
311                 edit_screen.second->trusted_devices
312                 );
313
314         if (d->ShowModal() != wxID_OK) {
315                 d->Destroy ();
316                 return;
317         }
318
319         auto cinema = edit_screen.second->cinema;
320         for (auto screen: cinema->screens()) {
321                 if (screen != edit_screen.second && screen->name == d->name()) {
322                         error_dialog (
323                                 GetParent(),
324                                 wxString::Format (
325                                         _("You cannot change this screen's name to '%s' as the cinema already has a screen with this name."),
326                                         std_to_wx(d->name()).data()
327                                         )
328                                 );
329                         return;
330                 }
331         }
332
333         edit_screen.second->name = d->name ();
334         edit_screen.second->notes = d->notes ();
335         edit_screen.second->recipient = d->recipient ();
336         edit_screen.second->recipient_file = d->recipient_file ();
337         edit_screen.second->trusted_devices = d->trusted_devices ();
338         _targets->SetItemText (edit_screen.first, std_to_wx(d->name()));
339         Config::instance()->changed (Config::CINEMAS);
340
341         d->Destroy ();
342 }
343
344
345 void
346 ScreensPanel::remove_screen_clicked ()
347 {
348         if (_selected_screens.size() == 1) {
349                 if (!confirm_dialog(this, wxString::Format(_("Are you sure you want to remove the screen '%s'?"), std_to_wx(_selected_screens.begin()->second->name)))) {
350                         return;
351                 }
352         } else {
353                 if (!confirm_dialog(this, wxString::Format(_("Are you sure you want to remove %d screens?"), int(_selected_screens.size())))) {
354                         return;
355                 }
356         }
357
358         for (auto const& i: _selected_screens) {
359                 auto j = _cinemas.begin ();
360                 while (j != _cinemas.end ()) {
361                         auto sc = j->second->screens ();
362                         if (find (sc.begin(), sc.end(), i.second) != sc.end ()) {
363                                 break;
364                         }
365
366                         ++j;
367                 }
368
369                 if (j == _cinemas.end()) {
370                         continue;
371                 }
372
373                 j->second->remove_screen (i.second);
374                 _targets->DeleteItem (i.first);
375         }
376
377         Config::instance()->changed (Config::CINEMAS);
378 }
379
380
381 vector<shared_ptr<Screen>>
382 ScreensPanel::screens () const
383 {
384         vector<shared_ptr<Screen>> output;
385
386         for (auto item = _targets->GetFirstItem(); item.IsOk(); item = _targets->GetNextItem(item)) {
387                 if (_targets->GetCheckedState(item) == wxCHK_CHECKED) {
388                         if (auto screen = item_to_screen(item)) {
389                                 output.push_back (screen);
390                         }
391                 }
392         }
393
394         return output;
395 }
396
397
398 void
399 ScreensPanel::selection_changed_shim (wxTreeListEvent &)
400 {
401         selection_changed ();
402 }
403
404
405 void
406 ScreensPanel::selection_changed ()
407 {
408         if (_ignore_selection_change) {
409                 return;
410         }
411
412         wxTreeListItems selection;
413         _targets->GetSelections (selection);
414
415         _selected_cinemas.clear ();
416         _selected_screens.clear ();
417
418         for (size_t i = 0; i < selection.size(); ++i) {
419                 if (auto cinema = item_to_cinema(selection[i])) {
420                         _selected_cinemas.push_back(make_pair(selection[i], cinema));
421                 }
422                 if (auto screen = item_to_screen(selection[i])) {
423                         _selected_screens.push_back(make_pair(selection[i], screen));
424                 }
425         }
426
427         setup_sensitivity ();
428 }
429
430
431 void
432 ScreensPanel::add_cinemas ()
433 {
434         for (auto cinema: Config::instance()->cinemas()) {
435                 add_cinema (cinema);
436         }
437 }
438
439
440 void
441 ScreensPanel::search_changed ()
442 {
443         _targets->DeleteAllItems ();
444         _cinemas.clear ();
445         _screens.clear ();
446
447         _item_to_cinema.clear ();
448         _item_to_screen.clear ();
449         add_cinemas ();
450
451         _ignore_selection_change = true;
452
453         for (auto const& selection: _selected_cinemas) {
454                 /* The wxTreeListItems will now be different, so we must search by cinema */
455                 auto cinema = _cinemas.begin ();
456                 while (cinema != _cinemas.end() && cinema->second != selection.second) {
457                         ++cinema;
458                 }
459
460                 if (cinema != _cinemas.end()) {
461                         _targets->Select (cinema->first);
462                 }
463         }
464
465         for (auto const& selection: _selected_screens) {
466                 auto screen = _screens.begin ();
467                 while (screen != _screens.end() && screen->second != selection.second) {
468                         ++screen;
469                 }
470
471                 if (screen != _screens.end()) {
472                         _targets->Select (screen->first);
473                 }
474         }
475
476         _ignore_selection_change = false;
477 }
478
479
480 void
481 ScreensPanel::checkbox_changed (wxTreeListEvent& ev)
482 {
483         if (item_to_cinema(ev.GetItem())) {
484                 /* Cinema: check/uncheck all children */
485                 auto const checked = _targets->GetCheckedState(ev.GetItem());
486                 for (auto child = _targets->GetFirstChild(ev.GetItem()); child.IsOk(); child = _targets->GetNextSibling(child)) {
487                         _targets->CheckItem(child, checked);
488                 }
489         } else {
490                 /* Screen: set cinema to checked/unchecked/3state */
491                 auto parent = _targets->GetItemParent(ev.GetItem());
492                 DCPOMATIC_ASSERT (parent.IsOk());
493                 int checked = 0;
494                 int unchecked = 0;
495                 for (auto child = _targets->GetFirstChild(parent); child.IsOk(); child = _targets->GetNextSibling(child)) {
496                         if (_targets->GetCheckedState(child) == wxCHK_CHECKED) {
497                             ++checked;
498                         } else {
499                             ++unchecked;
500                         }
501                 }
502                 if (checked == 0) {
503                         _targets->CheckItem(parent, wxCHK_UNCHECKED);
504                 } else if (unchecked == 0) {
505                         _targets->CheckItem(parent, wxCHK_CHECKED);
506                 } else {
507                         _targets->CheckItem(parent, wxCHK_UNDETERMINED);
508                 }
509         }
510
511         ScreensChanged ();
512 }
513
514
515 shared_ptr<Cinema>
516 ScreensPanel::item_to_cinema (wxTreeListItem item) const
517 {
518         auto iter = _item_to_cinema.find (item);
519         if (iter == _item_to_cinema.end()) {
520                 return {};
521         }
522
523         return iter->second;
524 }
525
526
527 shared_ptr<Screen>
528 ScreensPanel::item_to_screen (wxTreeListItem item) const
529 {
530         auto iter = _item_to_screen.find (item);
531         if (iter == _item_to_screen.end()) {
532                 return {};
533         }
534
535         return iter->second;
536 }
537
538
539 ScreensPanel::Comparator::Comparator ()
540 {
541         UErrorCode status = U_ZERO_ERROR;
542         _collator = ucol_open(nullptr, &status);
543         if (_collator) {
544                 ucol_setAttribute(_collator, UCOL_NORMALIZATION_MODE, UCOL_ON, &status);
545                 ucol_setAttribute(_collator, UCOL_STRENGTH, UCOL_PRIMARY, &status);
546                 ucol_setAttribute(_collator, UCOL_ALTERNATE_HANDLING, UCOL_SHIFTED, &status);
547         }
548 }
549
550 ScreensPanel::Comparator::~Comparator ()
551 {
552         if (_collator) {
553                 ucol_close (_collator);
554         }
555 }
556
557 int
558 ScreensPanel::Comparator::Compare (wxTreeListCtrl* tree_list, unsigned, wxTreeListItem a, wxTreeListItem b)
559 {
560         auto utf8_a = wx_to_std(tree_list->GetItemText(a));
561         auto utf8_b = wx_to_std(tree_list->GetItemText(b));
562         if (_collator) {
563                 UErrorCode error = U_ZERO_ERROR;
564                 boost::scoped_array<uint16_t> utf16_a(new uint16_t[utf8_a.size() + 1]);
565                 u_strFromUTF8(reinterpret_cast<UChar*>(utf16_a.get()), utf8_a.size() + 1, nullptr, utf8_a.c_str(), -1, &error);
566                 boost::scoped_array<uint16_t> utf16_b(new uint16_t[utf8_b.size() + 1]);
567                 u_strFromUTF8(reinterpret_cast<UChar*>(utf16_b.get()), utf8_b.size() + 1, nullptr, utf8_b.c_str(), -1, &error);
568                 return ucol_strcoll(_collator, reinterpret_cast<UChar*>(utf16_a.get()), -1, reinterpret_cast<UChar*>(utf16_b.get()), -1);
569         } else {
570                 return strcoll(utf8_a.c_str(), utf8_b.c_str());
571         }
572 }