Cleanup: extract sorted_cinemas().
[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/scope_guard.h"
30 #include "lib/screen.h"
31 #include "lib/timer.h"
32
33
34 using std::cout;
35 using std::list;
36 using std::make_pair;
37 using std::make_shared;
38 using std::map;
39 using std::pair;
40 using std::shared_ptr;
41 using std::string;
42 using std::vector;
43 using boost::optional;
44 #if BOOST_VERSION >= 106100
45 using namespace boost::placeholders;
46 #endif
47 using namespace dcpomatic;
48
49
50 ScreensPanel::ScreensPanel (wxWindow* parent)
51         : wxPanel (parent, wxID_ANY)
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
66         targets->Add (_targets, 1, wxEXPAND | wxRIGHT, DCPOMATIC_SIZER_GAP);
67
68         add_cinemas ();
69
70         auto side_buttons = new wxBoxSizer (wxVERTICAL);
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         side_buttons->Add (target_buttons, 0, 0);
88
89         auto check_buttons = new wxBoxSizer (wxVERTICAL);
90
91         _check_all = new Button (this, _("Check all"));
92         check_buttons->Add (_check_all, 1, wxEXPAND | wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
93         _uncheck_all = new Button (this, _("Uncheck all"));
94         check_buttons->Add (_uncheck_all, 1, wxEXPAND | wxBOTTOM, DCPOMATIC_BUTTON_STACK_GAP);
95
96         side_buttons->Add (check_buttons, 1, wxEXPAND | wxTOP, DCPOMATIC_BUTTON_STACK_GAP * 8);
97
98         targets->Add (side_buttons, 0, 0);
99
100         sizer->Add (targets, 1, wxEXPAND);
101
102         _search->Bind        (wxEVT_TEXT, boost::bind (&ScreensPanel::search_changed, this));
103         _targets->Bind       (wxEVT_TREELIST_SELECTION_CHANGED, &ScreensPanel::selection_changed_shim, this);
104         _targets->Bind       (wxEVT_TREELIST_ITEM_CHECKED, &ScreensPanel::checkbox_changed, this);
105
106         _add_cinema->Bind    (wxEVT_BUTTON, boost::bind (&ScreensPanel::add_cinema_clicked, this));
107         _edit_cinema->Bind   (wxEVT_BUTTON, boost::bind (&ScreensPanel::edit_cinema_clicked, this));
108         _remove_cinema->Bind (wxEVT_BUTTON, boost::bind (&ScreensPanel::remove_cinema_clicked, this));
109
110         _add_screen->Bind    (wxEVT_BUTTON, boost::bind (&ScreensPanel::add_screen_clicked, this));
111         _edit_screen->Bind   (wxEVT_BUTTON, boost::bind (&ScreensPanel::edit_screen_clicked, this));
112         _remove_screen->Bind (wxEVT_BUTTON, boost::bind (&ScreensPanel::remove_screen_clicked, this));
113
114         _check_all->Bind     (wxEVT_BUTTON, boost::bind(&ScreensPanel::check_all, this));
115         _uncheck_all->Bind   (wxEVT_BUTTON, boost::bind(&ScreensPanel::uncheck_all, this));
116
117         SetSizer (sizer);
118
119         _config_connection = Config::instance()->Changed.connect(boost::bind(&ScreensPanel::config_changed, this, _1));
120 }
121
122
123 ScreensPanel::~ScreensPanel ()
124 {
125         _targets->Unbind (wxEVT_TREELIST_SELECTION_CHANGED, &ScreensPanel::selection_changed_shim, this);
126         _targets->Unbind (wxEVT_TREELIST_ITEM_CHECKED, &ScreensPanel::checkbox_changed, this);
127 }
128
129
130 void
131 ScreensPanel::check_all ()
132 {
133         for (auto cinema = _targets->GetFirstChild(_targets->GetRootItem()); cinema.IsOk();  cinema = _targets->GetNextSibling(cinema)) {
134                 _targets->CheckItem(cinema, wxCHK_CHECKED);
135                 for (auto screen = _targets->GetFirstChild(cinema); screen.IsOk(); screen = _targets->GetNextSibling(screen)) {
136                         _targets->CheckItem(screen, wxCHK_CHECKED);
137                         set_screen_checked(screen, true);
138                 }
139         }
140 }
141
142
143 void
144 ScreensPanel::uncheck_all ()
145 {
146         for (auto cinema = _targets->GetFirstChild(_targets->GetRootItem()); cinema.IsOk();  cinema = _targets->GetNextSibling(cinema)) {
147                 _targets->CheckItem(cinema, wxCHK_UNCHECKED);
148                 for (auto screen = _targets->GetFirstChild(cinema); screen.IsOk(); screen = _targets->GetNextSibling(screen)) {
149                         _targets->CheckItem(screen, wxCHK_UNCHECKED);
150                         set_screen_checked(screen, false);
151                 }
152         }
153 }
154
155
156 void
157 ScreensPanel::setup_sensitivity ()
158 {
159         bool const sc = _selected_cinemas.size() == 1;
160         bool const ss = _selected_screens.size() == 1;
161
162         _edit_cinema->Enable (sc || ss);
163         _remove_cinema->Enable (_selected_cinemas.size() >= 1);
164
165         _add_screen->Enable (sc || ss);
166         _edit_screen->Enable (ss);
167         _remove_screen->Enable (_selected_screens.size() >= 1);
168 }
169
170
171 void
172 ScreensPanel::convert_to_lower(string& s)
173 {
174         transform(s.begin(), s.end(), s.begin(), ::tolower);
175 }
176
177
178 bool
179 ScreensPanel::matches_search(shared_ptr<const Cinema> cinema, string lower_case_search)
180 {
181         if (lower_case_search.empty()) {
182                 return true;
183         }
184
185         auto name = cinema->name;
186         convert_to_lower(name);
187         return name.find(lower_case_search) != string::npos;
188 }
189
190
191 optional<wxTreeListItem>
192 ScreensPanel::add_cinema (shared_ptr<Cinema> cinema, wxTreeListItem previous)
193 {
194         auto search = wx_to_std (_search->GetValue ());
195         convert_to_lower(search);
196         if (!matches_search(cinema, search)) {
197                 return {};
198         }
199
200         auto id = _targets->InsertItem(_targets->GetRootItem(), previous, std_to_wx(cinema->name));
201
202         _item_to_cinema[id] = cinema;
203         _cinema_to_item[cinema] = id;
204
205         for (auto screen: cinema->screens()) {
206                 add_screen (cinema, screen);
207         }
208
209         return id;
210 }
211
212
213 optional<wxTreeListItem>
214 ScreensPanel::add_screen (shared_ptr<Cinema> cinema, shared_ptr<Screen> screen)
215 {
216         auto item = cinema_to_item(cinema);
217         if (!item) {
218                 return {};
219         }
220
221         auto id = _targets->AppendItem(*item, std_to_wx(screen->name));
222
223         _item_to_screen[id] = screen;
224         _screen_to_item[screen] = id;
225
226         return item;
227 }
228
229
230 void
231 ScreensPanel::add_cinema_clicked ()
232 {
233         auto dialog = new CinemaDialog (GetParent(), _("Add Cinema"));
234         ScopeGuard sg = [dialog]() { dialog->Destroy(); };
235
236         if (dialog->ShowModal() == wxID_OK) {
237                 auto cinema = make_shared<Cinema>(dialog->name(), dialog->emails(), dialog->notes(), dialog->utc_offset_hour(), dialog->utc_offset_minute());
238
239                 auto cinemas = sorted_cinemas();
240
241                 try {
242                         _ignore_cinemas_changed = true;
243                         ScopeGuard sg = [this]() { _ignore_cinemas_changed = false; };
244                         Config::instance()->add_cinema(cinema);
245                 } catch (FileError& e) {
246                         error_dialog(GetParent(), _("Could not write cinema details to the cinemas.xml file.  Check that the location of cinemas.xml is valid in DCP-o-matic's preferences."), std_to_wx(e.what()));
247                         return;
248                 }
249
250                 wxTreeListItem previous = wxTLI_FIRST;
251                 bool found = false;
252                 for (auto existing_cinema: cinemas) {
253                         if (_collator.compare(dialog->name(), existing_cinema->name) < 0) {
254                                 /* existing_cinema should be after the one we're inserting */
255                                 found = true;
256                                 break;
257                         }
258                         auto item = cinema_to_item(existing_cinema);
259                         DCPOMATIC_ASSERT(item);
260                         previous = *item;
261                 }
262
263                 auto item = add_cinema(cinema, found ? previous : wxTLI_LAST);
264
265                 if (item) {
266                         _targets->UnselectAll ();
267                         _targets->Select (*item);
268                 }
269         }
270
271         selection_changed ();
272 }
273
274
275 shared_ptr<Cinema>
276 ScreensPanel::cinema_for_operation () const
277 {
278         if (_selected_cinemas.size() == 1) {
279                 return _selected_cinemas[0];
280         } else if (_selected_screens.size() == 1) {
281                 return _selected_screens[0]->cinema;
282         }
283
284         return {};
285 }
286
287
288 void
289 ScreensPanel::edit_cinema_clicked ()
290 {
291         auto cinema = cinema_for_operation ();
292         if (!cinema) {
293                 return;
294         }
295
296         auto dialog = new CinemaDialog(
297                 GetParent(), _("Edit cinema"), cinema->name, cinema->emails, cinema->notes, cinema->utc_offset_hour(), cinema->utc_offset_minute()
298                 );
299         ScopeGuard sg = [dialog]() { dialog->Destroy(); };
300
301         if (dialog->ShowModal() == wxID_OK) {
302                 cinema->name = dialog->name();
303                 cinema->emails = dialog->emails();
304                 cinema->notes = dialog->notes();
305                 cinema->set_utc_offset_hour(dialog->utc_offset_hour());
306                 cinema->set_utc_offset_minute(dialog->utc_offset_minute());
307                 notify_cinemas_changed();
308                 auto item = cinema_to_item(cinema);
309                 DCPOMATIC_ASSERT(item);
310                 _targets->SetItemText (*item, std_to_wx(dialog->name()));
311         }
312 }
313
314
315 void
316 ScreensPanel::remove_cinema_clicked ()
317 {
318         if (_selected_cinemas.size() == 1) {
319                 if (!confirm_dialog(this, wxString::Format(_("Are you sure you want to remove the cinema '%s'?"), std_to_wx(_selected_cinemas[0]->name)))) {
320                         return;
321                 }
322         } else {
323                 if (!confirm_dialog(this, wxString::Format(_("Are you sure you want to remove %d cinemas?"), int(_selected_cinemas.size())))) {
324                         return;
325                 }
326         }
327
328         for (auto const& cinema: _selected_cinemas) {
329                 _ignore_cinemas_changed = true;
330                 ScopeGuard sg = [this]() { _ignore_cinemas_changed = false; };
331                 Config::instance()->remove_cinema(cinema);
332                 auto item = cinema_to_item(cinema);
333                 DCPOMATIC_ASSERT(item);
334                 _targets->DeleteItem(*item);
335         }
336
337         selection_changed ();
338 }
339
340
341 void
342 ScreensPanel::add_screen_clicked ()
343 {
344         auto cinema = cinema_for_operation ();
345         if (!cinema) {
346                 return;
347         }
348
349         auto dialog = new ScreenDialog(GetParent(), _("Add Screen"));
350         ScopeGuard sg = [dialog]() { dialog->Destroy(); };
351
352         if (dialog->ShowModal () != wxID_OK) {
353                 return;
354         }
355
356         for (auto screen: cinema->screens()) {
357                 if (screen->name == dialog->name()) {
358                         error_dialog (
359                                 GetParent(),
360                                 wxString::Format (
361                                         _("You cannot add a screen called '%s' as the cinema already has a screen with this name."),
362                                         std_to_wx(dialog->name()).data()
363                                         )
364                                 );
365                         return;
366                 }
367         }
368
369         auto screen = std::make_shared<Screen>(dialog->name(), dialog->notes(), dialog->recipient(), dialog->recipient_file(), dialog->trusted_devices());
370         cinema->add_screen (screen);
371         notify_cinemas_changed();
372
373         auto id = add_screen (cinema, screen);
374         if (id) {
375                 _targets->Expand (id.get ());
376         }
377 }
378
379
380 void
381 ScreensPanel::edit_screen_clicked ()
382 {
383         if (_selected_screens.size() != 1) {
384                 return;
385         }
386
387         auto edit_screen = _selected_screens[0];
388
389         auto dialog = new ScreenDialog(
390                 GetParent(), _("Edit screen"),
391                 edit_screen->name,
392                 edit_screen->notes,
393                 edit_screen->recipient,
394                 edit_screen->recipient_file,
395                 edit_screen->trusted_devices
396                 );
397         ScopeGuard sg = [dialog]() { dialog->Destroy(); };
398
399         if (dialog->ShowModal() != wxID_OK) {
400                 return;
401         }
402
403         auto cinema = edit_screen->cinema;
404         for (auto screen: cinema->screens()) {
405                 if (screen != edit_screen && screen->name == dialog->name()) {
406                         error_dialog (
407                                 GetParent(),
408                                 wxString::Format (
409                                         _("You cannot change this screen's name to '%s' as the cinema already has a screen with this name."),
410                                         std_to_wx(dialog->name()).data()
411                                         )
412                                 );
413                         return;
414                 }
415         }
416
417         edit_screen->name = dialog->name();
418         edit_screen->notes = dialog->notes();
419         edit_screen->recipient = dialog->recipient();
420         edit_screen->recipient_file = dialog->recipient_file();
421         edit_screen->trusted_devices = dialog->trusted_devices();
422         notify_cinemas_changed();
423
424         auto item = screen_to_item(edit_screen);
425         DCPOMATIC_ASSERT (item);
426         _targets->SetItemText (*item, std_to_wx(dialog->name()));
427 }
428
429
430 void
431 ScreensPanel::remove_screen_clicked ()
432 {
433         if (_selected_screens.size() == 1) {
434                 if (!confirm_dialog(this, wxString::Format(_("Are you sure you want to remove the screen '%s'?"), std_to_wx(_selected_screens[0]->name)))) {
435                         return;
436                 }
437         } else {
438                 if (!confirm_dialog(this, wxString::Format(_("Are you sure you want to remove %d screens?"), int(_selected_screens.size())))) {
439                         return;
440                 }
441         }
442
443         for (auto const& screen: _selected_screens) {
444                 screen->cinema->remove_screen(screen);
445                 auto item = screen_to_item(screen);
446                 DCPOMATIC_ASSERT(item);
447                 _targets->DeleteItem(*item);
448         }
449
450         notify_cinemas_changed();
451 }
452
453
454 vector<shared_ptr<Screen>>
455 ScreensPanel::screens () const
456 {
457         vector<shared_ptr<Screen>> output;
458         std::copy (_checked_screens.begin(), _checked_screens.end(), std::back_inserter(output));
459         return output;
460 }
461
462
463 void
464 ScreensPanel::selection_changed_shim (wxTreeListEvent &)
465 {
466         selection_changed ();
467 }
468
469
470 void
471 ScreensPanel::selection_changed ()
472 {
473         if (_ignore_selection_change) {
474                 return;
475         }
476
477         wxTreeListItems selection;
478         _targets->GetSelections (selection);
479
480         _selected_cinemas.clear ();
481         _selected_screens.clear ();
482
483         for (size_t i = 0; i < selection.size(); ++i) {
484                 if (auto cinema = item_to_cinema(selection[i])) {
485                         _selected_cinemas.push_back(cinema);
486                 }
487                 if (auto screen = item_to_screen(selection[i])) {
488                         _selected_screens.push_back(screen);
489                 }
490         }
491
492         setup_sensitivity ();
493 }
494
495
496 list<shared_ptr<Cinema>>
497 ScreensPanel::sorted_cinemas() const
498 {
499         auto cinemas = Config::instance()->cinemas();
500
501         cinemas.sort(
502                 [this](shared_ptr<Cinema> a, shared_ptr<Cinema> b) { return _collator.compare(a->name, b->name) < 0; }
503                 );
504
505         return cinemas;
506 }
507
508
509 void
510 ScreensPanel::add_cinemas ()
511 {
512         for (auto cinema: sorted_cinemas()) {
513                 add_cinema (cinema, wxTLI_LAST);
514         }
515 }
516
517
518 void
519 ScreensPanel::clear_and_re_add()
520 {
521         _targets->DeleteAllItems ();
522
523         _item_to_cinema.clear ();
524         _cinema_to_item.clear ();
525         _item_to_screen.clear ();
526         _screen_to_item.clear ();
527
528         add_cinemas ();
529 }
530
531
532 void
533 ScreensPanel::search_changed ()
534 {
535         clear_and_re_add();
536
537         _ignore_selection_change = true;
538
539         for (auto const& selection: _selected_cinemas) {
540                 if (auto item = cinema_to_item(selection)) {
541                         _targets->Select (*item);
542                 }
543         }
544
545         for (auto const& selection: _selected_screens) {
546                 if (auto item = screen_to_item(selection)) {
547                         _targets->Select (*item);
548                 }
549         }
550
551         _ignore_selection_change = false;
552
553         _ignore_check_change = true;
554
555         for (auto const& checked: _checked_screens) {
556                 if (auto item = screen_to_item(checked)) {
557                         _targets->CheckItem(*item, wxCHK_CHECKED);
558                         setup_cinema_checked_state(*item);
559                 }
560         }
561
562         _ignore_check_change = false;
563 }
564
565
566 void
567 ScreensPanel::set_screen_checked (wxTreeListItem item, bool checked)
568 {
569         auto screen = item_to_screen(item);
570         DCPOMATIC_ASSERT(screen);
571         if (checked) {
572                 _checked_screens.insert(screen);
573         } else {
574                 _checked_screens.erase(screen);
575         }
576 }
577
578
579 void
580 ScreensPanel::setup_cinema_checked_state (wxTreeListItem screen)
581 {
582         auto cinema = _targets->GetItemParent(screen);
583         DCPOMATIC_ASSERT (cinema.IsOk());
584         int checked = 0;
585         int unchecked = 0;
586         for (auto child = _targets->GetFirstChild(cinema); child.IsOk(); child = _targets->GetNextSibling(child)) {
587                 if (_targets->GetCheckedState(child) == wxCHK_CHECKED) {
588                     ++checked;
589                 } else {
590                     ++unchecked;
591                 }
592         }
593         if (checked == 0) {
594                 _targets->CheckItem(cinema, wxCHK_UNCHECKED);
595         } else if (unchecked == 0) {
596                 _targets->CheckItem(cinema, wxCHK_CHECKED);
597         } else {
598                 _targets->CheckItem(cinema, wxCHK_UNDETERMINED);
599         }
600 }
601
602
603 void
604 ScreensPanel::checkbox_changed (wxTreeListEvent& ev)
605 {
606         if (_ignore_check_change) {
607                 return;
608         }
609
610         if (item_to_cinema(ev.GetItem())) {
611                 /* Cinema: check/uncheck all children */
612                 auto const checked = _targets->GetCheckedState(ev.GetItem());
613                 for (auto child = _targets->GetFirstChild(ev.GetItem()); child.IsOk(); child = _targets->GetNextSibling(child)) {
614                         _targets->CheckItem(child, checked);
615                         set_screen_checked(child, checked);
616                 }
617         } else {
618                 set_screen_checked(ev.GetItem(), _targets->GetCheckedState(ev.GetItem()));
619                 setup_cinema_checked_state(ev.GetItem());
620         }
621
622         ScreensChanged ();
623 }
624
625
626 shared_ptr<Cinema>
627 ScreensPanel::item_to_cinema (wxTreeListItem item) const
628 {
629         auto iter = _item_to_cinema.find (item);
630         if (iter == _item_to_cinema.end()) {
631                 return {};
632         }
633
634         return iter->second;
635 }
636
637
638 shared_ptr<Screen>
639 ScreensPanel::item_to_screen (wxTreeListItem item) const
640 {
641         auto iter = _item_to_screen.find (item);
642         if (iter == _item_to_screen.end()) {
643                 return {};
644         }
645
646         return iter->second;
647 }
648
649
650 optional<wxTreeListItem>
651 ScreensPanel::cinema_to_item (shared_ptr<Cinema> cinema) const
652 {
653         auto iter = _cinema_to_item.find (cinema);
654         if (iter == _cinema_to_item.end()) {
655                 return {};
656         }
657
658         return iter->second;
659 }
660
661
662 optional<wxTreeListItem>
663 ScreensPanel::screen_to_item (shared_ptr<Screen> screen) const
664 {
665         auto iter = _screen_to_item.find (screen);
666         if (iter == _screen_to_item.end()) {
667                 return {};
668         }
669
670         return iter->second;
671 }
672
673
674 bool
675 ScreensPanel::notify_cinemas_changed()
676 {
677         _ignore_cinemas_changed = true;
678         ScopeGuard sg = [this]() { _ignore_cinemas_changed = false; };
679
680         try {
681                 Config::instance()->changed(Config::CINEMAS);
682         } catch (FileError& e) {
683                 error_dialog(GetParent(), _("Could not write cinema details to the cinemas.xml file.  Check that the location of cinemas.xml is valid in DCP-o-matic's preferences."), std_to_wx(e.what()));
684                 return false;
685         }
686
687         return true;
688 }
689
690
691 void
692 ScreensPanel::config_changed(Config::Property property)
693 {
694         if (property == Config::Property::CINEMAS && !_ignore_cinemas_changed) {
695                 clear_and_re_add();
696         }
697 }