discard column colors if item is selected (optional)
[ncmpcpp.git] / src / tag_editor.cpp
blob443a805c4d62a4683a5a17a28d66cf922b65d409
1 /***************************************************************************
2 * Copyright (C) 2008-2009 by Andrzej Rybczak *
3 * electricityispower@gmail.com *
4 * *
5 * This program is free software; you can redistribute it and/or modify *
6 * it under the terms of the GNU General Public License as published by *
7 * the Free Software Foundation; either version 2 of the License, or *
8 * (at your option) any later version. *
9 * *
10 * This program is distributed in the hope that it will be useful, *
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
13 * GNU General Public License for more details. *
14 * *
15 * You should have received a copy of the GNU General Public License *
16 * along with this program; if not, write to the *
17 * Free Software Foundation, Inc., *
18 * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. *
19 ***************************************************************************/
21 #include "tag_editor.h"
23 #ifdef HAVE_TAGLIB_H
25 #include <fstream>
26 #include <stdexcept>
28 // taglib includes
29 #include "id3v2tag.h"
30 #include "textidentificationframe.h"
31 #include "mpegfile.h"
32 #include "vorbisfile.h"
33 #include "flacfile.h"
35 #include "browser.h"
36 #include "charset.h"
37 #include "display.h"
38 #include "global.h"
39 #include "info.h"
40 #include "playlist.h"
42 using Global::MainHeight;
43 using Global::MainStartY;
45 TagEditor *myTagEditor = new TagEditor;
47 const std::string TagEditor::PatternsFile = config_dir + "patterns.list";
48 std::list<std::string> TagEditor::Patterns;
50 size_t TagEditor::LeftColumnWidth;
51 size_t TagEditor::MiddleColumnWidth;
52 size_t TagEditor::MiddleColumnStartX;
53 size_t TagEditor::RightColumnWidth;
54 size_t TagEditor::RightColumnStartX;
56 size_t TagEditor::FParserDialogWidth;
57 size_t TagEditor::FParserDialogHeight;
58 size_t TagEditor::FParserWidth;
59 size_t TagEditor::FParserWidthOne;
60 size_t TagEditor::FParserWidthTwo;
61 size_t TagEditor::FParserHeight;
63 void TagEditor::Init()
65 SetDimensions();
67 Albums = new Menu<string_pair>(0, MainStartY, LeftColumnWidth, MainHeight, "Albums", Config.main_color, brNone);
68 Albums->HighlightColor(Config.active_column_color);
69 Albums->CyclicScrolling(Config.use_cyclic_scrolling);
70 Albums->SetItemDisplayer(Display::Pairs);
71 Albums->SetGetStringFunction(StringPairToString);
73 Dirs = new Menu<string_pair>(0, MainStartY, LeftColumnWidth, MainHeight, "Directories", Config.main_color, brNone);
74 Dirs->HighlightColor(Config.active_column_color);
75 Dirs->CyclicScrolling(Config.use_cyclic_scrolling);
76 Dirs->SetItemDisplayer(Display::Pairs);
77 Dirs->SetGetStringFunction(StringPairToString);
79 LeftColumn = Config.albums_in_tag_editor ? Albums : Dirs;
81 TagTypes = new Menu<std::string>(MiddleColumnStartX, MainStartY, MiddleColumnWidth, MainHeight, "Tag types", Config.main_color, brNone);
82 TagTypes->HighlightColor(Config.main_highlight_color);
83 TagTypes->CyclicScrolling(Config.use_cyclic_scrolling);
84 TagTypes->SetItemDisplayer(Display::Generic);
86 for (const Info::Metadata *m = Info::Tags; m->Name; ++m)
87 TagTypes->AddOption(m->Name);
88 TagTypes->AddSeparator();
89 TagTypes->AddOption("Filename");
90 TagTypes->AddSeparator();
91 TagTypes->AddOption("Options", 1, 1);
92 TagTypes->AddSeparator();
93 TagTypes->AddOption("Reset");
94 TagTypes->AddOption("Save");
95 TagTypes->AddSeparator();
96 TagTypes->AddOption("Capitalize First Letters");
97 TagTypes->AddOption("lower all letters");
99 Tags = new Menu<MPD::Song>(RightColumnStartX, MainStartY, RightColumnWidth, MainHeight, "Tags", Config.main_color, brNone);
100 Tags->HighlightColor(Config.main_highlight_color);
101 Tags->CyclicScrolling(Config.use_cyclic_scrolling);
102 Tags->SetSelectPrefix(&Config.selected_item_prefix);
103 Tags->SetSelectSuffix(&Config.selected_item_suffix);
104 Tags->SetItemDisplayer(Display::Tags);
105 Tags->SetItemDisplayerUserData(TagTypes);
106 Tags->SetGetStringFunction(TagToString);
107 Tags->SetGetStringFunctionUserData(TagTypes);
109 FParserDialog = new Menu<std::string>((COLS-FParserDialogWidth)/2, (MainHeight-FParserDialogHeight)/2+MainStartY, FParserDialogWidth, FParserDialogHeight, "", Config.main_color, Config.window_border);
110 FParserDialog->CyclicScrolling(Config.use_cyclic_scrolling);
111 FParserDialog->SetItemDisplayer(Display::Generic);
112 FParserDialog->AddOption("Get tags from filename");
113 FParserDialog->AddOption("Rename files");
114 FParserDialog->AddSeparator();
115 FParserDialog->AddOption("Cancel");
117 FParser = new Menu<std::string>((COLS-FParserWidth)/2, (MainHeight-FParserHeight)/2+MainStartY, FParserWidthOne, FParserHeight, "_", Config.main_color, Config.active_window_border);
118 FParser->CyclicScrolling(Config.use_cyclic_scrolling);
119 FParser->SetItemDisplayer(Display::Generic);
121 FParserLegend = new Scrollpad((COLS-FParserWidth)/2+FParserWidthOne, (MainHeight-FParserHeight)/2+MainStartY, FParserWidthTwo, FParserHeight, "Legend", Config.main_color, Config.window_border);
123 FParserPreview = new Scrollpad((COLS-FParserWidth)/2+FParserWidthOne, (MainHeight-FParserHeight)/2+MainStartY, FParserWidthTwo, FParserHeight, "Preview", Config.main_color, Config.window_border);
125 w = LeftColumn;
126 isInitialized = 1;
129 void TagEditor::SetDimensions()
131 MiddleColumnWidth = std::min(26, COLS-2);
132 LeftColumnWidth = (COLS-MiddleColumnWidth)/2;
133 MiddleColumnStartX = LeftColumnWidth+1;
134 RightColumnWidth = COLS-LeftColumnWidth-MiddleColumnWidth-2;
135 RightColumnStartX = LeftColumnWidth+MiddleColumnWidth+2;
137 FParserDialogWidth = std::min(30, COLS);
138 FParserDialogHeight = std::min(size_t(6), MainHeight);
139 FParserWidth = COLS*0.9;
140 FParserHeight = std::min(size_t(LINES*0.8), MainHeight);
141 FParserWidthOne = FParserWidth/2;
142 FParserWidthTwo = FParserWidth-FParserWidthOne;
145 void TagEditor::Resize()
147 SetDimensions();
149 Albums->Resize(LeftColumnWidth, MainHeight);
150 Dirs->Resize(LeftColumnWidth, MainHeight);
151 TagTypes->Resize(MiddleColumnWidth, MainHeight);
152 Tags->Resize(RightColumnWidth, MainHeight);
153 FParserDialog->Resize(FParserDialogWidth, FParserDialogHeight);
154 FParser->Resize(FParserWidthOne, FParserHeight);
155 FParserLegend->Resize(FParserWidthTwo, FParserHeight);
156 FParserPreview->Resize(FParserWidthTwo, FParserHeight);
158 Albums->MoveTo(0, MainStartY);
159 Dirs->MoveTo(0, MainStartY);
160 TagTypes->MoveTo(MiddleColumnStartX, MainStartY);
161 Tags->MoveTo(RightColumnStartX, MainStartY);
163 FParserDialog->MoveTo((COLS-FParserDialogWidth)/2, (MainHeight-FParserDialogHeight)/2+MainStartY);
164 FParser->MoveTo((COLS-FParserWidth)/2, (MainHeight-FParserHeight)/2+MainStartY);
165 FParserLegend->MoveTo((COLS-FParserWidth)/2+FParserWidthOne, (MainHeight-FParserHeight)/2+MainStartY);
166 FParserPreview->MoveTo((COLS-FParserWidth)/2+FParserWidthOne, (MainHeight-FParserHeight)/2+MainStartY);
168 if (MainHeight < 5 && (w == FParserDialog || w == FParser || w == FParserHelper)) // screen too low
169 w = TagTypes; // fall back to main columns
171 hasToBeResized = 0;
174 std::basic_string<my_char_t> TagEditor::Title()
176 return U("Tag editor");
179 void TagEditor::SwitchTo()
181 using Global::myScreen;
183 if (myScreen == this)
184 return;
186 if (!isInitialized)
187 Init();
189 if (hasToBeResized)
190 Resize();
192 if (myScreen != this && myScreen->isTabbable())
193 Global::myPrevScreen = myScreen;
194 myScreen = this;
195 Global::RedrawHeader = 1;
196 Refresh();
199 void TagEditor::Refresh()
201 LeftColumn->Display();
202 mvvline(MainStartY, MiddleColumnStartX-1, 0, MainHeight);
203 TagTypes->Display();
204 mvvline(MainStartY, RightColumnStartX-1, 0, MainHeight);
205 Tags->Display();
207 if (w == FParserDialog)
209 FParserDialog->Display();
211 else if (w == FParser || w == FParserHelper)
213 FParser->Display();
214 FParserHelper->Display();
218 void TagEditor::Update()
220 if (LeftColumn->Empty())
222 LeftColumn->Window::Clear();
223 Tags->Clear();
224 MPD::TagList list;
225 if (Config.albums_in_tag_editor)
227 *Albums << XY(0, 0) << "Fetching albums...";
228 Albums->Window::Refresh();
229 Mpd.GetList(list, MPD_TAG_ALBUM);
230 for (MPD::TagList::const_iterator it = list.begin(); it != list.end(); ++it)
232 MPD::SongList l;
233 Mpd.StartSearch(1);
234 Mpd.AddSearch(MPD_TAG_ALBUM, *it);
235 Mpd.CommitSearch(l);
236 if (!l.empty())
238 l[0]->Localize();
239 Albums->AddOption(std::make_pair(l[0]->toString(Config.tag_editor_album_format), *it));
241 MPD::FreeSongList(l);
243 Albums->Sort<CaseInsensitiveSorting>();
245 else
247 int highlightme = -1;
248 Mpd.GetDirectories(itsBrowsedDir, list);
249 sort(list.begin(), list.end(), CaseInsensitiveSorting());
250 if (itsBrowsedDir != "/")
252 size_t slash = itsBrowsedDir.rfind("/");
253 std::string parent = slash != std::string::npos ? itsBrowsedDir.substr(0, slash) : "/";
254 Dirs->AddOption(make_pair("[..]", parent));
256 else
258 Dirs->AddOption(std::make_pair(".", "/"));
260 for (MPD::TagList::const_iterator it = list.begin(); it != list.end(); ++it)
262 size_t slash = it->rfind("/");
263 std::string to_display = slash != std::string::npos ? it->substr(slash+1) : *it;
264 utf_to_locale(to_display);
265 Dirs->AddOption(make_pair(to_display, *it));
266 if (*it == itsHighlightedDir)
267 highlightme = Dirs->Size()-1;
269 if (highlightme != -1)
270 Dirs->Highlight(highlightme);
272 LeftColumn->Display();
273 TagTypes->Refresh();
276 if (Tags->Empty())
278 Tags->Reset();
279 MPD::SongList list;
280 if (Config.albums_in_tag_editor)
282 Mpd.StartSearch(1);
283 Mpd.AddSearch(MPD_TAG_ALBUM, Albums->Current().second);
284 Mpd.CommitSearch(list);
285 sort(list.begin(), list.end(), CaseInsensitiveSorting());
286 for (MPD::SongList::iterator it = list.begin(); it != list.end(); ++it)
288 (*it)->Localize();
289 Tags->AddOption(**it);
292 else
294 Mpd.GetSongs(Dirs->Current().second, list);
295 sort(list.begin(), list.end(), CaseInsensitiveSorting());
296 for (MPD::SongList::const_iterator it = list.begin(); it != list.end(); ++it)
298 (*it)->Localize();
299 Tags->AddOption(**it);
302 MPD::FreeSongList(list);
303 Tags->Window::Clear();
304 Tags->Refresh();
307 if (w == TagTypes && TagTypes->Choice() < 13)
309 Tags->Refresh();
311 else if (TagTypes->Choice() >= 13)
313 Tags->Window::Clear();
314 Tags->Window::Refresh();
318 void TagEditor::EnterPressed()
320 using Global::wFooter;
322 if (w == Dirs)
324 MPD::TagList test;
325 Mpd.GetDirectories(LeftColumn->Current().second, test);
326 if (!test.empty())
328 itsHighlightedDir = itsBrowsedDir;
329 itsBrowsedDir = LeftColumn->Current().second;
330 LeftColumn->Clear();
331 LeftColumn->Reset();
333 else
334 ShowMessage("No subdirs found");
336 else if (w == FParserDialog)
338 size_t choice = FParserDialog->RealChoice();
339 if (choice == 2) // cancel
341 w = TagTypes;
342 Refresh();
343 return;
345 GetPatternList();
347 // prepare additional windows
349 FParserLegend->Clear();
350 *FParserLegend << "%a - artist\n";
351 *FParserLegend << "%A - album artist\n";
352 *FParserLegend << "%t - title\n";
353 *FParserLegend << "%b - album\n";
354 *FParserLegend << "%y - year\n";
355 *FParserLegend << "%n - track number\n";
356 *FParserLegend << "%g - genre\n";
357 *FParserLegend << "%c - composer\n";
358 *FParserLegend << "%p - performer\n";
359 *FParserLegend << "%d - disc\n";
360 *FParserLegend << "%C - comment\n\n";
361 *FParserLegend << fmtBold << "Files:\n" << fmtBoldEnd;
362 for (MPD::SongList::const_iterator it = EditedSongs.begin(); it != EditedSongs.end(); ++it)
363 *FParserLegend << Config.color2 << " * " << clEnd << (*it)->GetName() << "\n";
364 FParserLegend->Flush();
366 if (!Patterns.empty())
367 Config.pattern = Patterns.front();
368 FParser->Clear();
369 FParser->Reset();
370 FParser->AddOption("Pattern: " + Config.pattern);
371 FParser->AddOption("Preview");
372 FParser->AddOption("Legend");
373 FParser->AddSeparator();
374 FParser->AddOption("Proceed");
375 FParser->AddOption("Cancel");
376 if (!Patterns.empty())
378 FParser->AddSeparator();
379 FParser->AddOption("Recent patterns", 1, 1);
380 FParser->AddSeparator();
381 for (std::list<std::string>::const_iterator it = Patterns.begin(); it != Patterns.end(); ++it)
382 FParser->AddOption(*it);
385 FParser->SetTitle(choice == 0 ? "Get tags from filename" : "Rename files");
386 w = FParser;
387 FParserUsePreview = 1;
388 FParserHelper = FParserLegend;
389 FParserHelper->Display();
391 else if (w == FParser)
393 bool quit = 0;
394 size_t pos = FParser->RealChoice();
396 if (pos == 3) // save
397 FParserUsePreview = 0;
399 if (pos == 0) // change pattern
401 LockStatusbar();
402 Statusbar() << "Pattern: ";
403 std::string new_pattern = wFooter->GetString(Config.pattern);
404 UnlockStatusbar();
405 if (!new_pattern.empty())
407 Config.pattern = new_pattern;
408 FParser->at(0) = "Pattern: ";
409 FParser->at(0) += Config.pattern;
412 else if (pos == 1 || pos == 3) // preview or proceed
414 bool success = 1;
415 ShowMessage("Parsing...");
416 FParserPreview->Clear();
417 for (MPD::SongList::iterator it = EditedSongs.begin(); it != EditedSongs.end(); ++it)
419 MPD::Song &s = **it;
420 if (FParserDialog->Choice() == 0) // get tags from filename
422 if (FParserUsePreview)
424 *FParserPreview << fmtBold << s.GetName() << ":\n" << fmtBoldEnd;
425 *FParserPreview << ParseFilename(s, Config.pattern, FParserUsePreview) << "\n";
427 else
428 ParseFilename(s, Config.pattern, FParserUsePreview);
430 else // rename files
432 std::string file = s.GetName();
433 size_t last_dot = file.rfind(".");
434 std::string extension = file.substr(last_dot);
435 std::string new_file = GenerateFilename(s, "{" + Config.pattern + "}");
436 if (new_file.empty() && !FParserUsePreview)
438 ShowMessage("File \"%s\" would have an empty name!", s.GetName().c_str());
439 FParserUsePreview = 1;
440 success = 0;
442 if (!FParserUsePreview)
443 s.SetNewName(new_file + extension);
444 *FParserPreview << file << Config.color2 << " -> " << clEnd;
445 if (new_file.empty())
446 *FParserPreview << Config.empty_tags_color << Config.empty_tag << clEnd;
447 else
448 *FParserPreview << new_file << extension;
449 *FParserPreview << "\n\n";
450 if (!success)
451 break;
454 if (FParserUsePreview)
456 FParserHelper = FParserPreview;
457 FParserHelper->Flush();
458 FParserHelper->Display();
460 else if (success)
462 Patterns.remove(Config.pattern);
463 Patterns.insert(Patterns.begin(), Config.pattern);
464 quit = 1;
466 if (pos != 3 || success)
467 ShowMessage("Operation finished!");
469 else if (pos == 2) // show legend
471 FParserHelper = FParserLegend;
472 FParserHelper->Display();
474 else if (pos == 4) // cancel
476 quit = 1;
478 else // list of patterns
480 Config.pattern = FParser->Current();
481 FParser->at(0) = "Pattern: " + Config.pattern;
484 if (quit)
486 SavePatternList();
487 w = TagTypes;
488 Refresh();
489 return;
493 if ((w != TagTypes && w != Tags) || Tags->Empty()) // after this point we start dealing with tags
494 return;
496 EditedSongs.clear();
497 if (Tags->hasSelected()) // if there are selected songs, perform operations only on them
499 std::vector<size_t> selected;
500 Tags->GetSelected(selected);
501 for (std::vector<size_t>::const_iterator it = selected.begin(); it != selected.end(); ++it)
502 EditedSongs.push_back(&(*Tags)[*it]);
504 else
505 for (size_t i = 0; i < Tags->Size(); ++i)
506 EditedSongs.push_back(&(*Tags)[i]);
508 size_t id = TagTypes->RealChoice();
510 if (w == TagTypes && id == 5)
512 LockStatusbar();
513 Statusbar() << "Number tracks? [" << fmtBold << 'y' << fmtBoldEnd << '/' << fmtBold << 'n' << fmtBoldEnd << "] ";
514 wFooter->Refresh();
515 int in = 0;
518 TraceMpdStatus();
519 wFooter->ReadKey(in);
521 while (in != 'y' && in != 'n');
522 UnlockStatusbar();
523 if (in == 'y')
525 MPD::SongList::iterator it = EditedSongs.begin();
526 for (unsigned i = 1; i <= EditedSongs.size(); ++i, ++it)
528 if (Config.tag_editor_extended_numeration)
529 (*it)->SetTrack(IntoStr(i) + "/" + IntoStr(EditedSongs.size()));
530 else
531 (*it)->SetTrack(i);
533 ShowMessage("Tracks numbered!");
535 else
536 ShowMessage("Aborted!");
537 return;
540 if (id < 11)
542 MPD::Song::GetFunction get = Info::Tags[id].Get;
543 MPD::Song::SetFunction set = Info::Tags[id].Set;
544 if (id > 0 && w == TagTypes)
546 LockStatusbar();
547 Statusbar() << fmtBold << TagTypes->Current() << fmtBoldEnd << ": ";
548 std::string new_tag = wFooter->GetString(Tags->Current().GetTags(get));
549 UnlockStatusbar();
550 for (MPD::SongList::iterator it = EditedSongs.begin(); it != EditedSongs.end(); ++it)
551 (*it)->SetTags(set, new_tag);
553 else if (w == Tags)
555 LockStatusbar();
556 Statusbar() << fmtBold << TagTypes->Current() << fmtBoldEnd << ": ";
557 std::string new_tag = wFooter->GetString(Tags->Current().GetTags(get));
558 UnlockStatusbar();
559 if (new_tag != Tags->Current().GetTags(get))
560 Tags->Current().SetTags(set, new_tag);
561 Tags->Scroll(wDown);
564 else
566 if (id == 11) // filename related options
568 if (w == TagTypes)
570 if (size_t(COLS) < FParserDialogWidth || MainHeight < FParserDialogHeight)
572 ShowMessage("Screen is too small to display additional windows!");
573 return;
575 FParserDialog->Reset();
576 w = FParserDialog;
578 else if (w == Tags)
580 MPD::Song &s = Tags->Current();
581 std::string old_name = s.GetNewName().empty() ? s.GetName() : s.GetNewName();
582 size_t last_dot = old_name.rfind(".");
583 std::string extension = old_name.substr(last_dot);
584 old_name = old_name.substr(0, last_dot);
585 LockStatusbar();
586 Statusbar() << fmtBold << "New filename: " << fmtBoldEnd;
587 std::string new_name = wFooter->GetString(old_name);
588 UnlockStatusbar();
589 if (!new_name.empty() && new_name != old_name)
590 s.SetNewName(new_name + extension);
591 Tags->Scroll(wDown);
594 else if (id == 12) // reset
596 Tags->Clear();
597 ShowMessage("Changes reset");
599 else if (id == 13) // save
601 bool success = 1;
602 ShowMessage("Writing changes...");
603 for (MPD::SongList::iterator it = EditedSongs.begin(); it != EditedSongs.end(); ++it)
605 ShowMessage("Writing tags in \"%s\"...", (*it)->GetName().c_str());
606 if (!WriteTags(**it))
608 static const char msg[] = "Error while writing tags in \"%s\"!";
609 ShowMessage(msg, Shorten(TO_WSTRING((*it)->GetFile()), COLS-static_strlen(msg)).c_str());
610 success = 0;
611 break;
614 if (success)
616 ShowMessage("Tags updated!");
617 TagTypes->HighlightColor(Config.main_highlight_color);
618 TagTypes->Reset();
619 w->Refresh();
620 w = LeftColumn;
621 LeftColumn->HighlightColor(Config.active_column_color);
622 Mpd.UpdateDirectory(locale_to_utf_cpy(FindSharedDir(Tags)));
624 else
625 Tags->Clear();
627 else if (id == 14) // capitalize first letters
629 ShowMessage("Processing...");
630 for (MPD::SongList::iterator it = EditedSongs.begin(); it != EditedSongs.end(); ++it)
631 CapitalizeFirstLetters(**it);
632 ShowMessage("Done!");
634 else if (id == 15) // lower all letters
636 ShowMessage("Processing...");
637 for (MPD::SongList::iterator it = EditedSongs.begin(); it != EditedSongs.end(); ++it)
638 LowerAllLetters(**it);
639 ShowMessage("Done!");
644 void TagEditor::SpacePressed()
646 if (w == Tags)
648 Tags->Select(Tags->Choice(), !Tags->isSelected());
649 w->Scroll(wDown);
650 return;
652 if (w != LeftColumn)
653 return;
655 Config.albums_in_tag_editor = !Config.albums_in_tag_editor;
656 w = LeftColumn = Config.albums_in_tag_editor ? Albums : Dirs;
657 ShowMessage("Switched to %s view", Config.albums_in_tag_editor ? "albums" : "directories");
658 LeftColumn->Display();
659 Tags->Clear();
662 void TagEditor::MouseButtonPressed(MEVENT me)
664 if (w == FParserDialog)
666 if (FParserDialog->hasCoords(me.x, me.y))
668 if (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED))
670 FParserDialog->Goto(me.y);
671 if (me.bstate & BUTTON3_PRESSED)
672 EnterPressed();
674 else
675 Screen<Window>::MouseButtonPressed(me);
678 else if (w == FParser || w == FParserHelper)
680 if (FParser->hasCoords(me.x, me.y))
682 if (w != FParser)
683 PrevColumn();
684 if (size_t(me.y) < FParser->Size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)))
686 FParser->Goto(me.y);
687 if (me.bstate & BUTTON3_PRESSED)
688 EnterPressed();
690 else
691 Screen<Window>::MouseButtonPressed(me);
693 else if (FParserHelper->hasCoords(me.x, me.y))
695 if (w != FParserHelper)
696 NextColumn();
697 reinterpret_cast<Screen<Scrollpad> *>(this)->Screen<Scrollpad>::MouseButtonPressed(me);
700 else if (!LeftColumn->Empty() && LeftColumn->hasCoords(me.x, me.y))
702 if (w != LeftColumn)
704 PrevColumn();
705 PrevColumn();
707 if (size_t(me.y) < LeftColumn->Size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)))
709 LeftColumn->Goto(me.y);
710 if (me.bstate & BUTTON1_PRESSED)
711 EnterPressed();
712 else
713 SpacePressed();
715 else
716 Screen<Window>::MouseButtonPressed(me);
717 Tags->Clear();
719 else if (!TagTypes->Empty() && TagTypes->hasCoords(me.x, me.y))
721 if (w != TagTypes)
722 w == LeftColumn ? NextColumn() : PrevColumn();
723 if (size_t(me.y) < TagTypes->Size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)))
725 if (!TagTypes->Goto(me.y))
726 return;
727 TagTypes->Refresh();
728 Tags->Refresh();
729 if (me.bstate & BUTTON3_PRESSED)
730 EnterPressed();
732 else
733 Screen<Window>::MouseButtonPressed(me);
735 else if (!Tags->Empty() && Tags->hasCoords(me.x, me.y))
737 if (w != Tags)
739 NextColumn();
740 NextColumn();
742 if (size_t(me.y) < Tags->Size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)))
744 Tags->Goto(me.y);
745 Tags->Refresh();
746 if (me.bstate & BUTTON3_PRESSED)
747 EnterPressed();
749 else
750 Screen<Window>::MouseButtonPressed(me);
754 MPD::Song *TagEditor::CurrentSong()
756 return w == Tags && !Tags->Empty() ? &Tags->Current() : 0;
759 void TagEditor::GetSelectedSongs(MPD::SongList &v)
761 std::vector<size_t> selected;
762 Tags->GetSelected(selected);
763 if (selected.empty())
764 selected.push_back(Tags->Choice());
765 for (std::vector<size_t>::const_iterator it = selected.begin(); it != selected.end(); ++it)
766 v.push_back(new MPD::Song(Tags->at(*it)));
769 void TagEditor::ApplyFilter(const std::string &s)
771 if (w == Dirs)
772 Dirs->ApplyFilter(s, 1, REG_ICASE | Config.regex_type);
773 else if (w == Albums)
774 Albums->ApplyFilter(s, 0, REG_ICASE | Config.regex_type);
775 else if (w == Tags)
776 Tags->ApplyFilter(s, 0, REG_ICASE | Config.regex_type);
779 List *TagEditor::GetList()
781 if (w == LeftColumn)
782 return LeftColumn;
783 else if (w == Tags)
784 return Tags;
785 else
786 return 0;
789 void TagEditor::NextColumn()
791 if (w == LeftColumn)
793 LeftColumn->HighlightColor(Config.main_highlight_color);
794 w->Refresh();
795 w = TagTypes;
796 TagTypes->HighlightColor(Config.active_column_color);
798 else if (w == TagTypes && TagTypes->Choice() < 12 && !Tags->Empty())
800 TagTypes->HighlightColor(Config.main_highlight_color);
801 w->Refresh();
802 w = Tags;
803 Tags->HighlightColor(Config.active_column_color);
805 else if (w == FParser)
807 FParser->SetBorder(Config.window_border);
808 FParser->Display();
809 w = FParserHelper;
810 FParserHelper->SetBorder(Config.active_window_border);
811 FParserHelper->Display();
815 void TagEditor::PrevColumn()
817 if (w == Tags)
819 Tags->HighlightColor(Config.main_highlight_color);
820 w->Refresh();
821 w = TagTypes;
822 TagTypes->HighlightColor(Config.active_column_color);
824 else if (w == TagTypes)
826 TagTypes->HighlightColor(Config.main_highlight_color);
827 w->Refresh();
828 w = LeftColumn;
829 LeftColumn->HighlightColor(Config.active_column_color);
831 else if (w == FParserHelper)
833 FParserHelper->SetBorder(Config.window_border);
834 FParserHelper->Display();
835 w = FParser;
836 FParser->SetBorder(Config.active_window_border);
837 FParser->Display();
841 void TagEditor::ReadTags(MPD::Song &s)
843 TagLib::FileRef f(s.GetFile().c_str());
844 if (f.isNull())
845 return;
847 TagLib::MPEG::File *mpegf = dynamic_cast<TagLib::MPEG::File *>(f.file());
849 s.SetArtist(f.tag()->artist().to8Bit(1));
850 s.SetTitle(f.tag()->title().to8Bit(1));
851 s.SetAlbum(f.tag()->album().to8Bit(1));
852 s.SetTrack(IntoStr(f.tag()->track()));
853 s.SetDate(IntoStr(f.tag()->year()));
854 s.SetGenre(f.tag()->genre().to8Bit(1));
855 if (mpegf)
857 s.SetAlbumArtist(!mpegf->ID3v2Tag()->frameListMap()["TPE2"].isEmpty() ? mpegf->ID3v2Tag()->frameListMap()["TPE2"].front()->toString().to8Bit(1) : "");
858 s.SetComposer(!mpegf->ID3v2Tag()->frameListMap()["TCOM"].isEmpty() ? mpegf->ID3v2Tag()->frameListMap()["TCOM"].front()->toString().to8Bit(1) : "");
859 s.SetPerformer(!mpegf->ID3v2Tag()->frameListMap()["TOPE"].isEmpty() ? mpegf->ID3v2Tag()->frameListMap()["TOPE"].front()->toString().to8Bit(1) : "");
860 s.SetDisc(!mpegf->ID3v2Tag()->frameListMap()["TPOS"].isEmpty() ? mpegf->ID3v2Tag()->frameListMap()["TPOS"].front()->toString().to8Bit(1) : "");
862 s.SetComment(f.tag()->comment().to8Bit(1));
865 namespace
867 template <typename T> void WriteID3v2(const TagLib::ByteVector &type, TagLib::ID3v2::Tag *tag, const T &list)
869 using TagLib::ID3v2::TextIdentificationFrame;
870 tag->removeFrames(type);
871 TextIdentificationFrame *frame = new TextIdentificationFrame(type, TagLib::String::UTF8);
872 frame->setText(list);
873 tag->addFrame(frame);
877 void TagEditor::WriteXiphComments(const MPD::Song &s, TagLib::Ogg::XiphComment *tag)
879 TagLib::StringList list;
881 tag->addField("DISCNUMBER", ToWString(s.GetDisc())); // disc
883 tag->removeField("ALBUM ARTIST"); // album artist
884 GetTagList(list, s, &MPD::Song::GetAlbumArtist);
885 for (TagLib::StringList::ConstIterator it = list.begin(); it != list.end(); ++it)
886 tag->addField("ALBUM ARTIST", *it, 0);
888 tag->removeField("COMPOSER"); // composer
889 GetTagList(list, s, &MPD::Song::GetComposer);
890 for (TagLib::StringList::ConstIterator it = list.begin(); it != list.end(); ++it)
891 tag->addField("COMPOSER", *it, 0);
893 tag->removeField("PERFORMER"); // performer
894 GetTagList(list, s, &MPD::Song::GetPerformer);
895 for (TagLib::StringList::ConstIterator it = list.begin(); it != list.end(); ++it)
896 tag->addField("PERFORMER", *it, 0);
899 bool TagEditor::WriteTags(MPD::Song &s)
901 std::string path_to_file;
902 bool file_is_from_db = s.isFromDB();
903 if (file_is_from_db)
904 path_to_file += Config.mpd_music_dir;
905 path_to_file += s.GetFile();
906 locale_to_utf(path_to_file);
907 TagLib::FileRef f(path_to_file.c_str());
908 if (!f.isNull())
910 f.tag()->setTitle(ToWString(s.GetTitle()));
911 f.tag()->setArtist(ToWString(s.GetArtist()));
912 f.tag()->setAlbum(ToWString(s.GetAlbum()));
913 f.tag()->setYear(StrToInt(s.GetDate()));
914 f.tag()->setTrack(StrToInt(s.GetTrack()));
915 f.tag()->setGenre(ToWString(s.GetGenre()));
916 f.tag()->setComment(ToWString(s.GetComment()));
917 if (TagLib::MPEG::File *mp3_file = dynamic_cast<TagLib::MPEG::File *>(f.file()))
919 TagLib::ID3v2::Tag *tag = mp3_file->ID3v2Tag(1);
920 TagLib::StringList list;
922 WriteID3v2("TIT2", tag, ToWString(s.GetTitle())); // title
923 WriteID3v2("TPE1", tag, ToWString(s.GetArtist())); // artist
924 WriteID3v2("TALB", tag, ToWString(s.GetAlbum())); // album
925 WriteID3v2("TDRC", tag, ToWString(s.GetDate())); // date
926 WriteID3v2("TRCK", tag, ToWString(s.GetTrack())); // track
927 WriteID3v2("TCON", tag, ToWString(s.GetGenre())); // genre
928 WriteID3v2("TPOS", tag, ToWString(s.GetDisc())); // disc
930 GetTagList(list, s, &MPD::Song::GetAlbumArtist);
931 WriteID3v2("TPE2", tag, list); // album artist
933 GetTagList(list, s, &MPD::Song::GetComposer);
934 WriteID3v2("TCOM", tag, list); // composer
936 GetTagList(list, s, &MPD::Song::GetPerformer);
937 // in >=mpd-0.16 treating TOPE frame as performer tag
938 // was dropped in favor of TPE3/TPE4 frames, so we have
939 // to write frame accurate to used mpd version
940 WriteID3v2(Mpd.Version() < 16 ? "TOPE" : "TPE3", tag, list); // performer
942 else if (TagLib::Ogg::Vorbis::File *ogg_file = dynamic_cast<TagLib::Ogg::Vorbis::File *>(f.file()))
944 WriteXiphComments(s, ogg_file->tag());
946 else if (TagLib::FLAC::File *flac_file = dynamic_cast<TagLib::FLAC::File *>(f.file()))
948 WriteXiphComments(s, flac_file->xiphComment(1));
950 if (!f.save())
951 return false;
953 if (!s.GetNewName().empty())
955 std::string new_name;
956 if (file_is_from_db)
957 new_name += Config.mpd_music_dir;
958 new_name += s.GetDirectory() + "/" + s.GetNewName();
959 locale_to_utf(new_name);
960 if (rename(path_to_file.c_str(), new_name.c_str()) == 0 && !file_is_from_db)
962 if (Global::myOldScreen == myPlaylist)
964 // if we rename local file, it won't get updated
965 // so just remove it from playlist and add again
966 size_t pos = myPlaylist->Items->Choice();
967 Mpd.StartCommandsList();
968 Mpd.Delete(pos);
969 int id = Mpd.AddSong("file://" + new_name);
970 if (id >= 0)
972 s = myPlaylist->Items->Back();
973 Mpd.Move(s.GetPosition(), pos);
975 Mpd.CommitCommandsList();
977 else // only myBrowser->Main()
978 myBrowser->GetDirectory(myBrowser->CurrentDir());
981 return true;
983 else
984 return false;
987 std::string TagEditor::CapitalizeFirstLetters(const std::string &s)
989 if (s.empty())
990 return "";
991 std::string result = s;
992 if (isalpha(result[0]))
993 result[0] = toupper(result[0]);
994 for (std::string::iterator it = result.begin()+1; it != result.end(); ++it)
996 if (isalpha(*it) && !isalpha(*(it-1)) && *(it-1) != '\'')
997 *it = toupper(*it);
999 return result;
1002 void TagEditor::CapitalizeFirstLetters(MPD::Song &s)
1004 for (const Info::Metadata *m = Info::Tags; m->Name; ++m)
1006 unsigned i = 0;
1007 for (std::string tag; !(tag = (s.*m->Get)(i)).empty(); ++i)
1008 (s.*m->Set)(CapitalizeFirstLetters(tag), i);
1012 void TagEditor::LowerAllLetters(MPD::Song &s)
1014 for (const Info::Metadata *m = Info::Tags; m->Name; ++m)
1016 unsigned i = 0;
1017 for (std::string tag; !(tag = (s.*m->Get)(i)).empty(); ++i)
1019 ToLower(tag);
1020 (s.*m->Set)(tag, i);
1025 void TagEditor::GetTagList(TagLib::StringList &list, const MPD::Song &s, MPD::Song::GetFunction f)
1027 list.clear();
1028 unsigned pos = 0;
1029 for (std::string value; !(value = (s.*f)(pos)).empty(); ++pos)
1030 list.append(ToWString(value));
1033 std::string TagEditor::TagToString(const MPD::Song &s, void *data)
1035 std::string result;
1036 size_t i = static_cast<Menu<std::string> *>(data)->Choice();
1037 if (i < 11)
1038 result = (s.*Info::Tags[i].Get)(0);
1039 else if (i == 12)
1040 result = s.GetNewName().empty() ? s.GetName() : s.GetName() + " -> " + s.GetNewName();
1041 return result.empty() ? Config.empty_tag : result;
1044 void TagEditor::GetPatternList()
1046 if (Patterns.empty())
1048 std::ifstream input(PatternsFile.c_str());
1049 if (input.is_open())
1051 std::string line;
1052 while (getline(input, line))
1053 if (!line.empty())
1054 Patterns.push_back(line);
1055 input.close();
1060 void TagEditor::SavePatternList()
1062 std::ofstream output(PatternsFile.c_str());
1063 if (output.is_open())
1065 std::list<std::string>::const_iterator it = Patterns.begin();
1066 for (unsigned i = 30; it != Patterns.end() && i; ++it, --i)
1067 output << *it << std::endl;
1068 output.close();
1072 MPD::Song::SetFunction TagEditor::IntoSetFunction(char c)
1074 switch (c)
1076 case 'a':
1077 return &MPD::Song::SetArtist;
1078 case 'A':
1079 return &MPD::Song::SetAlbumArtist;
1080 case 't':
1081 return &MPD::Song::SetTitle;
1082 case 'b':
1083 return &MPD::Song::SetAlbum;
1084 case 'y':
1085 return &MPD::Song::SetDate;
1086 case 'n':
1087 return &MPD::Song::SetTrack;
1088 case 'g':
1089 return &MPD::Song::SetGenre;
1090 case 'c':
1091 return &MPD::Song::SetComposer;
1092 case 'p':
1093 return &MPD::Song::SetPerformer;
1094 case 'd':
1095 return &MPD::Song::SetDisc;
1096 case 'C':
1097 return &MPD::Song::SetComment;
1098 default:
1099 return 0;
1103 std::string TagEditor::GenerateFilename(const MPD::Song &s, const std::string &pattern)
1105 std::string result = s.toString(pattern);
1106 EscapeUnallowedChars(result);
1107 return result;
1110 std::string TagEditor::ParseFilename(MPD::Song &s, std::string mask, bool preview)
1112 std::ostringstream result;
1113 std::vector<std::string> separators;
1114 std::vector< std::pair<char, std::string> > tags;
1115 std::string file = s.GetName().substr(0, s.GetName().rfind("."));
1117 for (size_t i = mask.find("%"); i != std::string::npos; i = mask.find("%"))
1119 tags.push_back(std::make_pair(mask.at(i+1), ""));
1120 mask = mask.substr(i+2);
1121 i = mask.find("%");
1122 if (!mask.empty())
1123 separators.push_back(mask.substr(0, i));
1125 size_t i = 0;
1126 for (std::vector<std::string>::const_iterator it = separators.begin(); it != separators.end(); ++it, ++i)
1128 size_t j = file.find(*it);
1129 tags.at(i).second = file.substr(0, j);
1130 if (j+it->length() > file.length())
1131 goto PARSE_FAILED;
1132 file = file.substr(j+it->length());
1134 if (!file.empty())
1136 if (i >= tags.size())
1137 goto PARSE_FAILED;
1138 tags.at(i).second = file;
1141 if (0) // tss...
1143 PARSE_FAILED:
1144 return "Error while parsing filename!\n";
1147 for (std::vector< std::pair<char, std::string> >::iterator it = tags.begin(); it != tags.end(); ++it)
1149 for (std::string::iterator j = it->second.begin(); j != it->second.end(); ++j)
1150 if (*j == '_')
1151 *j = ' ';
1153 if (!preview)
1155 MPD::Song::SetFunction set = IntoSetFunction(it->first);
1156 if (set)
1157 s.SetTags(set, it->second);
1159 else
1160 result << "%" << it->first << ": " << it->second << "\n";
1162 return result.str();
1165 #endif