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