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