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