Work around strange build error on Ubuntu 18.04
[dcpomatic.git] / src / wx / full_language_tag_dialog.cc
1 /*
2     Copyright (C) 2020-2021 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 "full_language_tag_dialog.h"
23 #include "lib/dcpomatic_assert.h"
24 #include <dcp/language_tag.h>
25 #include <dcp/warnings.h>
26 LIBDCP_DISABLE_WARNINGS
27 #include <wx/listctrl.h>
28 #include <wx/srchctrl.h>
29 #include <wx/wx.h>
30 LIBDCP_ENABLE_WARNINGS
31 #include <boost/algorithm/string.hpp>
32 #include <boost/bind/bind.hpp>
33 #include <boost/optional.hpp>
34 #include <boost/signals2.hpp>
35 #include <iterator>
36 #include <string>
37 #include <vector>
38
39
40 using std::min;
41 using std::pair;
42 using std::shared_ptr;
43 using std::string;
44 using std::vector;
45 using std::weak_ptr;
46 using boost::optional;
47 #if BOOST_VERSION >= 106100
48 using namespace boost::placeholders;
49 #endif
50
51
52 class SubtagListCtrl : public wxListCtrl
53 {
54 public:
55         SubtagListCtrl (wxWindow* parent)
56                 : wxListCtrl (parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLC_REPORT | wxLC_SINGLE_SEL | wxLC_NO_HEADER | wxLC_VIRTUAL)
57         {
58                 AppendColumn ("", wxLIST_FORMAT_LEFT, 80);
59                 AppendColumn ("", wxLIST_FORMAT_LEFT, 400);
60         }
61
62         void set (dcp::LanguageTag::SubtagType type, string search, optional<dcp::LanguageTag::SubtagData> subtag = optional<dcp::LanguageTag::SubtagData>())
63         {
64                 _all_subtags = dcp::LanguageTag::get_all(type);
65                 set_search (search);
66                 if (subtag) {
67                         auto i = find(_matching_subtags.begin(), _matching_subtags.end(), *subtag);
68                         if (i != _matching_subtags.end()) {
69                                 auto item = std::distance(_matching_subtags.begin(), i);
70                                 SetItemState (item, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED);
71                                 EnsureVisible (item);
72                         }
73                 } else {
74                         if (GetItemCount() > 0) {
75                                 /* The new list sometimes isn't visible without this */
76                                 EnsureVisible (0);
77                         }
78                 }
79         }
80
81         void set_search (string search)
82         {
83                 if (search == "") {
84                         _matching_subtags = _all_subtags;
85                 } else {
86                         _matching_subtags.clear ();
87
88                         boost::algorithm::to_lower(search);
89                         for (auto const& i: _all_subtags) {
90                                 if (
91                                         (boost::algorithm::to_lower_copy(i.subtag).find(search) != string::npos) ||
92                                         (boost::algorithm::to_lower_copy(i.description).find(search) != string::npos)) {
93                                         _matching_subtags.push_back (i);
94                                 }
95                         }
96                 }
97
98                 SetItemCount (_matching_subtags.size());
99                 if (GetItemCount() > 0) {
100                         RefreshItems (0, GetItemCount() - 1);
101                 }
102         }
103
104         optional<dcp::LanguageTag::SubtagData> selected_subtag () const
105         {
106                 auto selected = GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
107                 if (selected == -1) {
108                         return {};
109                 }
110
111                 DCPOMATIC_ASSERT (static_cast<size_t>(selected) < _matching_subtags.size());
112                 return _matching_subtags[selected];
113         }
114
115 private:
116         wxString OnGetItemText (long item, long column) const override
117         {
118                 if (column == 0) {
119                         return _matching_subtags[item].subtag;
120                 } else {
121                         return _matching_subtags[item].description;
122                 }
123         }
124
125         std::vector<dcp::LanguageTag::SubtagData> _all_subtags;
126         std::vector<dcp::LanguageTag::SubtagData> _matching_subtags;
127 };
128
129
130 class LanguageSubtagPanel : public wxPanel
131 {
132 public:
133         LanguageSubtagPanel (wxWindow* parent)
134                 : wxPanel (parent, wxID_ANY)
135         {
136 #ifdef __WXGTK3__
137                 int const height = 30;
138 #else
139                 int const height = -1;
140 #endif
141
142                 _search = new wxSearchCtrl (this, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(200, height));
143                 _list = new SubtagListCtrl (this);
144
145                 auto sizer = new wxBoxSizer (wxVERTICAL);
146                 sizer->Add (_search, 0, wxALL, 8);
147                 sizer->Add (_list, 1, wxALL, 8);
148                 SetSizer (sizer);
149
150                 _search->Bind (wxEVT_TEXT, boost::bind(&LanguageSubtagPanel::search_changed, this));
151                 _list->Bind (wxEVT_LIST_ITEM_SELECTED, boost::bind(&LanguageSubtagPanel::selection_changed, this));
152                 _list->Bind (wxEVT_LIST_ITEM_DESELECTED, boost::bind(&LanguageSubtagPanel::selection_changed, this));
153         }
154
155         void set (dcp::LanguageTag::SubtagType type, string search, optional<dcp::LanguageTag::SubtagData> subtag = optional<dcp::LanguageTag::SubtagData>())
156         {
157                 _list->set (type, search, subtag);
158                 _search->SetValue (wxString(search));
159         }
160
161         optional<dcp::LanguageTag::RegionSubtag> get () const
162         {
163                 if (!_list->selected_subtag()) {
164                         return {};
165                 }
166
167                 return dcp::LanguageTag::RegionSubtag(_list->selected_subtag()->subtag);
168         }
169
170         boost::signals2::signal<void (optional<dcp::LanguageTag::SubtagData>)> SelectionChanged;
171         boost::signals2::signal<void (string)> SearchChanged;
172
173 private:
174         void search_changed ()
175         {
176                 auto search = _search->GetValue();
177                 _list->set_search (search.ToStdString());
178                 if (search.Length() > 0 && _list->GetItemCount() > 0) {
179                         _list->EnsureVisible (0);
180                 }
181                 SearchChanged (_search->GetValue().ToStdString());
182         }
183
184         void selection_changed ()
185         {
186                 SelectionChanged (_list->selected_subtag());
187         }
188
189         wxSearchCtrl* _search;
190         SubtagListCtrl* _list;
191 };
192
193
194 FullLanguageTagDialog::FullLanguageTagDialog (wxWindow* parent, dcp::LanguageTag tag)
195         : wxDialog (parent, wxID_ANY, _("Language Tag"), wxDefaultPosition, wxSize(-1, 500))
196 {
197         _current_tag_list = new wxListCtrl (this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLC_REPORT | wxLC_SINGLE_SEL | wxLC_NO_HEADER);
198         _current_tag_list->AppendColumn ("", wxLIST_FORMAT_LEFT, 200);
199         _current_tag_list->AppendColumn ("", wxLIST_FORMAT_LEFT, 400);
200
201         auto button_sizer = new wxBoxSizer (wxVERTICAL);
202         _add_script = new wxButton(this, wxID_ANY, "Add script");
203         button_sizer->Add (_add_script, 0, wxTOP | wxBOTTOM | wxEXPAND, 2);
204         _add_region = new wxButton(this, wxID_ANY, "Add region");
205         button_sizer->Add (_add_region, 0, wxTOP | wxBOTTOM | wxEXPAND, 2);
206         _add_variant = new wxButton(this, wxID_ANY, "Add variant");
207         button_sizer->Add (_add_variant, 0, wxTOP | wxBOTTOM | wxEXPAND, 2);
208         _add_external = new wxButton(this, wxID_ANY, "Add external");
209         button_sizer->Add (_add_external, 0, wxTOP | wxBOTTOM | wxEXPAND, 2);
210         _remove = new wxButton(this, wxID_ANY, "Remove");
211         button_sizer->Add (_remove, 0, wxTOP | wxBOTTOM | wxEXPAND, 2);
212
213         _choose_subtag_panel = new LanguageSubtagPanel (this);
214         _choose_subtag_panel->set (dcp::LanguageTag::SubtagType::LANGUAGE, "");
215
216         auto ltor_sizer = new wxBoxSizer (wxHORIZONTAL);
217         ltor_sizer->Add (_current_tag_list, 1, wxALL, 8);
218         ltor_sizer->Add (button_sizer, 0, wxALL, 8);
219         ltor_sizer->Add (_choose_subtag_panel, 1, wxALL, 8);
220
221         auto overall_sizer = new wxBoxSizer (wxVERTICAL);
222         overall_sizer->Add (ltor_sizer, 0);
223
224         auto buttons = CreateSeparatedButtonSizer (wxOK);
225         if (buttons) {
226                 overall_sizer->Add (buttons, wxSizerFlags().Expand().DoubleBorder());
227         }
228
229         SetSizerAndFit (overall_sizer);
230
231         set (tag);
232
233         _add_script->Bind (wxEVT_BUTTON, boost::bind(&FullLanguageTagDialog::add_to_current_tag, this, dcp::LanguageTag::SubtagType::SCRIPT, boost::optional<dcp::LanguageTag::SubtagData>()));
234         _add_region->Bind (wxEVT_BUTTON, boost::bind(&FullLanguageTagDialog::add_to_current_tag, this, dcp::LanguageTag::SubtagType::REGION, boost::optional<dcp::LanguageTag::SubtagData>()));
235         _add_variant->Bind (wxEVT_BUTTON, boost::bind(&FullLanguageTagDialog::add_to_current_tag, this, dcp::LanguageTag::SubtagType::VARIANT, boost::optional<dcp::LanguageTag::SubtagData>()));
236         _add_external->Bind (wxEVT_BUTTON, boost::bind(&FullLanguageTagDialog::add_to_current_tag, this, dcp::LanguageTag::SubtagType::EXTLANG, boost::optional<dcp::LanguageTag::SubtagData>()));
237         _remove->Bind (wxEVT_BUTTON, boost::bind(&FullLanguageTagDialog::remove_from_current_tag, this));
238         _choose_subtag_panel->SelectionChanged.connect(bind(&FullLanguageTagDialog::chosen_subtag_changed, this, _1));
239         _choose_subtag_panel->SearchChanged.connect(bind(&FullLanguageTagDialog::search_changed, this, _1));
240         _current_tag_list->Bind (wxEVT_LIST_ITEM_SELECTED, boost::bind(&FullLanguageTagDialog::current_tag_selection_changed, this));
241         _current_tag_list->Bind (wxEVT_LIST_ITEM_DESELECTED, boost::bind(&FullLanguageTagDialog::current_tag_selection_changed, this));
242 }
243
244
245 void
246 FullLanguageTagDialog::remove_from_current_tag ()
247 {
248         auto selected = _current_tag_list->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
249         if (selected <= 0) {
250                 return;
251         }
252
253         _current_tag_subtags.erase (_current_tag_subtags.begin() + selected);
254         _current_tag_list->DeleteItem (selected);
255
256         _current_tag_list->SetItemState (min(selected, _current_tag_list->GetItemCount() - 1L), wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED);
257
258         setup_sensitivity ();
259         current_tag_selection_changed ();
260 }
261
262
263 dcp::LanguageTag FullLanguageTagDialog::get () const
264 {
265         dcp::LanguageTag tag;
266
267         vector<dcp::LanguageTag::VariantSubtag> variants;
268         vector<dcp::LanguageTag::ExtlangSubtag> extlangs;
269
270         for (auto i: _current_tag_subtags) {
271                 if (!i.subtag) {
272                         continue;
273                 }
274                 switch (i.type) {
275                         case dcp::LanguageTag::SubtagType::LANGUAGE:
276                                 tag.set_language (i.subtag->subtag);
277                                 break;
278                         case dcp::LanguageTag::SubtagType::SCRIPT:
279                                 tag.set_script (i.subtag->subtag);
280                                 break;
281                         case dcp::LanguageTag::SubtagType::REGION:
282                                 tag.set_region (i.subtag->subtag);
283                                 break;
284                         case dcp::LanguageTag::SubtagType::VARIANT:
285                                 variants.push_back (i.subtag->subtag);
286                                 break;
287                         case dcp::LanguageTag::SubtagType::EXTLANG:
288                                 extlangs.push_back (i.subtag->subtag);
289                                 break;
290                 }
291         }
292
293         tag.set_variants (variants);
294         tag.set_extlangs (extlangs);
295         return tag;
296 }
297
298
299 void
300 FullLanguageTagDialog::set (dcp::LanguageTag tag)
301 {
302         _current_tag_subtags.clear ();
303         _current_tag_list->DeleteAllItems ();
304
305         bool have_language = false;
306         for (auto const& i: tag.subtags()) {
307                 add_to_current_tag (i.first, i.second);
308                 if (i.first == dcp::LanguageTag::SubtagType::LANGUAGE) {
309                         have_language = true;
310                 }
311         }
312
313         if (!have_language) {
314                 add_to_current_tag (dcp::LanguageTag::SubtagType::LANGUAGE, dcp::LanguageTag::SubtagData("en", "English"));
315         }
316 }
317
318
319 string FullLanguageTagDialog::subtag_type_name (dcp::LanguageTag::SubtagType type)
320 {
321         switch (type) {
322                 case dcp::LanguageTag::SubtagType::LANGUAGE:
323                         return "Language";
324                 case dcp::LanguageTag::SubtagType::SCRIPT:
325                         return "Script";
326                 case dcp::LanguageTag::SubtagType::REGION:
327                         return "Region";
328                 case dcp::LanguageTag::SubtagType::VARIANT:
329                         return "Variant";
330                 case dcp::LanguageTag::SubtagType::EXTLANG:
331                         return "External";
332         }
333
334         return {};
335 }
336
337
338 void
339 FullLanguageTagDialog::search_changed (string search)
340 {
341         long int selected = _current_tag_list->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
342         if (selected >= 0) {
343                 _current_tag_subtags[selected].last_search = search;
344         }
345 }
346
347
348 void
349 FullLanguageTagDialog::add_to_current_tag (dcp::LanguageTag::SubtagType type, optional<dcp::LanguageTag::SubtagData> subtag)
350 {
351         _current_tag_subtags.push_back (Subtag(type, subtag));
352         wxListItem it;
353         it.SetId (_current_tag_list->GetItemCount());
354         it.SetColumn (0);
355         it.SetText (subtag_type_name(type));
356         _current_tag_list->InsertItem (it);
357         it.SetColumn (1);
358         if (subtag) {
359                 it.SetText (subtag->description);
360         } else {
361                 it.SetText ("Select...");
362         }
363         _current_tag_list->SetItem (it);
364         _current_tag_list->SetItemState (_current_tag_list->GetItemCount() - 1, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED);
365         _choose_subtag_panel->set (type, "");
366         setup_sensitivity ();
367         current_tag_selection_changed ();
368 }
369
370
371 void
372 FullLanguageTagDialog::current_tag_selection_changed ()
373 {
374         auto selected = _current_tag_list->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
375         if (selected >= 0) {
376                 _choose_subtag_panel->Enable (true);
377                 _choose_subtag_panel->set (_current_tag_subtags[selected].type, _current_tag_subtags[selected].last_search, _current_tag_subtags[selected].subtag);
378         } else {
379                 _choose_subtag_panel->Enable (false);
380         }
381 }
382
383
384 void
385 FullLanguageTagDialog::chosen_subtag_changed (optional<dcp::LanguageTag::SubtagData> selection)
386 {
387         if (!selection) {
388                 return;
389         }
390
391         auto selected = _current_tag_list->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
392         if (selected >= 0) {
393                 _current_tag_subtags[selected].subtag = *selection;
394                 _current_tag_list->SetItem (selected, 0, subtag_type_name(_current_tag_subtags[selected].type));
395                 _current_tag_list->SetItem (selected, 1, selection->description);
396         }
397
398         setup_sensitivity ();
399 }
400
401 void
402 FullLanguageTagDialog::setup_sensitivity ()
403 {
404         _add_script->Enable ();
405         _add_region->Enable ();
406         _add_variant->Enable ();
407         _add_external->Enable ();
408         for (auto const& i: _current_tag_subtags) {
409                 switch (i.type) {
410                         case dcp::LanguageTag::SubtagType::SCRIPT:
411                                 _add_script->Enable (false);
412                                 break;
413                         case dcp::LanguageTag::SubtagType::REGION:
414                                 _add_region->Enable (false);
415                                 break;
416                         case dcp::LanguageTag::SubtagType::VARIANT:
417                                 _add_variant->Enable (false);
418                                 break;
419                         case dcp::LanguageTag::SubtagType::EXTLANG:
420                                 _add_external->Enable (false);
421                                 break;
422                         default:
423                                 break;
424                 }
425         }
426         auto selected = _current_tag_list->GetNextItem (-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED);
427         _remove->Enable (selected > 0);
428 }
429
430
431 RegionSubtagDialog::RegionSubtagDialog (wxWindow* parent, dcp::LanguageTag::RegionSubtag region)
432         : wxDialog (parent, wxID_ANY, _("Region"), wxDefaultPosition, wxSize(-1, 500))
433         , _panel (new LanguageSubtagPanel (this))
434 {
435         auto sizer = new wxBoxSizer (wxVERTICAL);
436         sizer->Add (_panel, 1);
437
438         auto buttons = CreateSeparatedButtonSizer (wxOK);
439         if (buttons) {
440                 sizer->Add (buttons, wxSizerFlags().Expand().DoubleBorder());
441         }
442
443         SetSizer (sizer);
444
445         _panel->set (dcp::LanguageTag::SubtagType::REGION, "", *dcp::LanguageTag::get_subtag_data(region));
446 }
447
448
449 optional<dcp::LanguageTag::RegionSubtag>
450 RegionSubtagDialog::get () const
451 {
452         return _panel->get ();
453 }
454
455