Audacity 3.2.0
TagsEditor.cpp
Go to the documentation of this file.
1/**********************************************************************
2
3 Audacity: A Digital Audio Editor
4
5 TagsEditor.cpp
6
7 Paul Licameli split from Tags.cpp
8
9 **********************************************************************/
10
11#include "TagsEditor.h"
12
13#include "ProjectWindows.h"
14#include "SelectFile.h"
15#include "ShuttleGui.h"
16#include "AudacityMessageBox.h"
17#include "widgets/Grid.h"
18#include "HelpSystem.h"
19#include "XMLFileReader.h"
20#include <wx/combobox.h>
21#include <wx/display.h>
22#include <wx/scrolbar.h>
23#include <wx/button.h>
24#include <wx/bmpbuttn.h>
25#include <wx/stattext.h>
26
27#include "Theme.h"
28#include "AllThemeResources.h"
29
31{
32 TagsEditorDialog dlg(parent, title, { &tags }, {}, true, true);
33 return dlg.ShowModal() == wxID_OK;
34}
35
36//
37// ComboEditor - Wrapper to prevent unwanted background erasure
38//
39
40class ComboEditor final : public wxGridCellChoiceEditor
41{
42public:
43 ComboEditor(const wxArrayString& choices, bool allowOthers = false)
44 : wxGridCellChoiceEditor(choices, allowOthers)
45 , m_choices( choices )
46 , m_allowOthers{ allowOthers }
47 {
48 }
49
50 void PaintBackground(wxDC&, const wxRect& WXUNUSED(rectCell), const wxGridCellAttr & WXUNUSED(attr)) override
51 {
52 // Ignore it (a must on the Mac as the erasure causes problems.)
53 }
54
55 void SetParameters(const wxString& params) override
56 {
57 wxGridCellChoiceEditor::SetParameters(params);
58
59 // Refresh the wxComboBox with NEW values
60 if (Combo()) {
61 Combo()->Clear();
62 Combo()->Append(m_choices);
63 }
64 }
65
66 void SetSize(const wxRect& rectOrig) override
67 {
68 wxRect rect(rectOrig);
69 wxRect r = Combo()->GetRect();
70
71 // Center the combo box in or over the cell
72 rect.y -= (r.GetHeight() - rect.GetHeight()) / 2;
73 rect.height = r.GetHeight();
74
75 wxGridCellChoiceEditor::SetSize(rect);
76 }
77
78 // Fix for Bug 1389
79 // July 2016: ANSWER-ME: Does this need reporting upstream to wxWidgets?
80 virtual void StartingKey(wxKeyEvent& event) override
81 {
82 // Lifted from wxGridCellTextEditor and adapted to combo.
83
84 // [Below is comment from wxWidgets code]
85 // Since this is now happening in the EVT_CHAR event EmulateKeyPress is no
86 // longer an appropriate way to get the character into the text control.
87 // Do it ourselves instead. We know that if we get this far that we have
88 // a valid character, so not a whole lot of testing needs to be done.
89
90 //The only difference to wxGridCellTextEditor.
91 //wxTextCtrl* tc = (wxTextCtrl *)m_control;
92 wxComboBox * tc = Combo();
93 int ch;
94
95 bool isPrintable;
96
97 #if wxUSE_UNICODE
98 ch = event.GetUnicodeKey();
99 if ( ch != WXK_NONE )
100 isPrintable = true;
101 else
102 #endif // wxUSE_UNICODE
103 {
104 ch = event.GetKeyCode();
105 isPrintable = ch >= WXK_SPACE && ch < WXK_START;
106 }
107
108 switch (ch)
109 {
110 case WXK_DELETE:
111 // Delete the initial character when starting to edit with DELETE.
112 tc->Remove(0, 1);
113 break;
114
115 case WXK_BACK:
116 // Delete the last character when starting to edit with BACKSPACE.
117 {
118 const long pos = tc->GetLastPosition();
119 tc->Remove(pos - 1, pos);
120 }
121 break;
122
123 default:
124 if ( isPrintable )
125 tc->WriteText(static_cast<wxChar>(ch));
126 break;
127 }
128 }
129
130 // Clone is required by wxwidgets; implemented via copy constructor
131 wxGridCellEditor *Clone() const override
132 {
134 }
135
136private:
137 wxArrayString m_choices;
139};
140
141//
142// Editor
143//
144
145#define LABEL_ARTIST XO("Artist Name")
146#define LABEL_TITLE XO("Track Title")
147#define LABEL_ALBUM XO("Album Title")
148#define LABEL_TRACK XO("Track Number")
149#define LABEL_YEAR XO("Year")
150#define LABEL_GENRE XO("Genre")
151#define LABEL_COMMENTS XO("Comments")
152
161};
162
163static const struct
164{
166 wxString name;
167}
168labelmap[] =
169{
174 { LABEL_YEAR, TAG_YEAR },
178
179#define STATICCNT WXSIZEOF(labelmap)
180
181enum {
182 ClearID = 10000,
193
194BEGIN_EVENT_TABLE(TagsEditorDialog, wxDialogWrapper)
195 EVT_GRID_CELL_CHANGED(TagsEditorDialog::OnChange)
209 EVT_KEY_DOWN(TagsEditorDialog::OnKeyDown)
211
214 std::vector<Tags*> tags,
215 std::vector<wxString> names,
216 bool editTitle,
217 bool editTrack)
218: wxDialogWrapper(parent, wxID_ANY, title, wxDefaultPosition, wxDefaultSize,
219 wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER),
220 mTags(std::move(tags)),
221 mNames(std::move(names)),
222 mEditTitle(editTitle),
223 mEditTrack(editTrack)
224{
225 assert(mTags.size() == 1 || (mTags.size() > 1 && mTags.size() == mNames.size()));
226
227 SetName();
228
229 mGrid = NULL;
230
231 // Make a local copy of the passed in tags
232 mEditTags.reserve(mTags.size());
233 for(auto ptr : mTags)
234 mEditTags.push_back(std::make_unique<Tags>(*ptr));
235
236 // Build, size, and position the dialog
237 ShuttleGui S(this, eIsCreating);
238 PopulateOrExchange(S);
239
240 TransferDataToWindow();
241
242 Layout();
243 Fit();
244 Center();
245 wxSize sz = GetSize();
246 const auto maxHeight = std::max(1, wxDisplay().GetGeometry().GetHeight() - 100);
247 const auto minHeight = std::min({ sz.y, 600, maxHeight });
248 SetSizeHints(sz.x, minHeight, wxDefaultCoord, maxHeight);
249
250 // Restore the original tags because TransferDataToWindow() will be called again
251 for(unsigned i = 0; i < mEditTags.size(); ++i)
252 {
253 mEditTags[i]->Clear();
254 *mEditTags[i] = *mTags[i];
255 }
256 // Override size and position with last saved
257 wxRect r = GetRect();
258 gPrefs->Read(wxT("/TagsEditorDialog/x"), &r.x, r.x);
259 gPrefs->Read(wxT("/TagsEditorDialog/y"), &r.y, r.y);
260 gPrefs->Read(wxT("/TagsEditorDialog/width"), &r.width, r.width);
261 gPrefs->Read(wxT("/TagsEditorDialog/height"), &r.height, r.height);
262 //On multi-monitor systems, there's a chance the last saved window position is
263 //on a monitor that has been removed or is unavailable.
264 if (IsWindowRectValid(&r))
265 Move(r.GetPosition());
266
267 SetSize(r.GetSize());
268 Layout();
269
270 // Resize value column width based on width of columns and the vertical scrollbar
271 wxScrollBar sb(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxSB_VERTICAL);
272 r = mGrid->GetClientRect();
273 r.width -= mGrid->GetColSize(0);
274 r.width -= sb.GetSize().GetWidth();
275 r.width -= 10;
276 r.width -= r.x;
277 mGrid->SetColSize(1, r.width);
278 //Bug 2038
279 mGrid->SetFocus();
280
281 // Load the genres
282 PopulateGenres();
283}
284
286{
287 // This DELETE is not needed because wxWidgets owns the grid.
288// DELETE mGrid;
289
290// TODO: Need to figure out if these should be deleted. Looks like the wxGrid
291// code takes ownership and uses reference counting, but there's been
292// cases where they show up as memory leaks.
293// PRL: Fixed the leaks, see commit c87eb0804bc5f40659b133cab6e2ade061959645
294// DELETE mStringRenderer;
295// DELETE mComboEditor;
296}
297
299{
300 S.StartVerticalLay();
301 {
302 S.StartHorizontalLay(wxALIGN_LEFT, 0);
303 {
304 S.AddUnits(XO("Use arrow keys (or ENTER key after editing) to navigate fields."));
305 }
306 S.EndHorizontalLay();
307
308 if(mTags.size() > 1)
309 {
310 S.StartHorizontalLay(wxEXPAND, 0);
311 {
312 mPrev = S.Id(PrevID).Style(wxBU_EXACTFIT).AddButton(TranslatableString("<", {}));
313 mName = S.Style(wxALIGN_CENTER).AddVariableText({}, true, wxEXPAND);
314 mNext = S.Id(NextID).Style(wxBU_EXACTFIT).AddButton(TranslatableString(">", {}));
315 }
316 S.EndHorizontalLay();
317 }
318
319 if (mGrid == NULL) {
321 wxID_ANY,
322 wxDefaultPosition,
323 wxDefaultSize
324 );
325
326 mGrid->RegisterDataType(wxT("Combo"),
327 (mStringRenderer = safenew wxGridCellStringRenderer),
328 (mComboEditor = safenew ComboEditor(wxArrayString(), true)));
329
330 mGrid->SetColLabelSize(mGrid->GetDefaultRowSize());
331
332 auto cs = transform_container<wxArrayStringEx>(
333 names, std::mem_fn( &TranslatableString::Translation ) );
334
335 // Build the initial (empty) grid
336 mGrid->CreateGrid(0, 2, wxGrid::wxGridSelectRows);
337 mGrid->SetRowLabelSize(0);
338 mGrid->SetDefaultCellAlignment(wxALIGN_LEFT, wxALIGN_CENTER);
339 mGrid->SetColLabelValue(0, _("Tag"));
340 mGrid->SetColLabelValue(1, _("Value"));
341
342 // Resize the name column and set default row height.
343 wxComboBox tc(this, wxID_ANY, wxT(""), wxDefaultPosition, wxDefaultSize, cs);
344 mGrid->SetColSize(0, tc.GetSize().x);
345 mGrid->SetColMinimalWidth(0, tc.GetSize().x);
346 }
347 S.Prop(1)
348 .Position(wxEXPAND | wxALL)
349 .AddWindow(mGrid);
350
351 S.StartMultiColumn(4, wxALIGN_CENTER);
352 {
353 S.Id(AddID).AddButton(XXO("&Add"));
354 S.Id(RemoveID).AddButton(XXO("&Remove"));
355 S.AddTitle( {} );
356 S.Id(ClearID).AddButton(XXO("Cl&ear"));
357 }
358 S.EndMultiColumn();
359
360 S.StartHorizontalLay(wxALIGN_CENTRE, 0);
361 {
362 S.StartStatic(XO("Genres"));
363 {
364 S.StartMultiColumn(4, wxALIGN_CENTER);
365 {
366 S.Id(EditID).AddButton(XXO("E&dit..."));
367 S.Id(ResetID).AddButton(XXO("Rese&t..."));
368 }
369 S.EndMultiColumn();
370 }
371 S.EndStatic();
372 S.StartStatic(XO("Template"));
373 {
374 S.StartMultiColumn(4, wxALIGN_CENTER);
375 {
376 S.Id(LoadID).AddButton(XXO("&Load..."));
377 S.Id(SaveID).AddButton(XXO("&Save..."));
378 S.AddTitle( {} );
379 S.Id(SaveDefaultsID).AddButton(XXO("Set De&fault"));
380 }
381 S.EndMultiColumn();
382 }
383 S.EndStatic();
384 }
385 S.EndHorizontalLay();
386 }
387 S.EndVerticalLay();
388
389 S.AddStandardButtons(eOkButton | eCancelButton | eHelpButton);
390}
391
392void TagsEditorDialog::OnHelp(wxCommandEvent& WXUNUSED(event))
393{
394 HelpSystem::ShowHelp(this, L"Metadata_Editor", true);
395}
396
398{
399 int i, cnt = mGrid->GetNumberRows();
400
401 if (mGrid->IsCellEditControlShown()) {
402 mGrid->SaveEditControlValue();
403 mGrid->HideCellEditControl();
404 }
405
406 auto& local = *mEditTags[mSelectedIndex];
407
408 local.Clear();
409 for (i = 0; i < cnt; i++) {
410 // Get tag name from the grid
411
412 auto n = mGrid->GetCellValue(i, 0);
413 wxString v = mGrid->GetCellValue(i, 1);
414
415 if (n.empty()) {
416 continue;
417 }
418
419 bool bSpecialTag = true;
420
421 // Map special tag names back to internal keys
422 if (n.CmpNoCase(LABEL_ARTIST.Translation()) == 0) {
423 n = TAG_ARTIST;
424 }
425 else if (n.CmpNoCase(LABEL_TITLE.Translation()) == 0) {
426 n = TAG_TITLE;
427 }
428 else if (n.CmpNoCase(LABEL_ALBUM.Translation()) == 0) {
429 n = TAG_ALBUM;
430 }
431 else if (n.CmpNoCase(LABEL_TRACK.Translation()) == 0) {
432 n = TAG_TRACK;
433 }
434 else if (n.CmpNoCase(LABEL_YEAR.Translation()) == 0) {
435 n = TAG_YEAR;
436 }
437 else if (n.CmpNoCase(LABEL_GENRE.Translation()) == 0) {
438 n = TAG_GENRE;
439 }
440 else if (n.CmpNoCase(LABEL_COMMENTS.Translation()) == 0) {
441 n = TAG_COMMENTS;
442 }
443 else {
444 bSpecialTag = false;
445 }
446
447 local.SetTag(n, v, bSpecialTag);
448 }
449
450 return true;
451}
452
454{
455 size_t i;
456 TagMap popTagMap;
457
458 auto& local = *mEditTags[mSelectedIndex];
459
460 if(mName)
461 {
462 mPrev->Enable(mSelectedIndex > 0);
463 mName->SetLabel(mNames[mSelectedIndex]);
464 mNext->Enable(mSelectedIndex < mNames.size() - 1);
465 }
466
467 // Disable redrawing until we're done
468 mGrid->BeginBatch();
469
470 // Delete all rows
471 if (mGrid->GetNumberRows()) {
472 mGrid->DeleteRows(0, mGrid->GetNumberRows());
473 }
474
475 // Populate the static rows
476 for (i = 0; i < STATICCNT; i++) {
477 mGrid->AppendRows();
478
479 mGrid->SetReadOnly(i, 0);
480 // The special tag name that's displayed and translated may not match
481 // the key string used for internal lookup.
482 mGrid->SetCellValue(i, 0, labelmap[i].label.Translation() );
483 mGrid->SetCellValue(i, 1, local.GetTag(labelmap[i].name));
484
485 if (!mEditTitle &&
486 mGrid->GetCellValue(i, 0).CmpNoCase(LABEL_TITLE.Translation()) == 0) {
487 mGrid->SetReadOnly(i, 1);
488 }
489
490 if (!mEditTrack &&
491 mGrid->GetCellValue(i, 0).CmpNoCase(LABEL_TRACK.Translation()) == 0) {
492 mGrid->SetReadOnly(i, 1);
493 }
494
495 popTagMap[ labelmap[i].name ] = mGrid->GetCellValue(i, 1);
496 }
497
498 // Populate the rest
499 for (const auto &pair : local.GetRange()) {
500 const auto &n = pair.first;
501 const auto &v = pair.second;
502 if (popTagMap.find(n) == popTagMap.end()) {
503 mGrid->AppendRows();
504 mGrid->SetCellValue(i, 0, n);
505 mGrid->SetCellValue(i, 1, v);
506 i++;
507 }
508 }
509
510 // Add an extra one to help with initial sizing and to show it can be done
511 mGrid->AppendRows(1);
512
513 // We're done, so allow the grid to redraw
514 mGrid->EndBatch();
515
516 // Set the editors
517 SetEditors();
518 Layout();
519 Fit();
520
521 return true;
522}
523
524void TagsEditorDialog::OnChange(wxGridEvent & event)
525{
526 static bool ischanging = false;
527
528 // Prevent recursion
529 if (ischanging) {
530 return;
531 }
532
533 event.Skip();
534
535 if (event.GetCol() != 0) {
536 return;
537 }
538
539 // Do not permit duplication of any of the tags.
540 // Tags differing only in case are nondistinct.
541 auto row = event.GetRow();
542 const wxString key0 = mGrid->GetCellValue(row, 0).Upper();
543 auto nn = mGrid->GetNumberRows();
544 for (decltype(nn) ii = 0; ii < nn; ++ii) {
545 if (ii == row)
546 continue;
547 auto key = mGrid->GetCellValue(ii, 0).Upper();
548 if (key0.CmpNoCase(key) == 0) {
549 ischanging = true;
550 wxBell();
551 mGrid->SetGridCursor(ii, 0);
552 event.Veto();
553 ischanging = false;
554 break;
555 }
556 }
557
558 return;
559}
560
561void TagsEditorDialog::OnEdit(wxCommandEvent & WXUNUSED(event))
562{
563 if (mGrid->IsCellEditControlShown()) {
564 mGrid->SaveEditControlValue();
565 mGrid->HideCellEditControl();
566 }
567
568 wxDialogWrapper dlg(this, wxID_ANY, XO("Edit Genres"),
569 wxDefaultPosition, wxDefaultSize,
570 wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER);
571 dlg.SetName();
572 wxTextCtrl *tc;
573
574 ShuttleGui S(&dlg, eIsCreating);
575
576 S.StartVerticalLay(true);
577 {
578 tc = S.AddTextWindow(wxT(""));
579 }
580 S.EndVerticalLay();
581
582 S.AddStandardButtons();
583
584 auto& local = *mEditTags[mSelectedIndex];
585
586 wxArrayString g;
587 int cnt = local.GetNumUserGenres();
588 for (int i = 0; i < cnt; i++) {
589 g.push_back(local.GetUserGenre(i));
590 }
591 std::sort( g.begin(), g.end() );
592
593 for (int i = 0; i < cnt; i++) {
594 tc->AppendText(g[i] + wxT("\n"));
595 }
596
597 dlg.Center();
598 if (dlg.ShowModal() == wxID_CANCEL) {
599 return;
600 }
601
602 wxFileName fn(FileNames::DataDir(), wxT("genres.txt"));
603 wxFile f(fn.GetFullPath(), wxFile::write);
604 if (!f.IsOpened() || !f.Write(tc->GetValue())) {
606 XO("Unable to save genre file."),
607 XO("Reset Genres") );
608 return;
609 }
610
611 local.LoadGenres();
612
614}
615
616void TagsEditorDialog::OnReset(wxCommandEvent & WXUNUSED(event))
617{
618 int id = AudacityMessageBox(
619 XO("Are you sure you want to reset the genre list to defaults?"),
620 XO("Reset Genres"),
621 wxYES_NO);
622
623 if (id == wxNO) {
624 return;
625 }
626 auto& local = *mEditTags[mSelectedIndex];
627
628 local.LoadDefaultGenres();
629
630 wxFileName fn(FileNames::DataDir(), wxT("genres.txt"));
631 wxTextFile tf(fn.GetFullPath());
632
633 bool open = (tf.Exists() && tf.Open()) ||
634 (!tf.Exists() && tf.Create());
635
636 if (!open) {
638 XO("Unable to open genre file."),
639 XO("Reset Genres") );
640 local.LoadGenres();
641 return;
642 }
643
644 tf.Clear();
645 int cnt = local.GetNumUserGenres();
646 for (int i = 0; i < cnt; i++) {
647 tf.AddLine(local.GetUserGenre(i));
648 }
649
650 if (!tf.Write()) {
652 XO("Unable to save genre file."),
653 XO("Reset Genres") );
654 local.LoadGenres();
655 return;
656 }
657
658 local.LoadGenres();
659
661}
662
663void TagsEditorDialog::OnClear(wxCommandEvent & WXUNUSED(event))
664{
665 mEditTags[mSelectedIndex]->Clear();
666
668}
669
670void TagsEditorDialog::OnLoad(wxCommandEvent & WXUNUSED(event))
671{
672 wxString fn;
673
674 auto& local = *mEditTags[mSelectedIndex];
675
676 // Ask the user for the real name
677 fn = SelectFile(FileNames::Operation::_None,
678 XO("Load Metadata As:"),
680 wxT("Tags.xml"),
681 wxT("xml"),
683 wxFD_OPEN | wxRESIZE_BORDER,
684 this);
685
686 // User canceled...
687 if (fn.empty()) {
688 return;
689 }
690
691 // Load the metadata
692 Tags temp;
693 XMLFileReader reader;
694 if (!reader.Parse(&temp, fn)) {
695 // Inform user of load failure
697 reader.GetErrorStr(),
698 XO("Error Loading Metadata"),
699 wxOK | wxCENTRE,
700 this);
701 return;
702 }
703
704 // Remember title and track in case they're read only
705 wxString title = local.GetTag(TAG_TITLE);
706 wxString track = local.GetTag(TAG_TRACK);
707
708 // Replace existing tags with loaded ones
709 local = temp;
710
711 // Restore title
712 if (!mEditTitle) {
713 local.SetTag(TAG_TITLE, title);
714 }
715
716 // Restore track
717 if (!mEditTrack) {
718 local.SetTag(TAG_TRACK, track);
719 }
720
721 // Go fill up the window
723
724 return;
725}
726
727void TagsEditorDialog::OnSave(wxCommandEvent & WXUNUSED(event))
728{
729 wxString fn;
730
731 auto& local = *mEditTags[mSelectedIndex];
732
733 // Refresh tags
735
736 // Ask the user for the real name
737 fn = SelectFile(FileNames::Operation::_None,
738 XO("Save Metadata As:"),
740 wxT("Tags.xml"),
741 wxT("xml"),
743 wxFD_SAVE | wxFD_OVERWRITE_PROMPT | wxRESIZE_BORDER,
744 this);
745
746 // User canceled...
747 if (fn.empty()) {
748 return;
749 }
750
751 GuardedCall( [&] {
752 // Create/Open the file
753 XMLFileWriter writer{ fn, XO("Error Saving Tags File") };
754
755 // Remember title and track in case they're read only
756 wxString title = local.GetTag(TAG_TITLE);
757 wxString track = local.GetTag(TAG_TRACK);
758
759 // Clear title
760 if (!mEditTitle) {
761 local.SetTag(TAG_TITLE, wxEmptyString);
762 }
763
764 // Clear track
765 if (!mEditTrack) {
766 local.SetTag(TAG_TRACK, wxEmptyString);
767 }
768
769 auto cleanup = finally( [&] {
770 // Restore title
771 if (!mEditTitle) {
772 local.SetTag(TAG_TITLE, title);
773 }
774
775 // Restore track
776 if (!mEditTrack) {
777 local.SetTag(TAG_TRACK, track);
778 }
779 } );
780
781 // Write the metadata
782 local.WriteXML(writer);
783
784 writer.Commit();
785 } );
786}
787
788void TagsEditorDialog::OnSaveDefaults(wxCommandEvent & WXUNUSED(event))
789{
790 auto& local = *mEditTags[mSelectedIndex];
791 // Refresh tags
793
794 // Remember title and track in case they're read only
795 wxString title = local.GetTag(TAG_TITLE);
796 wxString track = local.GetTag(TAG_TRACK);
797
798 // Clear title
799 if (!mEditTitle) {
800 local.SetTag(TAG_TITLE, wxEmptyString);
801 }
802
803 // Clear track
804 if (!mEditTrack) {
805 local.SetTag(TAG_TRACK, wxEmptyString);
806 }
807
808 // Remove any previous defaults
809 gPrefs->DeleteGroup(wxT("/Tags"));
810
811 // Write out each tag
812 for (const auto &pair : local.GetRange()) {
813 const auto &n = pair.first;
814 const auto &v = pair.second;
815 gPrefs->Write(wxT("/Tags/") + n, v);
816 }
817 gPrefs->Flush();
818
819 // Restore title
820 if (!mEditTitle) {
821 local.SetTag(TAG_TITLE, title);
822 }
823
824 // Restore track
825 if (!mEditTrack) {
826 local.SetTag(TAG_TRACK, track);
827 }
828}
829
830void TagsEditorDialog::OnAdd(wxCommandEvent & WXUNUSED(event))
831{
832 mGrid->AppendRows();
833}
834
835void TagsEditorDialog::OnRemove(wxCommandEvent & WXUNUSED(event))
836{
837 size_t row = mGrid->GetGridCursorRow();
838
839 if (!mEditTitle &&
840 mGrid->GetCellValue(row, 0).CmpNoCase(LABEL_TITLE.Translation()) == 0) {
841 return;
842 }
843 else if (!mEditTrack &&
844 mGrid->GetCellValue(row, 0)
845 .CmpNoCase(LABEL_TRACK.Translation()) == 0) {
846 return;
847 }
848 else if (row < STATICCNT) {
849 mGrid->SetCellValue(row, 1, wxEmptyString);
850 }
851 else if (row >= STATICCNT) {
852 mGrid->DeleteRows(row, 1);
853 }
854}
855
856void TagsEditorDialog::OnOk(wxCommandEvent & WXUNUSED(event))
857{
858 if (mGrid->IsCellEditControlShown()) {
859 mGrid->SaveEditControlValue();
860 mGrid->HideCellEditControl();
861#if defined(__WXMAC__)
862 // The cell editors do not capture the ENTER key, so it invokes
863 // the default button ("Ok") when it should just close the
864 // editor. So, cancel the "Ok" action.
865 return;
866#endif
867 }
868
869 if (!Validate() || !TransferDataFromWindow()) {
870 return;
871 }
872
873 for(unsigned i = 0; i < mEditTags.size(); ++i)
874 *mTags[i] = *mEditTags[i];
875
876 wxRect r = GetRect();
877 gPrefs->Write(wxT("/TagsEditorDialog/x"), r.x);
878 gPrefs->Write(wxT("/TagsEditorDialog/y"), r.y);
879 gPrefs->Write(wxT("/TagsEditorDialog/width"), r.width);
880 gPrefs->Write(wxT("/TagsEditorDialog/height"), r.height);
881 gPrefs->Flush();
882
883 EndModal(wxID_OK);
884}
885
886void TagsEditorDialog::OnCancel(wxCommandEvent & WXUNUSED(event))
887{
888 DoCancel(false);
889}
890
892{
893 if (mGrid->IsCellEditControlShown()) {
894 auto editor = mGrid->GetCellEditor(mGrid->GetGridCursorRow(),
895 mGrid->GetGridCursorCol());
896 editor->Reset();
897 // To avoid memory leak, don't forget DecRef()!
898 editor->DecRef();
899 mGrid->HideCellEditControl();
900#if defined(__WXMSW__)
901 return;
902#endif
903 }
904
905 auto focus = wxWindow::FindFocus();
906 if (escKey && focus == mGrid)
907 return;
908
909 EndModal(wxID_CANCEL);
910}
911
912void TagsEditorDialog::OnKeyDown(wxKeyEvent &event)
913{
914 if (event.GetKeyCode() == WXK_ESCAPE)
915 DoCancel(true);
916 else
917 event.Skip();
918}
919
921{
922 int cnt = mGrid->GetNumberRows();
923
924 for (int i = 0; i < cnt; i++) {
925 wxString label = mGrid->GetCellValue(i, 0);
926 if (label.CmpNoCase(LABEL_GENRE.Translation()) == 0) {
927 // This use of GetDefaultEditorForType does not require DecRef.
928 mGrid->SetCellEditor(i, 1, mGrid->GetDefaultEditorForType(wxT("Combo")));
929 }
930 else {
931 mGrid->SetCellEditor(i, 1, NULL); //mGrid->GetDefaultEditor());
932 }
933 }
934}
935
936void TagsEditorDialog::OnNext(wxCommandEvent&)
937{
938 if(mSelectedIndex == static_cast<int>(mEditTags.size() - 1))
939 return;
940
944}
945
946void TagsEditorDialog::OnPrev(wxCommandEvent&)
947{
948 if(mSelectedIndex == 0)
949 return;
950
954}
955
957{
958 auto local = *mEditTags[mSelectedIndex];
959
960 int cnt = local.GetNumUserGenres();
961 int i;
962 wxString parm;
963 wxArrayString g;
964
965 for (i = 0; i < cnt; i++) {
966 g.push_back(local.GetUserGenre(i));
967 }
968 std::sort( g.begin(), g.end() );
969
970 for (i = 0; i < cnt; i++) {
971 parm = parm + (i == 0 ? wxT("") : wxT(",")) + g[i];
972 }
973
974 // Here was a memory leak! wxWidgets docs for wxGrid::GetDefaultEditorForType() say:
975 // "The caller must call DecRef() on the returned pointer."
976 auto editor = mGrid->GetDefaultEditorForType(wxT("Combo"));
977 editor->SetParameters(parm);
978 editor->DecRef();
979}
980
981bool TagsEditorDialog::IsWindowRectValid(const wxRect *windowRect) const
982{
983 wxDisplay display;
984 wxPoint topLeft(windowRect->GetTopLeft().x, windowRect->GetTopLeft().y);
985 wxPoint topRight(windowRect->GetTopRight().x, windowRect->GetTopRight().y);
986 wxPoint bottomLeft(windowRect->GetBottomLeft().x, windowRect->GetBottomLeft().y);
987 wxPoint bottomRight(windowRect->GetBottomRight().x, windowRect->GetBottomRight().y);
988 display.GetFromPoint(topLeft);
989 if (display.GetFromPoint(topLeft) == -1 &&
990 display.GetFromPoint(topRight) == -1 &&
991 display.GetFromPoint(bottomLeft) == -1 &&
992 display.GetFromPoint(bottomRight) == -1) {
993 return false;
994 }
995
996 return true;
997}
998
999#include "Project.h"
1000#include "ProjectHistory.h"
1001#include <wx/frame.h>
1002
1005 const TranslatableString &shortUndoDescription)
1006{
1007 auto &tags = Tags::Get( project );
1008
1009 // Back up my tags
1010 // Tags (artist name, song properties, MP3 ID3 info, etc.)
1011 // The structure may be shared with undo history entries
1012 // To keep undo working correctly, always replace this with a NEW duplicate
1013 // BEFORE doing any editing of it!
1014 auto newTags = tags.Duplicate();
1015
1016 if (ShowEditDialog(*newTags, &GetProjectFrame( project ), title)) {
1017 if (tags != *newTags) {
1018 // Commit the change to project state only now.
1019 Tags::Set( project, newTags );
1020 ProjectHistory::Get( project ).PushState( title, shortUndoDescription);
1021 }
1022 return true;
1023 }
1024
1025 return false;
1026}
1027
1028// Attach menu item
1029#include "CommandContext.h"
1030#include "MenuRegistry.h"
1031#include "CommonCommandFlags.h"
1032
1033namespace {
1034void OnEditMetadata(const CommandContext &context)
1035{
1036 auto &project = context.project;
1038 XO("Edit Metadata Tags"), XO("Metadata Tags"));
1039}
1040
1041using namespace MenuRegistry;
1042
1044 Command( wxT("EditMetaData"), XXO("&Metadata Editor"), OnEditMetadata,
1046 wxT("Edit/Other")
1047};
1048}
wxT("CloseDown"))
R GuardedCall(const F1 &body, const F2 &handler=F2::Default(), F3 delayedHandler=DefaultDelayedHandlerAction) noexcept(noexcept(handler(std::declval< AudacityException * >())) &&noexcept(handler(nullptr)) &&noexcept(std::function< void(AudacityException *)>{std::move(delayedHandler)}))
Execute some code on any thread; catch any AudacityException; enqueue error report on the main thread...
int AudacityMessageBox(const TranslatableString &message, const TranslatableString &caption, long style, wxWindow *parent, int x, int y)
END_EVENT_TABLE()
const ReservedCommandFlag & AudioIONotBusyFlag()
int min(int a, int b)
EVT_BUTTON(wxID_NO, DependencyDialog::OnNo) EVT_BUTTON(wxID_YES
EffectDistortionSettings params
XO("Cut/Copy/Paste")
XXO("&Cut/Copy/Paste Toolbar")
#define _(s)
Definition: Internat.h:73
#define safenew
Definition: MemoryX.h:10
static const AudacityProject::AttachedObjects::RegisteredFactory key
static const auto title
audacity::BasicSettings * gPrefs
Definition: Prefs.cpp:68
AUDACITY_DLL_API wxFrame & GetProjectFrame(AudacityProject &project)
Get the top-level window associated with the project (as a wxFrame only, when you do not need to use ...
accessors for certain important windows associated with each project
FilePath SelectFile(FileNames::Operation op, const TranslatableString &message, const FilePath &default_path, const FilePath &default_filename, const FileExtension &default_extension, const FileTypes &fileTypes, int flags, wxWindow *parent)
Definition: SelectFile.cpp:17
@ eIsCreating
Definition: ShuttleGui.h:37
@ eOkButton
Definition: ShuttleGui.h:609
@ eCancelButton
Definition: ShuttleGui.h:610
@ eHelpButton
Definition: ShuttleGui.h:613
#define TAG_TRACK
Definition: Tags.h:61
#define TAG_COMMENTS
Definition: Tags.h:64
#define TAG_GENRE
Definition: Tags.h:63
std::unordered_map< wxString, wxString > TagMap
Definition: Tags.h:56
#define TAG_ALBUM
Definition: Tags.h:60
#define TAG_YEAR
Definition: Tags.h:62
#define TAG_TITLE
Definition: Tags.h:58
#define TAG_ARTIST
Definition: Tags.h:59
wxString name
Definition: TagsEditor.cpp:166
@ SaveDefaultsID
Definition: TagsEditor.cpp:189
@ ClearID
Definition: TagsEditor.cpp:182
@ RemoveID
Definition: TagsEditor.cpp:191
@ PrevID
Definition: TagsEditor.cpp:183
@ LoadID
Definition: TagsEditor.cpp:187
@ ResetID
Definition: TagsEditor.cpp:186
@ SaveID
Definition: TagsEditor.cpp:188
@ EditID
Definition: TagsEditor.cpp:185
@ NextID
Definition: TagsEditor.cpp:184
@ AddID
Definition: TagsEditor.cpp:190
#define LABEL_TITLE
Definition: TagsEditor.cpp:146
#define LABEL_COMMENTS
Definition: TagsEditor.cpp:151
TranslatableString label
Definition: TagsEditor.cpp:165
#define STATICCNT
Definition: TagsEditor.cpp:179
#define LABEL_GENRE
Definition: TagsEditor.cpp:150
static TranslatableStrings names
Definition: TagsEditor.cpp:153
#define LABEL_YEAR
Definition: TagsEditor.cpp:149
#define LABEL_ARTIST
Definition: TagsEditor.cpp:145
#define LABEL_ALBUM
Definition: TagsEditor.cpp:147
#define LABEL_TRACK
Definition: TagsEditor.cpp:148
static const struct @54 labelmap[]
const auto project
#define S(N)
Definition: ToChars.cpp:64
std::vector< TranslatableString > TranslatableStrings
static const auto fn
The top-level handle to an Audacity project. It serves as a source of events that other objects can b...
Definition: Project.h:90
void PaintBackground(wxDC &, const wxRect &WXUNUSED(rectCell), const wxGridCellAttr &WXUNUSED(attr)) override
Definition: TagsEditor.cpp:50
ComboEditor(const wxArrayString &choices, bool allowOthers=false)
Definition: TagsEditor.cpp:43
wxArrayString m_choices
Definition: TagsEditor.cpp:137
virtual void StartingKey(wxKeyEvent &event) override
Definition: TagsEditor.cpp:80
bool m_allowOthers
Definition: TagsEditor.cpp:138
void SetParameters(const wxString &params) override
Definition: TagsEditor.cpp:55
void SetSize(const wxRect &rectOrig) override
Definition: TagsEditor.cpp:66
wxGridCellEditor * Clone() const override
Definition: TagsEditor.cpp:131
CommandContext provides additional information to an 'Apply()' command. It provides the project,...
AudacityProject & project
FILES_API const FileType XMLFiles
Definition: FileNames.h:73
static FormatterContext EmptyContext()
Supplies an accessible grid based on wxGrid.
Definition: Grid.h:190
static void ShowHelp(wxWindow *parent, const FilePath &localFileName, const URLString &remoteURL, bool bModal=false, bool alwaysDefaultBrowser=false)
Definition: HelpSystem.cpp:231
void PushState(const TranslatableString &desc, const TranslatableString &shortDesc)
static ProjectHistory & Get(AudacityProject &project)
Generates classes whose instances register items at construction.
Definition: Registry.h:388
Derived from ShuttleGuiBase, an Audacity specific class for shuttling data to and from GUI.
Definition: ShuttleGui.h:640
Derived from ExpandingToolBar, this dialog allows editing of Tags.
Definition: TagsEditor.h:22
void OnRemove(wxCommandEvent &event)
Definition: TagsEditor.cpp:835
wxButton * mPrev
Definition: TagsEditor.h:95
std::vector< Tags * > mTags
Definition: TagsEditor.h:83
wxGridCellStringRenderer * mStringRenderer
Definition: TagsEditor.h:97
void DoCancel(bool escKey)
Definition: TagsEditor.cpp:891
wxStaticText * mName
Definition: TagsEditor.h:93
static AUDACITY_DLL_API bool ShowEditDialog(Tags &tags, wxWindow *parent, const TranslatableString &title)
Definition: TagsEditor.cpp:30
virtual ~TagsEditorDialog()
Definition: TagsEditor.cpp:285
std::vector< std::unique_ptr< Tags > > mEditTags
Definition: TagsEditor.h:90
void OnClear(wxCommandEvent &event)
Definition: TagsEditor.cpp:663
void OnAdd(wxCommandEvent &event)
Definition: TagsEditor.cpp:830
void OnKeyDown(wxKeyEvent &event)
Definition: TagsEditor.cpp:912
bool IsWindowRectValid(const wxRect *windowRect) const
Definition: TagsEditor.cpp:981
void OnReset(wxCommandEvent &event)
Definition: TagsEditor.cpp:616
void PopulateOrExchange(ShuttleGui &S)
Definition: TagsEditor.cpp:298
void OnNext(wxCommandEvent &)
Definition: TagsEditor.cpp:936
void OnPrev(wxCommandEvent &)
Definition: TagsEditor.cpp:946
bool TransferDataToWindow() override
Definition: TagsEditor.cpp:453
void OnOk(wxCommandEvent &event)
Definition: TagsEditor.cpp:856
void OnLoad(wxCommandEvent &event)
Definition: TagsEditor.cpp:670
std::vector< wxString > mNames
Definition: TagsEditor.h:84
void OnSave(wxCommandEvent &event)
Definition: TagsEditor.cpp:727
bool TransferDataFromWindow() override
Definition: TagsEditor.cpp:397
void OnHelp(wxCommandEvent &Evt)
Definition: TagsEditor.cpp:392
void OnSaveDefaults(wxCommandEvent &event)
Definition: TagsEditor.cpp:788
void OnChange(wxGridEvent &event)
Definition: TagsEditor.cpp:524
ComboEditor * mComboEditor
Definition: TagsEditor.h:96
void OnEdit(wxCommandEvent &event)
Definition: TagsEditor.cpp:561
wxButton * mNext
Definition: TagsEditor.h:94
void OnCancel(wxCommandEvent &event)
Definition: TagsEditor.cpp:886
static AUDACITY_DLL_API bool EditProjectMetadata(AudacityProject &project, const TranslatableString &title, const TranslatableString &shortUndoDescription)
ID3 Tags (for MP3)
Definition: Tags.h:73
static Tags & Get(AudacityProject &project)
Definition: Tags.cpp:214
static Tags & Set(AudacityProject &project, const std::shared_ptr< Tags > &tags)
Definition: Tags.cpp:224
void SetTag(const wxString &name, const wxString &value, const bool bSpecialTag=false)
Definition: Tags.cpp:431
Holds a msgid for the translation catalog; may also bind format arguments.
wxString Translation() const
Reads a file and passes the results through an XMLTagHandler.
Definition: XMLFileReader.h:19
const TranslatableString & GetErrorStr() const
bool Parse(XMLTagHandler *baseHandler, const FilePath &fname)
Wrapper to output XML data to files.
Definition: XMLWriter.h:84
virtual bool Flush() noexcept=0
bool DeleteGroup(const wxString &key)
Deletes specified group if exists.
virtual bool Write(const wxString &key, bool value)=0
virtual bool Read(const wxString &key, bool *value) const =0
void SetName(const TranslatableString &title)
std::unique_ptr< WindowPlacement > FindFocus()
Find the window that is accepting keyboard input, if any.
Definition: BasicUI.h:383
FILES_API FilePath DataDir()
Audacity user data directory.
constexpr auto Command
Definition: MenuRegistry.h:456
void OnEditMetadata(const CommandContext &context)
STL namespace.