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