Use the ICU library to sort cinemas rather than strcoll() (part of #2208).
[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
147         for (auto screen: cinema->screens()) {
148                 add_screen (cinema, screen);
149         }
150
151         return id;
152 }
153
154
155 optional<wxTreeListItem>
156 ScreensPanel::add_screen (shared_ptr<Cinema> cinema, shared_ptr<Screen> screen)
157 {
158         auto cinema_iter = _cinemas.begin();
159         while (cinema_iter != _cinemas.end() && cinema_iter->second != cinema) {
160                 ++cinema_iter;
161         }
162
163         if (cinema_iter == _cinemas.end()) {
164                 return {};
165         }
166
167         _screens.push_back(make_pair(_targets->AppendItem(cinema_iter->first, std_to_wx(screen->name)), screen));
168         return cinema_iter->first;
169 }
170
171
172 void
173 ScreensPanel::add_cinema_clicked ()
174 {
175         auto d = new CinemaDialog (GetParent(), _("Add Cinema"));
176         if (d->ShowModal () == wxID_OK) {
177                 auto cinema = make_shared<Cinema>(d->name(), d->emails(), d->notes(), d->utc_offset_hour(), d->utc_offset_minute());
178                 Config::instance()->add_cinema (cinema);
179                 auto id = add_cinema (cinema);
180                 if (id) {
181                         _targets->UnselectAll ();
182                         _targets->Select (*id);
183                 }
184         }
185
186         d->Destroy ();
187 }
188
189
190 optional<pair<wxTreeListItem, shared_ptr<Cinema>>>
191 ScreensPanel::cinema_for_operation () const
192 {
193         if (_selected_cinemas.size() == 1) {
194                 return make_pair(_selected_cinemas.begin()->first, _selected_cinemas.begin()->second);
195         } else if (_selected_screens.size() == 1) {
196                 return make_pair(_targets->GetItemParent(_selected_screens.begin()->first), _selected_screens.begin()->second->cinema);
197         }
198
199         return {};
200 }
201
202
203 void
204 ScreensPanel::edit_cinema_clicked ()
205 {
206         auto cinema = cinema_for_operation ();
207         if (!cinema) {
208                 return;
209         }
210
211         auto d = new CinemaDialog (
212                 GetParent(), _("Edit cinema"), cinema->second->name, cinema->second->emails, cinema->second->notes, cinema->second->utc_offset_hour(), cinema->second->utc_offset_minute()
213                 );
214
215         if (d->ShowModal() == wxID_OK) {
216                 cinema->second->name = d->name ();
217                 cinema->second->emails = d->emails ();
218                 cinema->second->notes = d->notes ();
219                 cinema->second->set_utc_offset_hour (d->utc_offset_hour ());
220                 cinema->second->set_utc_offset_minute (d->utc_offset_minute ());
221                 _targets->SetItemText (cinema->first, std_to_wx(d->name()));
222                 Config::instance()->changed (Config::CINEMAS);
223         }
224
225         d->Destroy ();
226 }
227
228
229 void
230 ScreensPanel::remove_cinema_clicked ()
231 {
232         if (_selected_cinemas.size() == 1) {
233                 if (!confirm_dialog(this, wxString::Format(_("Are you sure you want to remove the cinema '%s'?"), std_to_wx(_selected_cinemas.begin()->second->name)))) {
234                         return;
235                 }
236         } else {
237                 if (!confirm_dialog(this, wxString::Format(_("Are you sure you want to remove %d cinemas?"), int(_selected_cinemas.size())))) {
238                         return;
239                 }
240         }
241
242         for (auto const& i: _selected_cinemas) {
243                 Config::instance()->remove_cinema (i.second);
244                 _targets->DeleteItem (i.first);
245         }
246
247         selection_changed ();
248 }
249
250
251 void
252 ScreensPanel::add_screen_clicked ()
253 {
254         auto cinema = cinema_for_operation ();
255         if (!cinema) {
256                 return;
257         }
258
259         auto d = new ScreenDialog (GetParent(), _("Add Screen"));
260         if (d->ShowModal () != wxID_OK) {
261                 d->Destroy ();
262                 return;
263         }
264
265         for (auto screen: cinema->second->screens()) {
266                 if (screen->name == d->name()) {
267                         error_dialog (
268                                 GetParent(),
269                                 wxString::Format (
270                                         _("You cannot add a screen called '%s' as the cinema already has a screen with this name."),
271                                         std_to_wx(d->name()).data()
272                                         )
273                                 );
274                         return;
275                 }
276         }
277
278         auto screen = std::make_shared<Screen>(d->name(), d->notes(), d->recipient(), d->recipient_file(), d->trusted_devices());
279         cinema->second->add_screen (screen);
280         auto id = add_screen (cinema->second, screen);
281         if (id) {
282                 _targets->Expand (id.get ());
283         }
284
285         Config::instance()->changed (Config::CINEMAS);
286
287         d->Destroy ();
288 }
289
290
291 void
292 ScreensPanel::edit_screen_clicked ()
293 {
294         if (_selected_screens.size() != 1) {
295                 return;
296         }
297
298         auto edit_screen = *_selected_screens.begin();
299
300         auto d = new ScreenDialog (
301                 GetParent(), _("Edit screen"),
302                 edit_screen.second->name,
303                 edit_screen.second->notes,
304                 edit_screen.second->recipient,
305                 edit_screen.second->recipient_file,
306                 edit_screen.second->trusted_devices
307                 );
308
309         if (d->ShowModal() != wxID_OK) {
310                 d->Destroy ();
311                 return;
312         }
313
314         auto cinema = edit_screen.second->cinema;
315         for (auto screen: cinema->screens()) {
316                 if (screen != edit_screen.second && screen->name == d->name()) {
317                         error_dialog (
318                                 GetParent(),
319                                 wxString::Format (
320                                         _("You cannot change this screen's name to '%s' as the cinema already has a screen with this name."),
321                                         std_to_wx(d->name()).data()
322                                         )
323                                 );
324                         return;
325                 }
326         }
327
328         edit_screen.second->name = d->name ();
329         edit_screen.second->notes = d->notes ();
330         edit_screen.second->recipient = d->recipient ();
331         edit_screen.second->recipient_file = d->recipient_file ();
332         edit_screen.second->trusted_devices = d->trusted_devices ();
333         _targets->SetItemText (edit_screen.first, std_to_wx(d->name()));
334         Config::instance()->changed (Config::CINEMAS);
335
336         d->Destroy ();
337 }
338
339
340 void
341 ScreensPanel::remove_screen_clicked ()
342 {
343         if (_selected_screens.size() == 1) {
344                 if (!confirm_dialog(this, wxString::Format(_("Are you sure you want to remove the screen '%s'?"), std_to_wx(_selected_screens.begin()->second->name)))) {
345                         return;
346                 }
347         } else {
348                 if (!confirm_dialog(this, wxString::Format(_("Are you sure you want to remove %d screens?"), int(_selected_screens.size())))) {
349                         return;
350                 }
351         }
352
353         for (auto const& i: _selected_screens) {
354                 auto j = _cinemas.begin ();
355                 while (j != _cinemas.end ()) {
356                         auto sc = j->second->screens ();
357                         if (find (sc.begin(), sc.end(), i.second) != sc.end ()) {
358                                 break;
359                         }
360
361                         ++j;
362                 }
363
364                 if (j == _cinemas.end()) {
365                         continue;
366                 }
367
368                 j->second->remove_screen (i.second);
369                 _targets->DeleteItem (i.first);
370         }
371
372         Config::instance()->changed (Config::CINEMAS);
373 }
374
375
376 vector<shared_ptr<Screen>>
377 ScreensPanel::screens () const
378 {
379         vector<shared_ptr<Screen>> output;
380
381         for (auto item = _targets->GetFirstItem(); item.IsOk(); item = _targets->GetNextItem(item)) {
382                 if (_targets->GetCheckedState(item) == wxCHK_CHECKED) {
383                         auto screen_iter = screen_by_tree_list_item(item);
384                         if (screen_iter != _screens.end()) {
385                                 output.push_back (screen_iter->second);
386                         }
387                 }
388         }
389
390         return output;
391 }
392
393
394 void
395 ScreensPanel::selection_changed_shim (wxTreeListEvent &)
396 {
397         selection_changed ();
398 }
399
400
401 void
402 ScreensPanel::selection_changed ()
403 {
404         if (_ignore_selection_change) {
405                 return;
406         }
407
408         wxTreeListItems selection;
409         _targets->GetSelections (selection);
410
411         _selected_cinemas.clear ();
412         _selected_screens.clear ();
413
414         for (size_t i = 0; i < selection.size(); ++i) {
415                 auto cinema = cinema_by_tree_list_item(selection[i]);
416                 if (cinema != _cinemas.end ()) {
417                         _selected_cinemas.push_back(*cinema);
418                 }
419                 auto screen = screen_by_tree_list_item(selection[i]);
420                 if (screen != _screens.end ()) {
421                         _selected_screens.push_back(*screen);
422                 }
423         }
424
425         setup_sensitivity ();
426 }
427
428
429 void
430 ScreensPanel::add_cinemas ()
431 {
432         for (auto cinema: Config::instance()->cinemas()) {
433                 add_cinema (cinema);
434         }
435 }
436
437
438 void
439 ScreensPanel::search_changed ()
440 {
441         _targets->DeleteAllItems ();
442         _cinemas.clear ();
443         _screens.clear ();
444
445         add_cinemas ();
446
447         _ignore_selection_change = true;
448
449         for (auto const& selection: _selected_cinemas) {
450                 /* The wxTreeListItems will now be different, so we must search by cinema */
451                 auto cinema = _cinemas.begin ();
452                 while (cinema != _cinemas.end() && cinema->second != selection.second) {
453                         ++cinema;
454                 }
455
456                 if (cinema != _cinemas.end()) {
457                         _targets->Select (cinema->first);
458                 }
459         }
460
461         for (auto const& selection: _selected_screens) {
462                 auto screen = _screens.begin ();
463                 while (screen != _screens.end() && screen->second != selection.second) {
464                         ++screen;
465                 }
466
467                 if (screen != _screens.end()) {
468                         _targets->Select (screen->first);
469                 }
470         }
471
472         _ignore_selection_change = false;
473 }
474
475
476 void
477 ScreensPanel::checkbox_changed (wxTreeListEvent& ev)
478 {
479         auto cinema_iter = cinema_by_tree_list_item(ev.GetItem());
480         if (cinema_iter != _cinemas.end()) {
481                 /* Cinema: check/uncheck all children */
482                 auto const checked = _targets->GetCheckedState(ev.GetItem());
483                 for (auto child = _targets->GetFirstChild(ev.GetItem()); child.IsOk(); child = _targets->GetNextSibling(child)) {
484                         _targets->CheckItem(child, checked);
485                 }
486         } else {
487                 /* Screen: set cinema to checked/unchecked/3state */
488                 auto parent = _targets->GetItemParent(ev.GetItem());
489                 DCPOMATIC_ASSERT (parent.IsOk());
490                 int checked = 0;
491                 int unchecked = 0;
492                 for (auto child = _targets->GetFirstChild(parent); child.IsOk(); child = _targets->GetNextSibling(child)) {
493                         if (_targets->GetCheckedState(child) == wxCHK_CHECKED) {
494                             ++checked;
495                         } else {
496                             ++unchecked;
497                         }
498                 }
499                 if (checked == 0) {
500                         _targets->CheckItem(parent, wxCHK_UNCHECKED);
501                 } else if (unchecked == 0) {
502                         _targets->CheckItem(parent, wxCHK_CHECKED);
503                 } else {
504                         _targets->CheckItem(parent, wxCHK_UNDETERMINED);
505                 }
506         }
507
508         ScreensChanged ();
509 }
510
511
512 ScreensPanel::Cinemas::iterator
513 ScreensPanel::cinema_by_tree_list_item (wxTreeListItem item)
514 {
515         return std::find_if(
516                 _cinemas.begin(), _cinemas.end(),
517                 [item](pair<wxTreeListItem, shared_ptr<Cinema>> const& s) { return s.first == item; }
518                 );
519 }
520
521
522 ScreensPanel::Screens::const_iterator
523 ScreensPanel::screen_by_tree_list_item (wxTreeListItem item) const
524 {
525         return std::find_if(
526                 _screens.begin(), _screens.end(),
527                 [item](pair<wxTreeListItem, shared_ptr<Screen>> const& s) { return s.first == item; }
528                 );
529 }
530
531
532 ScreensPanel::Comparator::Comparator ()
533 {
534         UErrorCode status = U_ZERO_ERROR;
535         _collator = ucol_open(nullptr, &status);
536         if (_collator) {
537                 ucol_setAttribute(_collator, UCOL_NORMALIZATION_MODE, UCOL_ON, &status);
538                 ucol_setAttribute(_collator, UCOL_STRENGTH, UCOL_PRIMARY, &status);
539                 ucol_setAttribute(_collator, UCOL_ALTERNATE_HANDLING, UCOL_SHIFTED, &status);
540         }
541 }
542
543 ScreensPanel::Comparator::~Comparator ()
544 {
545         if (_collator) {
546                 ucol_close (_collator);
547         }
548 }
549
550 int
551 ScreensPanel::Comparator::Compare (wxTreeListCtrl* tree_list, unsigned, wxTreeListItem a, wxTreeListItem b)
552 {
553         auto utf8_a = wx_to_std(tree_list->GetItemText(a));
554         auto utf8_b = wx_to_std(tree_list->GetItemText(b));
555         if (_collator) {
556                 UErrorCode error = U_ZERO_ERROR;
557                 boost::scoped_array<uint16_t> utf16_a(new uint16_t[utf8_a.size() + 1]);
558                 u_strFromUTF8(reinterpret_cast<UChar*>(utf16_a.get()), utf8_a.size() + 1, nullptr, utf8_a.c_str(), -1, &error);
559                 boost::scoped_array<uint16_t> utf16_b(new uint16_t[utf8_b.size() + 1]);
560                 u_strFromUTF8(reinterpret_cast<UChar*>(utf16_b.get()), utf8_b.size() + 1, nullptr, utf8_b.c_str(), -1, &error);
561                 return ucol_strcoll(_collator, reinterpret_cast<UChar*>(utf16_a.get()), -1, reinterpret_cast<UChar*>(utf16_b.get()), -1);
562         } else {
563                 return strcoll(utf8_a.c_str(), utf8_b.c_str());
564         }
565 }