center cursor directly in Menu class
[ncmpcpp.git] / src / tag_editor.cpp
blobad0997378560fbd0240626ce46f3a7495895cd03
1 /***************************************************************************
2 * Copyright (C) 2008-2010 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->CenteredCursor(Config.centered_cursor);
71 Albums->SetItemDisplayer(Display::Pairs);
72 Albums->SetGetStringFunction(StringPairToString);
74 Dirs = new Menu<string_pair>(0, MainStartY, LeftColumnWidth, MainHeight, "Directories", Config.main_color, brNone);
75 Dirs->HighlightColor(Config.active_column_color);
76 Dirs->CyclicScrolling(Config.use_cyclic_scrolling);
77 Dirs->CenteredCursor(Config.centered_cursor);
78 Dirs->SetItemDisplayer(Display::Pairs);
79 Dirs->SetGetStringFunction(StringPairToString);
81 LeftColumn = Config.albums_in_tag_editor ? Albums : Dirs;
83 TagTypes = new Menu<std::string>(MiddleColumnStartX, MainStartY, MiddleColumnWidth, MainHeight, "Tag types", Config.main_color, brNone);
84 TagTypes->HighlightColor(Config.main_highlight_color);
85 TagTypes->CyclicScrolling(Config.use_cyclic_scrolling);
86 TagTypes->CenteredCursor(Config.centered_cursor);
87 TagTypes->SetItemDisplayer(Display::Generic);
89 for (const Info::Metadata *m = Info::Tags; m->Name; ++m)
90 TagTypes->AddOption(m->Name);
91 TagTypes->AddSeparator();
92 TagTypes->AddOption("Filename");
93 TagTypes->AddSeparator();
94 TagTypes->AddOption("Options", 1, 1);
95 TagTypes->AddSeparator();
96 TagTypes->AddOption("Reset");
97 TagTypes->AddOption("Save");
98 TagTypes->AddSeparator();
99 TagTypes->AddOption("Capitalize First Letters");
100 TagTypes->AddOption("lower all letters");
102 Tags = new Menu<MPD::Song>(RightColumnStartX, MainStartY, RightColumnWidth, MainHeight, "Tags", Config.main_color, brNone);
103 Tags->HighlightColor(Config.main_highlight_color);
104 Tags->CyclicScrolling(Config.use_cyclic_scrolling);
105 Tags->CenteredCursor(Config.centered_cursor);
106 Tags->SetSelectPrefix(&Config.selected_item_prefix);
107 Tags->SetSelectSuffix(&Config.selected_item_suffix);
108 Tags->SetItemDisplayer(Display::Tags);
109 Tags->SetItemDisplayerUserData(TagTypes);
110 Tags->SetGetStringFunction(TagToString);
111 Tags->SetGetStringFunctionUserData(TagTypes);
113 FParserDialog = new Menu<std::string>((COLS-FParserDialogWidth)/2, (MainHeight-FParserDialogHeight)/2+MainStartY, FParserDialogWidth, FParserDialogHeight, "", Config.main_color, Config.window_border);
114 FParserDialog->CyclicScrolling(Config.use_cyclic_scrolling);
115 FParserDialog->CenteredCursor(Config.centered_cursor);
116 FParserDialog->SetItemDisplayer(Display::Generic);
117 FParserDialog->AddOption("Get tags from filename");
118 FParserDialog->AddOption("Rename files");
119 FParserDialog->AddSeparator();
120 FParserDialog->AddOption("Cancel");
122 FParser = new Menu<std::string>((COLS-FParserWidth)/2, (MainHeight-FParserHeight)/2+MainStartY, FParserWidthOne, FParserHeight, "_", Config.main_color, Config.active_window_border);
123 FParser->CyclicScrolling(Config.use_cyclic_scrolling);
124 FParser->CenteredCursor(Config.centered_cursor);
125 FParser->SetItemDisplayer(Display::Generic);
127 FParserLegend = new Scrollpad((COLS-FParserWidth)/2+FParserWidthOne, (MainHeight-FParserHeight)/2+MainStartY, FParserWidthTwo, FParserHeight, "Legend", Config.main_color, Config.window_border);
129 FParserPreview = new Scrollpad((COLS-FParserWidth)/2+FParserWidthOne, (MainHeight-FParserHeight)/2+MainStartY, FParserWidthTwo, FParserHeight, "Preview", Config.main_color, Config.window_border);
131 w = LeftColumn;
132 isInitialized = 1;
135 void TagEditor::SetDimensions()
137 MiddleColumnWidth = std::min(26, COLS-2);
138 LeftColumnWidth = (COLS-MiddleColumnWidth)/2;
139 MiddleColumnStartX = LeftColumnWidth+1;
140 RightColumnWidth = COLS-LeftColumnWidth-MiddleColumnWidth-2;
141 RightColumnStartX = LeftColumnWidth+MiddleColumnWidth+2;
143 FParserDialogWidth = std::min(30, COLS);
144 FParserDialogHeight = std::min(size_t(6), MainHeight);
145 FParserWidth = COLS*0.9;
146 FParserHeight = std::min(size_t(LINES*0.8), MainHeight);
147 FParserWidthOne = FParserWidth/2;
148 FParserWidthTwo = FParserWidth-FParserWidthOne;
151 void TagEditor::Resize()
153 SetDimensions();
155 Albums->Resize(LeftColumnWidth, MainHeight);
156 Dirs->Resize(LeftColumnWidth, MainHeight);
157 TagTypes->Resize(MiddleColumnWidth, MainHeight);
158 Tags->Resize(RightColumnWidth, MainHeight);
159 FParserDialog->Resize(FParserDialogWidth, FParserDialogHeight);
160 FParser->Resize(FParserWidthOne, FParserHeight);
161 FParserLegend->Resize(FParserWidthTwo, FParserHeight);
162 FParserPreview->Resize(FParserWidthTwo, FParserHeight);
164 Albums->MoveTo(0, MainStartY);
165 Dirs->MoveTo(0, MainStartY);
166 TagTypes->MoveTo(MiddleColumnStartX, MainStartY);
167 Tags->MoveTo(RightColumnStartX, MainStartY);
169 FParserDialog->MoveTo((COLS-FParserDialogWidth)/2, (MainHeight-FParserDialogHeight)/2+MainStartY);
170 FParser->MoveTo((COLS-FParserWidth)/2, (MainHeight-FParserHeight)/2+MainStartY);
171 FParserLegend->MoveTo((COLS-FParserWidth)/2+FParserWidthOne, (MainHeight-FParserHeight)/2+MainStartY);
172 FParserPreview->MoveTo((COLS-FParserWidth)/2+FParserWidthOne, (MainHeight-FParserHeight)/2+MainStartY);
174 if (MainHeight < 5 && (w == FParserDialog || w == FParser || w == FParserHelper)) // screen too low
175 w = TagTypes; // fall back to main columns
177 hasToBeResized = 0;
180 std::basic_string<my_char_t> TagEditor::Title()
182 return U("Tag editor");
185 void TagEditor::SwitchTo()
187 using Global::myScreen;
189 if (myScreen == this)
190 return;
192 if (!isInitialized)
193 Init();
195 if (hasToBeResized)
196 Resize();
198 if (myScreen != this && myScreen->isTabbable())
199 Global::myPrevScreen = myScreen;
200 myScreen = this;
201 Global::RedrawHeader = 1;
202 Refresh();
205 void TagEditor::Refresh()
207 LeftColumn->Display();
208 mvvline(MainStartY, MiddleColumnStartX-1, 0, MainHeight);
209 TagTypes->Display();
210 mvvline(MainStartY, RightColumnStartX-1, 0, MainHeight);
211 Tags->Display();
213 if (w == FParserDialog)
215 FParserDialog->Display();
217 else if (w == FParser || w == FParserHelper)
219 FParser->Display();
220 FParserHelper->Display();
224 void TagEditor::Update()
226 if (LeftColumn->Empty())
228 LeftColumn->Window::Clear();
229 Tags->Clear();
230 MPD::TagList list;
231 if (Config.albums_in_tag_editor)
233 *Albums << XY(0, 0) << "Fetching albums...";
234 Albums->Window::Refresh();
235 Mpd.GetList(list, MPD_TAG_ALBUM);
236 for (MPD::TagList::const_iterator it = list.begin(); it != list.end(); ++it)
238 MPD::SongList l;
239 Mpd.StartSearch(1);
240 Mpd.AddSearch(MPD_TAG_ALBUM, *it);
241 Mpd.CommitSearch(l);
242 if (!l.empty())
244 l[0]->Localize();
245 Albums->AddOption(std::make_pair(l[0]->toString(Config.tag_editor_album_format), *it));
247 MPD::FreeSongList(l);
249 Albums->Sort<CaseInsensitiveSorting>();
251 else
253 int highlightme = -1;
254 Mpd.GetDirectories(itsBrowsedDir, list);
255 sort(list.begin(), list.end(), CaseInsensitiveSorting());
256 if (itsBrowsedDir != "/")
258 size_t slash = itsBrowsedDir.rfind("/");
259 std::string parent = slash != std::string::npos ? itsBrowsedDir.substr(0, slash) : "/";
260 Dirs->AddOption(make_pair("[..]", parent));
262 else
264 Dirs->AddOption(std::make_pair(".", "/"));
266 for (MPD::TagList::const_iterator it = list.begin(); it != list.end(); ++it)
268 size_t slash = it->rfind("/");
269 std::string to_display = slash != std::string::npos ? it->substr(slash+1) : *it;
270 utf_to_locale(to_display);
271 Dirs->AddOption(make_pair(to_display, *it));
272 if (*it == itsHighlightedDir)
273 highlightme = Dirs->Size()-1;
275 if (highlightme != -1)
276 Dirs->Highlight(highlightme);
278 LeftColumn->Display();
279 TagTypes->Refresh();
282 if (Tags->Empty())
284 Tags->Reset();
285 MPD::SongList list;
286 if (Config.albums_in_tag_editor)
288 Mpd.StartSearch(1);
289 Mpd.AddSearch(MPD_TAG_ALBUM, Albums->Current().second);
290 Mpd.CommitSearch(list);
291 sort(list.begin(), list.end(), CaseInsensitiveSorting());
292 for (MPD::SongList::iterator it = list.begin(); it != list.end(); ++it)
294 (*it)->Localize();
295 Tags->AddOption(**it);
298 else
300 Mpd.GetSongs(Dirs->Current().second, list);
301 sort(list.begin(), list.end(), CaseInsensitiveSorting());
302 for (MPD::SongList::const_iterator it = list.begin(); it != list.end(); ++it)
304 (*it)->Localize();
305 Tags->AddOption(**it);
308 MPD::FreeSongList(list);
309 Tags->Window::Clear();
310 Tags->Refresh();
313 if (w == TagTypes && TagTypes->Choice() < 13)
315 Tags->Refresh();
317 else if (TagTypes->Choice() >= 13)
319 Tags->Window::Clear();
320 Tags->Window::Refresh();
324 void TagEditor::EnterPressed()
326 using Global::wFooter;
328 if (w == Dirs)
330 MPD::TagList test;
331 Mpd.GetDirectories(LeftColumn->Current().second, test);
332 if (!test.empty())
334 itsHighlightedDir = itsBrowsedDir;
335 itsBrowsedDir = LeftColumn->Current().second;
336 LeftColumn->Clear();
337 LeftColumn->Reset();
339 else
340 ShowMessage("No subdirs found");
342 else if (w == FParserDialog)
344 size_t choice = FParserDialog->RealChoice();
345 if (choice == 2) // cancel
347 w = TagTypes;
348 Refresh();
349 return;
351 GetPatternList();
353 // prepare additional windows
355 FParserLegend->Clear();
356 *FParserLegend << "%a - artist\n";
357 *FParserLegend << "%A - album artist\n";
358 *FParserLegend << "%t - title\n";
359 *FParserLegend << "%b - album\n";
360 *FParserLegend << "%y - year\n";
361 *FParserLegend << "%n - track number\n";
362 *FParserLegend << "%g - genre\n";
363 *FParserLegend << "%c - composer\n";
364 *FParserLegend << "%p - performer\n";
365 *FParserLegend << "%d - disc\n";
366 *FParserLegend << "%C - comment\n\n";
367 *FParserLegend << fmtBold << "Files:\n" << fmtBoldEnd;
368 for (MPD::SongList::const_iterator it = EditedSongs.begin(); it != EditedSongs.end(); ++it)
369 *FParserLegend << Config.color2 << " * " << clEnd << (*it)->GetName() << "\n";
370 FParserLegend->Flush();
372 if (!Patterns.empty())
373 Config.pattern = Patterns.front();
374 FParser->Clear();
375 FParser->Reset();
376 FParser->AddOption("Pattern: " + Config.pattern);
377 FParser->AddOption("Preview");
378 FParser->AddOption("Legend");
379 FParser->AddSeparator();
380 FParser->AddOption("Proceed");
381 FParser->AddOption("Cancel");
382 if (!Patterns.empty())
384 FParser->AddSeparator();
385 FParser->AddOption("Recent patterns", 1, 1);
386 FParser->AddSeparator();
387 for (std::list<std::string>::const_iterator it = Patterns.begin(); it != Patterns.end(); ++it)
388 FParser->AddOption(*it);
391 FParser->SetTitle(choice == 0 ? "Get tags from filename" : "Rename files");
392 w = FParser;
393 FParserUsePreview = 1;
394 FParserHelper = FParserLegend;
395 FParserHelper->Display();
397 else if (w == FParser)
399 bool quit = 0;
400 size_t pos = FParser->RealChoice();
402 if (pos == 3) // save
403 FParserUsePreview = 0;
405 if (pos == 0) // change pattern
407 LockStatusbar();
408 Statusbar() << "Pattern: ";
409 std::string new_pattern = wFooter->GetString(Config.pattern);
410 UnlockStatusbar();
411 if (!new_pattern.empty())
413 Config.pattern = new_pattern;
414 FParser->at(0) = "Pattern: ";
415 FParser->at(0) += Config.pattern;
418 else if (pos == 1 || pos == 3) // preview or proceed
420 bool success = 1;
421 ShowMessage("Parsing...");
422 FParserPreview->Clear();
423 for (MPD::SongList::iterator it = EditedSongs.begin(); it != EditedSongs.end(); ++it)
425 MPD::Song &s = **it;
426 if (FParserDialog->Choice() == 0) // get tags from filename
428 if (FParserUsePreview)
430 *FParserPreview << fmtBold << s.GetName() << ":\n" << fmtBoldEnd;
431 *FParserPreview << ParseFilename(s, Config.pattern, FParserUsePreview) << "\n";
433 else
434 ParseFilename(s, Config.pattern, FParserUsePreview);
436 else // rename files
438 std::string file = s.GetName();
439 size_t last_dot = file.rfind(".");
440 std::string extension = file.substr(last_dot);
441 std::string new_file = GenerateFilename(s, "{" + Config.pattern + "}");
442 if (new_file.empty() && !FParserUsePreview)
444 ShowMessage("File \"%s\" would have an empty name!", s.GetName().c_str());
445 FParserUsePreview = 1;
446 success = 0;
448 if (!FParserUsePreview)
449 s.SetNewName(new_file + extension);
450 *FParserPreview << file << Config.color2 << " -> " << clEnd;
451 if (new_file.empty())
452 *FParserPreview << Config.empty_tags_color << Config.empty_tag << clEnd;
453 else
454 *FParserPreview << new_file << extension;
455 *FParserPreview << "\n\n";
456 if (!success)
457 break;
460 if (FParserUsePreview)
462 FParserHelper = FParserPreview;
463 FParserHelper->Flush();
464 FParserHelper->Display();
466 else if (success)
468 Patterns.remove(Config.pattern);
469 Patterns.insert(Patterns.begin(), Config.pattern);
470 quit = 1;
472 if (pos != 3 || success)
473 ShowMessage("Operation finished!");
475 else if (pos == 2) // show legend
477 FParserHelper = FParserLegend;
478 FParserHelper->Display();
480 else if (pos == 4) // cancel
482 quit = 1;
484 else // list of patterns
486 Config.pattern = FParser->Current();
487 FParser->at(0) = "Pattern: " + Config.pattern;
490 if (quit)
492 SavePatternList();
493 w = TagTypes;
494 Refresh();
495 return;
499 if ((w != TagTypes && w != Tags) || Tags->Empty()) // after this point we start dealing with tags
500 return;
502 EditedSongs.clear();
503 if (Tags->hasSelected()) // if there are selected songs, perform operations only on them
505 std::vector<size_t> selected;
506 Tags->GetSelected(selected);
507 for (std::vector<size_t>::const_iterator it = selected.begin(); it != selected.end(); ++it)
508 EditedSongs.push_back(&(*Tags)[*it]);
510 else
511 for (size_t i = 0; i < Tags->Size(); ++i)
512 EditedSongs.push_back(&(*Tags)[i]);
514 size_t id = TagTypes->RealChoice();
516 if (w == TagTypes && id == 5)
518 LockStatusbar();
519 Statusbar() << "Number tracks? [" << fmtBold << 'y' << fmtBoldEnd << '/' << fmtBold << 'n' << fmtBoldEnd << "] ";
520 wFooter->Refresh();
521 int in = 0;
524 TraceMpdStatus();
525 wFooter->ReadKey(in);
527 while (in != 'y' && in != 'n');
528 UnlockStatusbar();
529 if (in == 'y')
531 MPD::SongList::iterator it = EditedSongs.begin();
532 for (unsigned i = 1; i <= EditedSongs.size(); ++i, ++it)
534 if (Config.tag_editor_extended_numeration)
535 (*it)->SetTrack(IntoStr(i) + "/" + IntoStr(EditedSongs.size()));
536 else
537 (*it)->SetTrack(i);
539 ShowMessage("Tracks numbered!");
541 else
542 ShowMessage("Aborted!");
543 return;
546 if (id < 11)
548 MPD::Song::GetFunction get = Info::Tags[id].Get;
549 MPD::Song::SetFunction set = Info::Tags[id].Set;
550 if (id > 0 && w == TagTypes)
552 LockStatusbar();
553 Statusbar() << fmtBold << TagTypes->Current() << fmtBoldEnd << ": ";
554 std::string new_tag = wFooter->GetString(Tags->Current().GetTags(get));
555 UnlockStatusbar();
556 for (MPD::SongList::iterator it = EditedSongs.begin(); it != EditedSongs.end(); ++it)
557 (*it)->SetTags(set, new_tag);
559 else if (w == Tags)
561 LockStatusbar();
562 Statusbar() << fmtBold << TagTypes->Current() << fmtBoldEnd << ": ";
563 std::string new_tag = wFooter->GetString(Tags->Current().GetTags(get));
564 UnlockStatusbar();
565 if (new_tag != Tags->Current().GetTags(get))
566 Tags->Current().SetTags(set, new_tag);
567 Tags->Scroll(wDown);
570 else
572 if (id == 11) // filename related options
574 if (w == TagTypes)
576 if (size_t(COLS) < FParserDialogWidth || MainHeight < FParserDialogHeight)
578 ShowMessage("Screen is too small to display additional windows!");
579 return;
581 FParserDialog->Reset();
582 w = FParserDialog;
584 else if (w == Tags)
586 MPD::Song &s = Tags->Current();
587 std::string old_name = s.GetNewName().empty() ? s.GetName() : s.GetNewName();
588 size_t last_dot = old_name.rfind(".");
589 std::string extension = old_name.substr(last_dot);
590 old_name = old_name.substr(0, last_dot);
591 LockStatusbar();
592 Statusbar() << fmtBold << "New filename: " << fmtBoldEnd;
593 std::string new_name = wFooter->GetString(old_name);
594 UnlockStatusbar();
595 if (!new_name.empty() && new_name != old_name)
596 s.SetNewName(new_name + extension);
597 Tags->Scroll(wDown);
600 else if (id == 12) // reset
602 Tags->Clear();
603 ShowMessage("Changes reset");
605 else if (id == 13) // save
607 bool success = 1;
608 ShowMessage("Writing changes...");
609 for (MPD::SongList::iterator it = EditedSongs.begin(); it != EditedSongs.end(); ++it)
611 ShowMessage("Writing tags in \"%s\"...", (*it)->GetName().c_str());
612 if (!WriteTags(**it))
614 static const char msg[] = "Error while writing tags in \"%s\"!";
615 ShowMessage(msg, Shorten(TO_WSTRING((*it)->GetFile()), COLS-static_strlen(msg)).c_str());
616 success = 0;
617 break;
620 if (success)
622 ShowMessage("Tags updated!");
623 TagTypes->HighlightColor(Config.main_highlight_color);
624 TagTypes->Reset();
625 w->Refresh();
626 w = LeftColumn;
627 LeftColumn->HighlightColor(Config.active_column_color);
628 Mpd.UpdateDirectory(locale_to_utf_cpy(FindSharedDir(Tags)));
630 else
631 Tags->Clear();
633 else if (id == 14) // capitalize first letters
635 ShowMessage("Processing...");
636 for (MPD::SongList::iterator it = EditedSongs.begin(); it != EditedSongs.end(); ++it)
637 CapitalizeFirstLetters(**it);
638 ShowMessage("Done!");
640 else if (id == 15) // lower all letters
642 ShowMessage("Processing...");
643 for (MPD::SongList::iterator it = EditedSongs.begin(); it != EditedSongs.end(); ++it)
644 LowerAllLetters(**it);
645 ShowMessage("Done!");
650 void TagEditor::SpacePressed()
652 if (w == Tags)
654 Tags->Select(Tags->Choice(), !Tags->isSelected());
655 w->Scroll(wDown);
656 return;
658 if (w != LeftColumn)
659 return;
661 Config.albums_in_tag_editor = !Config.albums_in_tag_editor;
662 w = LeftColumn = Config.albums_in_tag_editor ? Albums : Dirs;
663 ShowMessage("Switched to %s view", Config.albums_in_tag_editor ? "albums" : "directories");
664 LeftColumn->Display();
665 Tags->Clear();
668 void TagEditor::MouseButtonPressed(MEVENT me)
670 if (w == FParserDialog)
672 if (FParserDialog->hasCoords(me.x, me.y))
674 if (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED))
676 FParserDialog->Goto(me.y);
677 if (me.bstate & BUTTON3_PRESSED)
678 EnterPressed();
680 else
681 Screen<Window>::MouseButtonPressed(me);
684 else if (w == FParser || w == FParserHelper)
686 if (FParser->hasCoords(me.x, me.y))
688 if (w != FParser)
689 PrevColumn();
690 if (size_t(me.y) < FParser->Size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)))
692 FParser->Goto(me.y);
693 if (me.bstate & BUTTON3_PRESSED)
694 EnterPressed();
696 else
697 Screen<Window>::MouseButtonPressed(me);
699 else if (FParserHelper->hasCoords(me.x, me.y))
701 if (w != FParserHelper)
702 NextColumn();
703 reinterpret_cast<Screen<Scrollpad> *>(this)->Screen<Scrollpad>::MouseButtonPressed(me);
706 else if (!LeftColumn->Empty() && LeftColumn->hasCoords(me.x, me.y))
708 if (w != LeftColumn)
710 PrevColumn();
711 PrevColumn();
713 if (size_t(me.y) < LeftColumn->Size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)))
715 LeftColumn->Goto(me.y);
716 if (me.bstate & BUTTON1_PRESSED)
717 EnterPressed();
718 else
719 SpacePressed();
721 else
722 Screen<Window>::MouseButtonPressed(me);
723 Tags->Clear();
725 else if (!TagTypes->Empty() && TagTypes->hasCoords(me.x, me.y))
727 if (w != TagTypes)
728 w == LeftColumn ? NextColumn() : PrevColumn();
729 if (size_t(me.y) < TagTypes->Size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)))
731 if (!TagTypes->Goto(me.y))
732 return;
733 TagTypes->Refresh();
734 Tags->Refresh();
735 if (me.bstate & BUTTON3_PRESSED)
736 EnterPressed();
738 else
739 Screen<Window>::MouseButtonPressed(me);
741 else if (!Tags->Empty() && Tags->hasCoords(me.x, me.y))
743 if (w != Tags)
745 NextColumn();
746 NextColumn();
748 if (size_t(me.y) < Tags->Size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)))
750 Tags->Goto(me.y);
751 Tags->Refresh();
752 if (me.bstate & BUTTON3_PRESSED)
753 EnterPressed();
755 else
756 Screen<Window>::MouseButtonPressed(me);
760 MPD::Song *TagEditor::CurrentSong()
762 return w == Tags && !Tags->Empty() ? &Tags->Current() : 0;
765 void TagEditor::GetSelectedSongs(MPD::SongList &v)
767 std::vector<size_t> selected;
768 Tags->GetSelected(selected);
769 if (selected.empty())
770 selected.push_back(Tags->Choice());
771 for (std::vector<size_t>::const_iterator it = selected.begin(); it != selected.end(); ++it)
772 v.push_back(new MPD::Song(Tags->at(*it)));
775 void TagEditor::ApplyFilter(const std::string &s)
777 if (w == Dirs)
778 Dirs->ApplyFilter(s, 1, REG_ICASE | Config.regex_type);
779 else if (w == Albums)
780 Albums->ApplyFilter(s, 0, REG_ICASE | Config.regex_type);
781 else if (w == Tags)
782 Tags->ApplyFilter(s, 0, REG_ICASE | Config.regex_type);
785 List *TagEditor::GetList()
787 if (w == LeftColumn)
788 return LeftColumn;
789 else if (w == Tags)
790 return Tags;
791 else
792 return 0;
795 void TagEditor::NextColumn()
797 if (w == LeftColumn)
799 LeftColumn->HighlightColor(Config.main_highlight_color);
800 w->Refresh();
801 w = TagTypes;
802 TagTypes->HighlightColor(Config.active_column_color);
804 else if (w == TagTypes && TagTypes->Choice() < 12 && !Tags->Empty())
806 TagTypes->HighlightColor(Config.main_highlight_color);
807 w->Refresh();
808 w = Tags;
809 Tags->HighlightColor(Config.active_column_color);
811 else if (w == FParser)
813 FParser->SetBorder(Config.window_border);
814 FParser->Display();
815 w = FParserHelper;
816 FParserHelper->SetBorder(Config.active_window_border);
817 FParserHelper->Display();
821 void TagEditor::PrevColumn()
823 if (w == Tags)
825 Tags->HighlightColor(Config.main_highlight_color);
826 w->Refresh();
827 w = TagTypes;
828 TagTypes->HighlightColor(Config.active_column_color);
830 else if (w == TagTypes)
832 TagTypes->HighlightColor(Config.main_highlight_color);
833 w->Refresh();
834 w = LeftColumn;
835 LeftColumn->HighlightColor(Config.active_column_color);
837 else if (w == FParserHelper)
839 FParserHelper->SetBorder(Config.window_border);
840 FParserHelper->Display();
841 w = FParser;
842 FParser->SetBorder(Config.active_window_border);
843 FParser->Display();
847 void TagEditor::ReadTags(MPD::Song &s)
849 TagLib::FileRef f(s.GetFile().c_str());
850 if (f.isNull())
851 return;
853 TagLib::MPEG::File *mpegf = dynamic_cast<TagLib::MPEG::File *>(f.file());
855 s.SetArtist(f.tag()->artist().to8Bit(1));
856 s.SetTitle(f.tag()->title().to8Bit(1));
857 s.SetAlbum(f.tag()->album().to8Bit(1));
858 s.SetTrack(IntoStr(f.tag()->track()));
859 s.SetDate(IntoStr(f.tag()->year()));
860 s.SetGenre(f.tag()->genre().to8Bit(1));
861 if (mpegf)
863 s.SetAlbumArtist(!mpegf->ID3v2Tag()->frameListMap()["TPE2"].isEmpty() ? mpegf->ID3v2Tag()->frameListMap()["TPE2"].front()->toString().to8Bit(1) : "");
864 s.SetComposer(!mpegf->ID3v2Tag()->frameListMap()["TCOM"].isEmpty() ? mpegf->ID3v2Tag()->frameListMap()["TCOM"].front()->toString().to8Bit(1) : "");
865 s.SetPerformer(!mpegf->ID3v2Tag()->frameListMap()["TOPE"].isEmpty() ? mpegf->ID3v2Tag()->frameListMap()["TOPE"].front()->toString().to8Bit(1) : "");
866 s.SetDisc(!mpegf->ID3v2Tag()->frameListMap()["TPOS"].isEmpty() ? mpegf->ID3v2Tag()->frameListMap()["TPOS"].front()->toString().to8Bit(1) : "");
868 s.SetComment(f.tag()->comment().to8Bit(1));
871 namespace
873 template <typename T> void WriteID3v2(const TagLib::ByteVector &type, TagLib::ID3v2::Tag *tag, const T &list)
875 using TagLib::ID3v2::TextIdentificationFrame;
876 tag->removeFrames(type);
877 TextIdentificationFrame *frame = new TextIdentificationFrame(type, TagLib::String::UTF8);
878 frame->setText(list);
879 tag->addFrame(frame);
883 void TagEditor::WriteXiphComments(const MPD::Song &s, TagLib::Ogg::XiphComment *tag)
885 TagLib::StringList list;
887 tag->addField("DISCNUMBER", ToWString(s.GetDisc())); // disc
889 tag->removeField("ALBUM ARTIST"); // album artist
890 GetTagList(list, s, &MPD::Song::GetAlbumArtist);
891 for (TagLib::StringList::ConstIterator it = list.begin(); it != list.end(); ++it)
892 tag->addField("ALBUM ARTIST", *it, 0);
894 tag->removeField("COMPOSER"); // composer
895 GetTagList(list, s, &MPD::Song::GetComposer);
896 for (TagLib::StringList::ConstIterator it = list.begin(); it != list.end(); ++it)
897 tag->addField("COMPOSER", *it, 0);
899 tag->removeField("PERFORMER"); // performer
900 GetTagList(list, s, &MPD::Song::GetPerformer);
901 for (TagLib::StringList::ConstIterator it = list.begin(); it != list.end(); ++it)
902 tag->addField("PERFORMER", *it, 0);
905 bool TagEditor::WriteTags(MPD::Song &s)
907 std::string path_to_file;
908 bool file_is_from_db = s.isFromDB();
909 if (file_is_from_db)
910 path_to_file += Config.mpd_music_dir;
911 path_to_file += s.GetFile();
912 locale_to_utf(path_to_file);
913 TagLib::FileRef f(path_to_file.c_str());
914 if (!f.isNull())
916 f.tag()->setTitle(ToWString(s.GetTitle()));
917 f.tag()->setArtist(ToWString(s.GetArtist()));
918 f.tag()->setAlbum(ToWString(s.GetAlbum()));
919 f.tag()->setYear(StrToInt(s.GetDate()));
920 f.tag()->setTrack(StrToInt(s.GetTrack()));
921 f.tag()->setGenre(ToWString(s.GetGenre()));
922 f.tag()->setComment(ToWString(s.GetComment()));
923 if (TagLib::MPEG::File *mp3_file = dynamic_cast<TagLib::MPEG::File *>(f.file()))
925 TagLib::ID3v2::Tag *tag = mp3_file->ID3v2Tag(1);
926 TagLib::StringList list;
928 WriteID3v2("TIT2", tag, ToWString(s.GetTitle())); // title
929 WriteID3v2("TPE1", tag, ToWString(s.GetArtist())); // artist
930 WriteID3v2("TALB", tag, ToWString(s.GetAlbum())); // album
931 WriteID3v2("TDRC", tag, ToWString(s.GetDate())); // date
932 WriteID3v2("TRCK", tag, ToWString(s.GetTrack())); // track
933 WriteID3v2("TCON", tag, ToWString(s.GetGenre())); // genre
934 WriteID3v2("TPOS", tag, ToWString(s.GetDisc())); // disc
936 GetTagList(list, s, &MPD::Song::GetAlbumArtist);
937 WriteID3v2("TPE2", tag, list); // album artist
939 GetTagList(list, s, &MPD::Song::GetComposer);
940 WriteID3v2("TCOM", tag, list); // composer
942 GetTagList(list, s, &MPD::Song::GetPerformer);
943 // in >=mpd-0.16 treating TOPE frame as performer tag
944 // was dropped in favor of TPE3/TPE4 frames, so we have
945 // to write frame accurate to used mpd version
946 WriteID3v2(Mpd.Version() < 16 ? "TOPE" : "TPE3", tag, list); // performer
948 else if (TagLib::Ogg::Vorbis::File *ogg_file = dynamic_cast<TagLib::Ogg::Vorbis::File *>(f.file()))
950 WriteXiphComments(s, ogg_file->tag());
952 else if (TagLib::FLAC::File *flac_file = dynamic_cast<TagLib::FLAC::File *>(f.file()))
954 WriteXiphComments(s, flac_file->xiphComment(1));
956 if (!f.save())
957 return false;
959 if (!s.GetNewName().empty())
961 std::string new_name;
962 if (file_is_from_db)
963 new_name += Config.mpd_music_dir;
964 new_name += s.GetDirectory() + "/" + s.GetNewName();
965 locale_to_utf(new_name);
966 if (rename(path_to_file.c_str(), new_name.c_str()) == 0 && !file_is_from_db)
968 if (Global::myOldScreen == myPlaylist)
970 // if we rename local file, it won't get updated
971 // so just remove it from playlist and add again
972 size_t pos = myPlaylist->Items->Choice();
973 Mpd.StartCommandsList();
974 Mpd.Delete(pos);
975 int id = Mpd.AddSong("file://" + new_name);
976 if (id >= 0)
978 s = myPlaylist->Items->Back();
979 Mpd.Move(s.GetPosition(), pos);
981 Mpd.CommitCommandsList();
983 else // only myBrowser->Main()
984 myBrowser->GetDirectory(myBrowser->CurrentDir());
987 return true;
989 else
990 return false;
993 std::string TagEditor::CapitalizeFirstLetters(const std::string &s)
995 if (s.empty())
996 return "";
997 std::string result = s;
998 if (isalpha(result[0]))
999 result[0] = toupper(result[0]);
1000 for (std::string::iterator it = result.begin()+1; it != result.end(); ++it)
1002 if (isalpha(*it) && !isalpha(*(it-1)) && *(it-1) != '\'')
1003 *it = toupper(*it);
1005 return result;
1008 void TagEditor::CapitalizeFirstLetters(MPD::Song &s)
1010 for (const Info::Metadata *m = Info::Tags; m->Name; ++m)
1012 unsigned i = 0;
1013 for (std::string tag; !(tag = (s.*m->Get)(i)).empty(); ++i)
1014 (s.*m->Set)(CapitalizeFirstLetters(tag), i);
1018 void TagEditor::LowerAllLetters(MPD::Song &s)
1020 for (const Info::Metadata *m = Info::Tags; m->Name; ++m)
1022 unsigned i = 0;
1023 for (std::string tag; !(tag = (s.*m->Get)(i)).empty(); ++i)
1025 ToLower(tag);
1026 (s.*m->Set)(tag, i);
1031 void TagEditor::GetTagList(TagLib::StringList &list, const MPD::Song &s, MPD::Song::GetFunction f)
1033 list.clear();
1034 unsigned pos = 0;
1035 for (std::string value; !(value = (s.*f)(pos)).empty(); ++pos)
1036 list.append(ToWString(value));
1039 std::string TagEditor::TagToString(const MPD::Song &s, void *data)
1041 std::string result;
1042 size_t i = static_cast<Menu<std::string> *>(data)->Choice();
1043 if (i < 11)
1044 result = (s.*Info::Tags[i].Get)(0);
1045 else if (i == 12)
1046 result = s.GetNewName().empty() ? s.GetName() : s.GetName() + " -> " + s.GetNewName();
1047 return result.empty() ? Config.empty_tag : result;
1050 void TagEditor::GetPatternList()
1052 if (Patterns.empty())
1054 std::ifstream input(PatternsFile.c_str());
1055 if (input.is_open())
1057 std::string line;
1058 while (getline(input, line))
1059 if (!line.empty())
1060 Patterns.push_back(line);
1061 input.close();
1066 void TagEditor::SavePatternList()
1068 std::ofstream output(PatternsFile.c_str());
1069 if (output.is_open())
1071 std::list<std::string>::const_iterator it = Patterns.begin();
1072 for (unsigned i = 30; it != Patterns.end() && i; ++it, --i)
1073 output << *it << std::endl;
1074 output.close();
1078 MPD::Song::SetFunction TagEditor::IntoSetFunction(char c)
1080 switch (c)
1082 case 'a':
1083 return &MPD::Song::SetArtist;
1084 case 'A':
1085 return &MPD::Song::SetAlbumArtist;
1086 case 't':
1087 return &MPD::Song::SetTitle;
1088 case 'b':
1089 return &MPD::Song::SetAlbum;
1090 case 'y':
1091 return &MPD::Song::SetDate;
1092 case 'n':
1093 return &MPD::Song::SetTrack;
1094 case 'g':
1095 return &MPD::Song::SetGenre;
1096 case 'c':
1097 return &MPD::Song::SetComposer;
1098 case 'p':
1099 return &MPD::Song::SetPerformer;
1100 case 'd':
1101 return &MPD::Song::SetDisc;
1102 case 'C':
1103 return &MPD::Song::SetComment;
1104 default:
1105 return 0;
1109 std::string TagEditor::GenerateFilename(const MPD::Song &s, const std::string &pattern)
1111 std::string result = s.toString(pattern);
1112 EscapeUnallowedChars(result);
1113 return result;
1116 std::string TagEditor::ParseFilename(MPD::Song &s, std::string mask, bool preview)
1118 std::ostringstream result;
1119 std::vector<std::string> separators;
1120 std::vector< std::pair<char, std::string> > tags;
1121 std::string file = s.GetName().substr(0, s.GetName().rfind("."));
1123 for (size_t i = mask.find("%"); i != std::string::npos; i = mask.find("%"))
1125 tags.push_back(std::make_pair(mask.at(i+1), ""));
1126 mask = mask.substr(i+2);
1127 i = mask.find("%");
1128 if (!mask.empty())
1129 separators.push_back(mask.substr(0, i));
1131 size_t i = 0;
1132 for (std::vector<std::string>::const_iterator it = separators.begin(); it != separators.end(); ++it, ++i)
1134 size_t j = file.find(*it);
1135 tags.at(i).second = file.substr(0, j);
1136 if (j+it->length() > file.length())
1137 goto PARSE_FAILED;
1138 file = file.substr(j+it->length());
1140 if (!file.empty())
1142 if (i >= tags.size())
1143 goto PARSE_FAILED;
1144 tags.at(i).second = file;
1147 if (0) // tss...
1149 PARSE_FAILED:
1150 return "Error while parsing filename!\n";
1153 for (std::vector< std::pair<char, std::string> >::iterator it = tags.begin(); it != tags.end(); ++it)
1155 for (std::string::iterator j = it->second.begin(); j != it->second.end(); ++j)
1156 if (*j == '_')
1157 *j = ' ';
1159 if (!preview)
1161 MPD::Song::SetFunction set = IntoSetFunction(it->first);
1162 if (set)
1163 s.SetTags(set, it->second);
1165 else
1166 result << "%" << it->first << ": " << it->second << "\n";
1168 return result.str();
1171 #endif