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