strbuffer: change basic_buffer to BasicBuffer
[ncmpcpp.git] / src / tag_editor.cpp
blob7f00d57fce5f9ca4ad941d796a819395fb427db7
1 /***************************************************************************
2 * Copyright (C) 2008-2012 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 <sstream>
27 #include <stdexcept>
29 // taglib includes
30 #include "id3v2tag.h"
31 #include "textidentificationframe.h"
32 #include "mpegfile.h"
33 #include "vorbisfile.h"
34 #include "flacfile.h"
35 #include "xiphcomment.h"
36 #include "fileref.h"
37 #include "tag.h"
39 #include "browser.h"
40 #include "charset.h"
41 #include "display.h"
42 #include "global.h"
43 #include "helpers.h"
44 #include "playlist.h"
45 #include "song_info.h"
46 #include "statusbar.h"
47 #include "utility/comparators.h"
48 #include "title.h"
50 using namespace std::placeholders;
52 using Global::myScreen;
53 using Global::MainHeight;
54 using Global::MainStartY;
56 TagEditor *myTagEditor = new TagEditor;
58 namespace {//
60 size_t LeftColumnWidth;
61 size_t LeftColumnStartX;
62 size_t MiddleColumnWidth;
63 size_t MiddleColumnStartX;
64 size_t RightColumnWidth;
65 size_t RightColumnStartX;
67 size_t FParserDialogWidth;
68 size_t FParserDialogHeight;
69 size_t FParserWidth;
70 size_t FParserWidthOne;
71 size_t FParserWidthTwo;
72 size_t FParserHeight;
74 std::list<std::string> Patterns;
75 std::string PatternsFile = "patterns.list";
77 bool isAnyModified(const NC::Menu<MPD::MutableSong> &m);
79 std::string CapitalizeFirstLetters(const std::string &s);
80 void CapitalizeFirstLetters(MPD::MutableSong &s);
81 void LowerAllLetters(MPD::MutableSong &s);
82 void GetTagList(TagLib::StringList &list, const MPD::MutableSong &s, MPD::Song::GetFunction f);
84 template <typename T>
85 void WriteID3v2(const TagLib::ByteVector &type, TagLib::ID3v2::Tag *tag, const T &list);
86 void WriteXiphComments(const MPD::MutableSong &s, TagLib::Ogg::XiphComment *tag);
88 void GetPatternList();
89 void SavePatternList();
91 MPD::MutableSong::SetFunction IntoSetFunction(char c);
92 std::string GenerateFilename(const MPD::MutableSong &s, const std::string &pattern);
93 std::string ParseFilename(MPD::MutableSong &s, std::string mask, bool preview);
95 std::string SongToString(const MPD::MutableSong &s);
96 bool DirEntryMatcher(const Regex &rx, const std::pair<std::string, std::string> &dir, bool filter);
97 bool SongEntryMatcher(const Regex &rx, const MPD::MutableSong &s);
101 void TagEditor::Init()
103 PatternsFile = Config.ncmpcpp_directory + "patterns.list";
104 SetDimensions(0, COLS);
106 Dirs = new NC::Menu< std::pair<std::string, std::string> >(0, MainStartY, LeftColumnWidth, MainHeight, Config.titles_visibility ? "Directories" : "", Config.main_color, NC::brNone);
107 Dirs->setHighlightColor(Config.active_column_color);
108 Dirs->cyclicScrolling(Config.use_cyclic_scrolling);
109 Dirs->centeredCursor(Config.centered_cursor);
110 Dirs->setItemDisplayer(Display::Pair<std::string, std::string>);
112 TagTypes = new NC::Menu<std::string>(MiddleColumnStartX, MainStartY, MiddleColumnWidth, MainHeight, Config.titles_visibility ? "Tag types" : "", Config.main_color, NC::brNone);
113 TagTypes->setHighlightColor(Config.main_highlight_color);
114 TagTypes->cyclicScrolling(Config.use_cyclic_scrolling);
115 TagTypes->centeredCursor(Config.centered_cursor);
116 TagTypes->setItemDisplayer(Display::Default<std::string>);
118 for (const SongInfo::Metadata *m = SongInfo::Tags; m->Name; ++m)
119 TagTypes->addItem(m->Name);
120 TagTypes->addSeparator();
121 TagTypes->addItem("Filename");
122 TagTypes->addSeparator();
123 if (Config.titles_visibility)
124 TagTypes->addItem("Options", 1, 1);
125 TagTypes->addSeparator();
126 TagTypes->addItem("Capitalize First Letters");
127 TagTypes->addItem("lower all letters");
128 TagTypes->addSeparator();
129 TagTypes->addItem("Reset");
130 TagTypes->addItem("Save");
132 Tags = new NC::Menu<MPD::MutableSong>(RightColumnStartX, MainStartY, RightColumnWidth, MainHeight, Config.titles_visibility ? "Tags" : "", Config.main_color, NC::brNone);
133 Tags->setHighlightColor(Config.main_highlight_color);
134 Tags->cyclicScrolling(Config.use_cyclic_scrolling);
135 Tags->centeredCursor(Config.centered_cursor);
136 Tags->setSelectedPrefix(Config.selected_item_prefix);
137 Tags->setSelectedSuffix(Config.selected_item_suffix);
138 Tags->setItemDisplayer(Display::Tags);
140 FParserDialog = new NC::Menu<std::string>((COLS-FParserDialogWidth)/2, (MainHeight-FParserDialogHeight)/2+MainStartY, FParserDialogWidth, FParserDialogHeight, "", Config.main_color, Config.window_border);
141 FParserDialog->cyclicScrolling(Config.use_cyclic_scrolling);
142 FParserDialog->centeredCursor(Config.centered_cursor);
143 FParserDialog->setItemDisplayer(Display::Default<std::string>);
144 FParserDialog->addItem("Get tags from filename");
145 FParserDialog->addItem("Rename files");
146 FParserDialog->addSeparator();
147 FParserDialog->addItem("Cancel");
149 FParser = new NC::Menu<std::string>((COLS-FParserWidth)/2, (MainHeight-FParserHeight)/2+MainStartY, FParserWidthOne, FParserHeight, "_", Config.main_color, Config.active_window_border);
150 FParser->cyclicScrolling(Config.use_cyclic_scrolling);
151 FParser->centeredCursor(Config.centered_cursor);
152 FParser->setItemDisplayer(Display::Default<std::string>);
154 FParserLegend = new NC::Scrollpad((COLS-FParserWidth)/2+FParserWidthOne, (MainHeight-FParserHeight)/2+MainStartY, FParserWidthTwo, FParserHeight, "Legend", Config.main_color, Config.window_border);
156 FParserPreview = new NC::Scrollpad((COLS-FParserWidth)/2+FParserWidthOne, (MainHeight-FParserHeight)/2+MainStartY, FParserWidthTwo, FParserHeight, "Preview", Config.main_color, Config.window_border);
158 w = Dirs;
159 isInitialized = 1;
162 void TagEditor::SetDimensions(size_t x_offset, size_t width)
164 MiddleColumnWidth = std::min(26, COLS-2);
165 LeftColumnStartX = x_offset;
166 LeftColumnWidth = (width-MiddleColumnWidth)/2;
167 MiddleColumnStartX = LeftColumnStartX+LeftColumnWidth+1;
168 RightColumnWidth = width-LeftColumnWidth-MiddleColumnWidth-2;
169 RightColumnStartX = MiddleColumnStartX+MiddleColumnWidth+1;
171 FParserDialogWidth = std::min(30, COLS);
172 FParserDialogHeight = std::min(size_t(6), MainHeight);
173 FParserWidth = width*0.9;
174 FParserHeight = std::min(size_t(LINES*0.8), MainHeight);
175 FParserWidthOne = FParserWidth/2;
176 FParserWidthTwo = FParserWidth-FParserWidthOne;
179 void TagEditor::Resize()
181 size_t x_offset, width;
182 GetWindowResizeParams(x_offset, width);
183 SetDimensions(x_offset, width);
185 Dirs->resize(LeftColumnWidth, MainHeight);
186 TagTypes->resize(MiddleColumnWidth, MainHeight);
187 Tags->resize(RightColumnWidth, MainHeight);
188 FParserDialog->resize(FParserDialogWidth, FParserDialogHeight);
189 FParser->resize(FParserWidthOne, FParserHeight);
190 FParserLegend->resize(FParserWidthTwo, FParserHeight);
191 FParserPreview->resize(FParserWidthTwo, FParserHeight);
193 Dirs->moveTo(LeftColumnStartX, MainStartY);
194 TagTypes->moveTo(MiddleColumnStartX, MainStartY);
195 Tags->moveTo(RightColumnStartX, MainStartY);
197 FParserDialog->moveTo(x_offset+(width-FParserDialogWidth)/2, (MainHeight-FParserDialogHeight)/2+MainStartY);
198 FParser->moveTo(x_offset+(width-FParserWidth)/2, (MainHeight-FParserHeight)/2+MainStartY);
199 FParserLegend->moveTo(x_offset+(width-FParserWidth)/2+FParserWidthOne, (MainHeight-FParserHeight)/2+MainStartY);
200 FParserPreview->moveTo(x_offset+(width-FParserWidth)/2+FParserWidthOne, (MainHeight-FParserHeight)/2+MainStartY);
202 if (MainHeight < 5 && (w == FParserDialog || w == FParser || w == FParserHelper)) // screen too low
203 w = TagTypes; // fall back to main columns
205 hasToBeResized = 0;
208 std::wstring TagEditor::Title()
210 return L"Tag editor";
213 void TagEditor::SwitchTo()
215 using Global::myLockedScreen;
217 if (myScreen == this)
218 return;
220 if (!isInitialized)
221 Init();
223 if (myLockedScreen)
224 UpdateInactiveScreen(this);
226 if (hasToBeResized || myLockedScreen)
227 Resize();
229 if (myScreen != this && myScreen->isTabbable())
230 Global::myPrevScreen = myScreen;
231 myScreen = this;
232 drawHeader();
233 Refresh();
236 void TagEditor::Refresh()
238 Dirs->display();
239 mvvline(MainStartY, MiddleColumnStartX-1, 0, MainHeight);
240 TagTypes->display();
241 mvvline(MainStartY, RightColumnStartX-1, 0, MainHeight);
242 Tags->display();
244 if (w == FParserDialog)
246 FParserDialog->display();
248 else if (w == FParser || w == FParserHelper)
250 FParser->display();
251 FParserHelper->display();
255 void TagEditor::Update()
257 if (Dirs->reallyEmpty())
259 Dirs->Window::clear();
260 Tags->clear();
262 int highlightme = -1;
263 auto dirs = Mpd.GetDirectories(itsBrowsedDir);
264 std::sort(dirs.begin(), dirs.end(), LocaleBasedSorting(std::locale(), Config.ignore_leading_the));
265 if (itsBrowsedDir != "/")
267 size_t slash = itsBrowsedDir.rfind("/");
268 std::string parent = slash != std::string::npos ? itsBrowsedDir.substr(0, slash) : "/";
269 Dirs->addItem(make_pair("..", parent));
271 else
272 Dirs->addItem(std::make_pair(".", "/"));
273 for (auto dir = dirs.begin(); dir != dirs.end(); ++dir)
275 size_t slash = dir->rfind("/");
276 std::string to_display = slash != std::string::npos ? dir->substr(slash+1) : *dir;
277 Dirs->addItem(make_pair(to_display, *dir));
278 if (*dir == itsHighlightedDir)
279 highlightme = Dirs->size()-1;
281 if (highlightme != -1)
282 Dirs->highlight(highlightme);
284 Dirs->display();
287 if (Tags->reallyEmpty())
289 Tags->reset();
290 auto songs = Mpd.GetSongs(Dirs->current().value().second);
291 std::sort(songs.begin(), songs.end(),
292 LocaleBasedSorting(std::locale(), Config.ignore_leading_the));
293 for (auto s = songs.begin(); s != songs.end(); ++s)
294 Tags->addItem(*s);
295 Tags->refresh();
298 if (w == TagTypes && TagTypes->choice() < 13)
300 Tags->refresh();
302 else if (TagTypes->choice() >= 13)
304 Tags->Window::clear();
305 Tags->Window::refresh();
309 void TagEditor::EnterPressed()
311 using Global::wFooter;
313 if (w == Dirs)
315 auto dirs = Mpd.GetDirectories(Dirs->current().value().second);
316 if (!dirs.empty())
318 itsHighlightedDir = itsBrowsedDir;
319 itsBrowsedDir = Dirs->current().value().second;
320 Dirs->clear();
321 Dirs->reset();
323 else
324 Statusbar::msg("No subdirectories found");
326 else if (w == FParserDialog)
328 size_t choice = FParserDialog->choice();
329 if (choice == 3) // cancel
331 w = TagTypes;
332 Refresh();
333 return;
335 GetPatternList();
337 // prepare additional windows
339 FParserLegend->clear();
340 *FParserLegend << L"%a - artist\n";
341 *FParserLegend << L"%A - album artist\n";
342 *FParserLegend << L"%t - title\n";
343 *FParserLegend << L"%b - album\n";
344 *FParserLegend << L"%y - date\n";
345 *FParserLegend << L"%n - track number\n";
346 *FParserLegend << L"%g - genre\n";
347 *FParserLegend << L"%c - composer\n";
348 *FParserLegend << L"%p - performer\n";
349 *FParserLegend << L"%d - disc\n";
350 *FParserLegend << L"%C - comment\n\n";
351 *FParserLegend << NC::fmtBold << L"Files:\n" << NC::fmtBoldEnd;
352 for (auto it = EditedSongs.begin(); it != EditedSongs.end(); ++it)
353 *FParserLegend << Config.color2 << L" * " << NC::clEnd << (*it)->getName() << '\n';
354 FParserLegend->flush();
356 if (!Patterns.empty())
357 Config.pattern = Patterns.front();
358 FParser->clear();
359 FParser->reset();
360 FParser->addItem("Pattern: " + Config.pattern);
361 FParser->addItem("Preview");
362 FParser->addItem("Legend");
363 FParser->addSeparator();
364 FParser->addItem("Proceed");
365 FParser->addItem("Cancel");
366 if (!Patterns.empty())
368 FParser->addSeparator();
369 FParser->addItem("Recent patterns", 1, 1);
370 FParser->addSeparator();
371 for (std::list<std::string>::const_iterator it = Patterns.begin(); it != Patterns.end(); ++it)
372 FParser->addItem(*it);
375 FParser->setTitle(choice == 0 ? "Get tags from filename" : "Rename files");
376 w = FParser;
377 FParserUsePreview = 1;
378 FParserHelper = FParserLegend;
379 FParserHelper->display();
381 else if (w == FParser)
383 bool quit = 0;
384 size_t pos = FParser->choice();
386 if (pos == 4) // save
387 FParserUsePreview = 0;
389 if (pos == 0) // change pattern
391 Statusbar::lock();
392 Statusbar::put() << "Pattern: ";
393 std::string new_pattern = wFooter->getString(Config.pattern);
394 Statusbar::unlock();
395 if (!new_pattern.empty())
397 Config.pattern = new_pattern;
398 FParser->at(0).value() = "Pattern: ";
399 FParser->at(0).value() += Config.pattern;
402 else if (pos == 1 || pos == 4) // preview or proceed
404 bool success = 1;
405 Statusbar::msg("Parsing...");
406 FParserPreview->clear();
407 for (auto it = EditedSongs.begin(); it != EditedSongs.end(); ++it)
409 MPD::MutableSong &s = **it;
410 if (FParserDialog->choice() == 0) // get tags from filename
412 if (FParserUsePreview)
414 *FParserPreview << NC::fmtBold << s.getName() << L":\n" << NC::fmtBoldEnd;
415 *FParserPreview << ParseFilename(s, Config.pattern, FParserUsePreview) << '\n';
417 else
418 ParseFilename(s, Config.pattern, FParserUsePreview);
420 else // rename files
422 std::string file = s.getName();
423 size_t last_dot = file.rfind(".");
424 std::string extension = file.substr(last_dot);
425 std::string new_file = GenerateFilename(s, "{" + Config.pattern + "}");
426 if (new_file.empty() && !FParserUsePreview)
428 Statusbar::msg("File \"%s\" would have an empty name", s.getName().c_str());
429 FParserUsePreview = 1;
430 success = 0;
432 if (!FParserUsePreview)
433 s.setNewURI(new_file + extension);
434 *FParserPreview << file << Config.color2 << L" -> " << NC::clEnd;
435 if (new_file.empty())
436 *FParserPreview << Config.empty_tags_color << Config.empty_tag << NC::clEnd;
437 else
438 *FParserPreview << new_file << extension;
439 *FParserPreview << '\n' << '\n';
440 if (!success)
441 break;
444 if (FParserUsePreview)
446 FParserHelper = FParserPreview;
447 FParserHelper->flush();
448 FParserHelper->display();
450 else if (success)
452 Patterns.remove(Config.pattern);
453 Patterns.insert(Patterns.begin(), Config.pattern);
454 quit = 1;
456 if (pos != 4 || success)
457 Statusbar::msg("Operation finished");
459 else if (pos == 2) // show legend
461 FParserHelper = FParserLegend;
462 FParserHelper->display();
464 else if (pos == 5) // cancel
466 quit = 1;
468 else // list of patterns
470 Config.pattern = FParser->current().value();
471 FParser->at(0).value() = "Pattern: " + Config.pattern;
474 if (quit)
476 SavePatternList();
477 w = TagTypes;
478 Refresh();
479 return;
483 if ((w != TagTypes && w != Tags) || Tags->empty()) // after this point we start dealing with tags
484 return;
486 EditedSongs.clear();
487 if (Tags->hasSelected()) // if there are selected songs, perform operations only on them
489 for (auto it = Tags->begin(); it != Tags->end(); ++it)
490 if (it->isSelected())
491 EditedSongs.push_back(&it->value());
493 else
495 for (auto it = Tags->begin(); it != Tags->end(); ++it)
496 EditedSongs.push_back(&it->value());
499 size_t id = TagTypes->choice();
501 if (w == TagTypes && id == 5)
503 bool yes = Action::AskYesNoQuestion("Number tracks?", Status::trace);
504 if (yes)
506 auto it = EditedSongs.begin();
507 for (unsigned i = 1; i <= EditedSongs.size(); ++i, ++it)
509 if (Config.tag_editor_extended_numeration)
510 (*it)->setTrack(unsignedIntTo<std::string>::apply(i) + "/" + unsignedIntTo<std::string>::apply(EditedSongs.size()));
511 else
512 (*it)->setTrack(unsignedIntTo<std::string>::apply(i));
514 Statusbar::msg("Tracks numbered");
516 else
517 Statusbar::msg("Aborted");
518 return;
521 if (id < 11)
523 MPD::Song::GetFunction get = SongInfo::Tags[id].Get;
524 MPD::MutableSong::SetFunction set = SongInfo::Tags[id].Set;
525 if (id > 0 && w == TagTypes)
527 Statusbar::lock();
528 Statusbar::put() << NC::fmtBold << TagTypes->current().value() << NC::fmtBoldEnd << ": ";
529 std::string new_tag = wFooter->getString(Tags->current().value().getTags(get));
530 Statusbar::unlock();
531 for (auto it = EditedSongs.begin(); it != EditedSongs.end(); ++it)
532 (*it)->setTags(set, new_tag);
534 else if (w == Tags)
536 Statusbar::lock();
537 Statusbar::put() << NC::fmtBold << TagTypes->current().value() << NC::fmtBoldEnd << ": ";
538 std::string new_tag = wFooter->getString(Tags->current().value().getTags(get));
539 Statusbar::unlock();
540 if (new_tag != Tags->current().value().getTags(get))
541 Tags->current().value().setTags(set, new_tag);
542 Tags->scroll(NC::wDown);
545 else
547 if (id == 12) // filename related options
549 if (w == TagTypes)
551 if (size_t(COLS) < FParserDialogWidth || MainHeight < FParserDialogHeight)
553 Statusbar::msg("Screen is too small to display additional windows");
554 return;
556 FParserDialog->reset();
557 w = FParserDialog;
559 else if (w == Tags)
561 MPD::MutableSong &s = Tags->current().value();
562 std::string old_name = s.getNewURI().empty() ? s.getName() : s.getNewURI();
563 size_t last_dot = old_name.rfind(".");
564 std::string extension = old_name.substr(last_dot);
565 old_name = old_name.substr(0, last_dot);
566 Statusbar::lock();
567 Statusbar::put() << NC::fmtBold << "New filename: " << NC::fmtBoldEnd;
568 std::string new_name = wFooter->getString(old_name);
569 Statusbar::unlock();
570 if (!new_name.empty() && new_name != old_name)
571 s.setNewURI(new_name + extension);
572 Tags->scroll(NC::wDown);
575 else if (id == 16) // capitalize first letters
577 Statusbar::msg("Processing...");
578 for (auto it = EditedSongs.begin(); it != EditedSongs.end(); ++it)
579 CapitalizeFirstLetters(**it);
580 Statusbar::msg("Done");
582 else if (id == 17) // lower all letters
584 Statusbar::msg("Processing...");
585 for (auto it = EditedSongs.begin(); it != EditedSongs.end(); ++it)
586 LowerAllLetters(**it);
587 Statusbar::msg("Done");
589 else if (id == 19) // reset
591 for (auto it = Tags->beginV(); it != Tags->endV(); ++it)
592 it->clearModifications();
593 Statusbar::msg("Changes reset");
595 else if (id == 20) // save
597 bool success = 1;
598 Statusbar::msg("Writing changes...");
599 for (auto it = EditedSongs.begin(); it != EditedSongs.end(); ++it)
601 Statusbar::msg("Writing tags in \"%s\"...", (*it)->getName().c_str());
602 if (!WriteTags(**it))
604 const char msg[] = "Error while writing tags in \"%ls\"";
605 Statusbar::msg(msg, wideShorten(ToWString((*it)->getURI()), COLS-const_strlen(msg)).c_str());
606 success = 0;
607 break;
610 if (success)
612 Statusbar::msg("Tags updated");
613 TagTypes->setHighlightColor(Config.main_highlight_color);
614 TagTypes->reset();
615 w->refresh();
616 w = Dirs;
617 Dirs->setHighlightColor(Config.active_column_color);
618 Mpd.UpdateDirectory(getSharedDirectory(Tags->beginV(), Tags->endV()));
620 else
621 Tags->clear();
626 void TagEditor::SpacePressed()
628 if (w == Tags && !Tags->empty())
630 Tags->current().setSelected(!Tags->current().isSelected());
631 w->scroll(NC::wDown);
635 void TagEditor::MouseButtonPressed(MEVENT me)
637 if (w == FParserDialog)
639 if (FParserDialog->hasCoords(me.x, me.y))
641 if (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED))
643 FParserDialog->Goto(me.y);
644 if (me.bstate & BUTTON3_PRESSED)
645 EnterPressed();
647 else
648 Screen<NC::Window>::MouseButtonPressed(me);
651 else if (w == FParser || w == FParserHelper)
653 if (FParser->hasCoords(me.x, me.y))
655 if (w != FParser)
656 PrevColumn();
657 if (size_t(me.y) < FParser->size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)))
659 FParser->Goto(me.y);
660 if (me.bstate & BUTTON3_PRESSED)
661 EnterPressed();
663 else
664 Screen<NC::Window>::MouseButtonPressed(me);
666 else if (FParserHelper->hasCoords(me.x, me.y))
668 if (w != FParserHelper)
669 NextColumn();
670 ScrollpadMouseButtonPressed(FParserHelper, me);
673 else if (!Dirs->empty() && Dirs->hasCoords(me.x, me.y))
675 if (w != Dirs)
677 PrevColumn();
678 PrevColumn();
680 if (size_t(me.y) < Dirs->size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)))
682 Dirs->Goto(me.y);
683 if (me.bstate & BUTTON1_PRESSED)
684 EnterPressed();
685 else
686 SpacePressed();
688 else
689 Screen<NC::Window>::MouseButtonPressed(me);
690 Tags->clear();
692 else if (!TagTypes->empty() && TagTypes->hasCoords(me.x, me.y))
694 if (w != TagTypes)
695 w == Dirs ? NextColumn() : PrevColumn();
696 if (size_t(me.y) < TagTypes->size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)))
698 if (!TagTypes->Goto(me.y))
699 return;
700 TagTypes->refresh();
701 Tags->refresh();
702 if (me.bstate & BUTTON3_PRESSED)
703 EnterPressed();
705 else
706 Screen<NC::Window>::MouseButtonPressed(me);
708 else if (!Tags->empty() && Tags->hasCoords(me.x, me.y))
710 if (w != Tags)
712 NextColumn();
713 NextColumn();
715 if (size_t(me.y) < Tags->size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)))
717 Tags->Goto(me.y);
718 Tags->refresh();
719 if (me.bstate & BUTTON3_PRESSED)
720 EnterPressed();
722 else
723 Screen<NC::Window>::MouseButtonPressed(me);
727 /***********************************************************************/
729 bool TagEditor::allowsFiltering()
731 return w == Dirs || w == Tags;
734 std::string TagEditor::currentFilter()
736 std::string filter;
737 if (w == Dirs)
738 filter = RegexFilter< std::pair<std::string, std::string> >::currentFilter(*Dirs);
739 else if (w == Tags)
740 filter = RegexFilter<MPD::MutableSong>::currentFilter(*Tags);
741 return filter;
744 void TagEditor::applyFilter(const std::string &filter)
746 if (w == Dirs)
748 Dirs->showAll();
749 auto fun = std::bind(DirEntryMatcher, _1, _2, true);
750 auto rx = RegexFilter< std::pair<std::string, std::string> >(filter, Config.regex_type, fun);
751 Dirs->filter(Dirs->begin(), Dirs->end(), rx);
753 else if (w == Tags)
755 Tags->showAll();
756 auto rx = RegexFilter<MPD::MutableSong>(filter, Config.regex_type, SongEntryMatcher);
757 Tags->filter(Tags->begin(), Tags->end(), rx);
761 /***********************************************************************/
763 bool TagEditor::allowsSearching()
765 return w == Dirs || w == Tags;
768 bool TagEditor::search(const std::string &constraint)
770 bool result = false;
771 if (w == Dirs)
773 auto fun = std::bind(DirEntryMatcher, _1, _2, false);
774 auto rx = RegexFilter< std::pair<std::string, std::string> >(constraint, Config.regex_type, fun);
775 result = Dirs->search(Dirs->begin(), Dirs->end(), rx);
777 else if (w == Tags)
779 auto rx = RegexFilter<MPD::MutableSong>(constraint, Config.regex_type, SongEntryMatcher);
780 result = Tags->search(Tags->begin(), Tags->end(), rx);
782 return result;
785 void TagEditor::nextFound(bool wrap)
787 if (w == Dirs)
788 Dirs->nextFound(wrap);
789 else if (w == Tags)
790 Tags->nextFound(wrap);
793 void TagEditor::prevFound(bool wrap)
795 if (w == Dirs)
796 Dirs->prevFound(wrap);
797 else if (w == Tags)
798 Tags->prevFound(wrap);
801 /***********************************************************************/
803 std::shared_ptr<ProxySongList> TagEditor::getProxySongList()
805 auto ptr = nullProxySongList();
806 if (w == Tags)
807 ptr = mkProxySongList(*Tags, [](NC::Menu<MPD::MutableSong>::Item &item) {
808 return &item.value();
810 return ptr;
813 bool TagEditor::allowsSelection()
815 return w == Tags;
818 void TagEditor::reverseSelection()
820 if (w == Tags)
821 reverseSelectionHelper(Tags->begin(), Tags->end());
824 MPD::SongList TagEditor::getSelectedSongs()
826 MPD::SongList result;
827 if (w == Tags)
829 for (auto it = Tags->begin(); it != Tags->end(); ++it)
830 if (it->isSelected())
831 result.push_back(it->value());
832 // if no song was selected, add current one
833 if (result.empty() && !Tags->empty())
834 result.push_back(Tags->current().value());
836 return result;
839 /***********************************************************************/
841 bool TagEditor::ifAnyModifiedAskForDiscarding()
843 bool result = true;
844 if (isAnyModified(*Tags))
845 result = Action::AskYesNoQuestion("There are pending changes, are you sure?", Status::trace);
846 return result;
849 bool TagEditor::isNextColumnAvailable()
851 bool result = false;
852 if (w == Dirs)
854 if (!TagTypes->reallyEmpty() && !Tags->reallyEmpty())
855 result = true;
857 else if (w == TagTypes)
859 if (!Tags->reallyEmpty())
860 result = true;
862 else if (w == FParser)
863 result = true;
864 return result;
867 bool TagEditor::NextColumn()
869 if (w == Dirs)
871 Dirs->setHighlightColor(Config.main_highlight_color);
872 w->refresh();
873 w = TagTypes;
874 TagTypes->setHighlightColor(Config.active_column_color);
875 return true;
877 else if (w == TagTypes && TagTypes->choice() < 13 && !Tags->reallyEmpty())
879 TagTypes->setHighlightColor(Config.main_highlight_color);
880 w->refresh();
881 w = Tags;
882 Tags->setHighlightColor(Config.active_column_color);
883 return true;
885 else if (w == FParser)
887 FParser->setBorder(Config.window_border);
888 FParser->display();
889 w = FParserHelper;
890 FParserHelper->setBorder(Config.active_window_border);
891 FParserHelper->display();
892 return true;
894 return false;
897 bool TagEditor::isPrevColumnAvailable()
899 bool result = false;
900 if (w == Tags)
902 if (!TagTypes->reallyEmpty() && !Dirs->reallyEmpty())
903 result = true;
905 else if (w == TagTypes)
907 if (!Dirs->reallyEmpty())
908 result = ifAnyModifiedAskForDiscarding();
910 else if (w == FParserHelper)
911 result = true;
912 return result;
915 bool TagEditor::PrevColumn()
917 if (w == Tags)
919 Tags->setHighlightColor(Config.main_highlight_color);
920 w->refresh();
921 w = TagTypes;
922 TagTypes->setHighlightColor(Config.active_column_color);
923 return true;
925 else if (w == TagTypes)
927 TagTypes->setHighlightColor(Config.main_highlight_color);
928 w->refresh();
929 w = Dirs;
930 Dirs->setHighlightColor(Config.active_column_color);
931 return true;
933 else if (w == FParserHelper)
935 FParserHelper->setBorder(Config.window_border);
936 FParserHelper->display();
937 w = FParser;
938 FParser->setBorder(Config.active_window_border);
939 FParser->display();
940 return true;
942 return false;
945 void TagEditor::LocateSong(const MPD::Song &s)
947 if (myScreen == this)
948 return;
950 if (s.getDirectory().empty())
951 return;
953 if (Global::myScreen != this)
954 SwitchTo();
956 // go to right directory
957 if (itsBrowsedDir != s.getDirectory())
959 itsBrowsedDir = s.getDirectory();
960 size_t last_slash = itsBrowsedDir.rfind('/');
961 if (last_slash != std::string::npos)
962 itsBrowsedDir = itsBrowsedDir.substr(0, last_slash);
963 else
964 itsBrowsedDir = "/";
965 if (itsBrowsedDir.empty())
966 itsBrowsedDir = "/";
967 Dirs->clear();
968 Update();
970 if (itsBrowsedDir == "/")
971 Dirs->reset(); // go to the first pos, which is "." (music dir root)
973 // highlight directory we need and get files from it
974 std::string dir = getBasename(s.getDirectory());
975 for (size_t i = 0; i < Dirs->size(); ++i)
977 if ((*Dirs)[i].value().first == dir)
979 Dirs->highlight(i);
980 break;
983 // refresh window so we can be highlighted item
984 Dirs->refresh();
986 Tags->clear();
987 Update();
989 // reset TagTypes since it can be under Filename
990 // and then songs in right column are not visible.
991 TagTypes->reset();
992 // go to the right column
993 NextColumn();
994 NextColumn();
996 // highlight our file
997 for (size_t i = 0; i < Tags->size(); ++i)
999 if ((*Tags)[i].value().getHash() == s.getHash())
1001 Tags->highlight(i);
1002 break;
1007 void TagEditor::ReadTags(MPD::MutableSong &s)
1009 TagLib::FileRef f(s.getURI().c_str());
1010 if (f.isNull())
1011 return;
1013 TagLib::MPEG::File *mpegf = dynamic_cast<TagLib::MPEG::File *>(f.file());
1014 s.setDuration(f.audioProperties()->length());
1016 s.setArtist(f.tag()->artist().to8Bit(1));
1017 s.setTitle(f.tag()->title().to8Bit(1));
1018 s.setAlbum(f.tag()->album().to8Bit(1));
1019 s.setTrack(intTo<std::string>::apply(f.tag()->track()));
1020 s.setDate(intTo<std::string>::apply(f.tag()->year()));
1021 s.setGenre(f.tag()->genre().to8Bit(1));
1022 if (mpegf)
1024 s.setAlbumArtist(!mpegf->ID3v2Tag()->frameListMap()["TPE2"].isEmpty() ? mpegf->ID3v2Tag()->frameListMap()["TPE2"].front()->toString().to8Bit(1) : "");
1025 s.setComposer(!mpegf->ID3v2Tag()->frameListMap()["TCOM"].isEmpty() ? mpegf->ID3v2Tag()->frameListMap()["TCOM"].front()->toString().to8Bit(1) : "");
1026 s.setPerformer(!mpegf->ID3v2Tag()->frameListMap()["TOPE"].isEmpty() ? mpegf->ID3v2Tag()->frameListMap()["TOPE"].front()->toString().to8Bit(1) : "");
1027 s.setDisc(!mpegf->ID3v2Tag()->frameListMap()["TPOS"].isEmpty() ? mpegf->ID3v2Tag()->frameListMap()["TPOS"].front()->toString().to8Bit(1) : "");
1029 s.setComment(f.tag()->comment().to8Bit(1));
1032 bool TagEditor::WriteTags(MPD::MutableSong &s)
1034 std::string path_to_file;
1035 bool file_is_from_db = s.isFromDatabase();
1036 if (file_is_from_db)
1037 path_to_file += Config.mpd_music_dir;
1038 path_to_file += s.getURI();
1039 TagLib::FileRef f(path_to_file.c_str());
1040 if (!f.isNull())
1042 f.tag()->setTitle(ToWString(s.getTitle()));
1043 f.tag()->setArtist(ToWString(s.getArtist()));
1044 f.tag()->setAlbum(ToWString(s.getAlbum()));
1045 f.tag()->setYear(stringToInt(s.getDate()));
1046 f.tag()->setTrack(stringToInt(s.getTrack()));
1047 f.tag()->setGenre(ToWString(s.getGenre()));
1048 f.tag()->setComment(ToWString(s.getComment()));
1049 if (TagLib::MPEG::File *mp3_file = dynamic_cast<TagLib::MPEG::File *>(f.file()))
1051 TagLib::ID3v2::Tag *tag = mp3_file->ID3v2Tag(1);
1052 TagLib::StringList list;
1054 WriteID3v2("TIT2", tag, ToWString(s.getTitle())); // title
1055 WriteID3v2("TPE1", tag, ToWString(s.getArtist())); // artist
1056 WriteID3v2("TALB", tag, ToWString(s.getAlbum())); // album
1057 WriteID3v2("TDRC", tag, ToWString(s.getDate())); // date
1058 WriteID3v2("TRCK", tag, ToWString(s.getTrack())); // track
1059 WriteID3v2("TCON", tag, ToWString(s.getGenre())); // genre
1060 WriteID3v2("TPOS", tag, ToWString(s.getDisc())); // disc
1062 GetTagList(list, s, &MPD::Song::getAlbumArtist);
1063 WriteID3v2("TPE2", tag, list); // album artist
1065 GetTagList(list, s, &MPD::Song::getComposer);
1066 WriteID3v2("TCOM", tag, list); // composer
1068 GetTagList(list, s, &MPD::Song::getPerformer);
1069 WriteID3v2("TPE3", tag, list); // performer
1071 else if (TagLib::Ogg::Vorbis::File *ogg_file = dynamic_cast<TagLib::Ogg::Vorbis::File *>(f.file()))
1073 WriteXiphComments(s, ogg_file->tag());
1075 else if (TagLib::FLAC::File *flac_file = dynamic_cast<TagLib::FLAC::File *>(f.file()))
1077 WriteXiphComments(s, flac_file->xiphComment(1));
1079 if (!f.save())
1080 return false;
1082 if (!s.getNewURI().empty())
1084 std::string new_name;
1085 if (file_is_from_db)
1086 new_name += Config.mpd_music_dir;
1087 new_name += s.getDirectory() + "/" + s.getNewURI();
1088 if (rename(path_to_file.c_str(), new_name.c_str()) == 0 && !file_is_from_db)
1090 if (Global::myOldScreen == myPlaylist)
1092 // if we rename local file, it won't get updated
1093 // so just remove it from playlist and add again
1094 size_t pos = myPlaylist->Items->choice();
1095 Mpd.StartCommandsList();
1096 Mpd.Delete(pos);
1097 int id = Mpd.AddSong("file://" + new_name);
1098 if (id >= 0)
1100 s = myPlaylist->Items->back().value();
1101 Mpd.Move(s.getPosition(), pos);
1103 Mpd.CommitCommandsList();
1105 else // only myBrowser->Main()
1106 myBrowser->GetDirectory(myBrowser->CurrentDir());
1109 return true;
1111 else
1112 return false;
1115 namespace {//
1117 bool isAnyModified(const NC::Menu<MPD::MutableSong> &m)
1119 for (auto it = m.beginV(); it != m.endV(); ++it)
1120 if (it->isModified())
1121 return true;
1122 return false;
1125 std::string CapitalizeFirstLetters(const std::string &s)
1127 std::wstring ws = ToWString(s);
1128 wchar_t prev = 0;
1129 for (auto it = ws.begin(); it != ws.end(); ++it)
1131 if (!iswalpha(prev) && prev != L'\'')
1132 *it = towupper(*it);
1133 prev = *it;
1135 return ToString(ws);
1138 void CapitalizeFirstLetters(MPD::MutableSong &s)
1140 for (const SongInfo::Metadata *m = SongInfo::Tags; m->Name; ++m)
1142 unsigned i = 0;
1143 for (std::string tag; !(tag = (s.*m->Get)(i)).empty(); ++i)
1144 (s.*m->Set)(CapitalizeFirstLetters(tag), i);
1148 void LowerAllLetters(MPD::MutableSong &s)
1150 for (const SongInfo::Metadata *m = SongInfo::Tags; m->Name; ++m)
1152 unsigned i = 0;
1153 for (std::string tag; !(tag = (s.*m->Get)(i)).empty(); ++i)
1154 (s.*m->Set)(ToString(lowercase(ToWString(tag))), i);
1158 void GetTagList(TagLib::StringList &list, const MPD::MutableSong &s, MPD::Song::GetFunction f)
1160 list.clear();
1161 unsigned pos = 0;
1162 for (std::string value; !(value = (s.*f)(pos)).empty(); ++pos)
1163 list.append(ToWString(value));
1166 template <typename T> void WriteID3v2(const TagLib::ByteVector &type, TagLib::ID3v2::Tag *tag, const T &list)
1168 using TagLib::ID3v2::TextIdentificationFrame;
1169 tag->removeFrames(type);
1170 TextIdentificationFrame *frame = new TextIdentificationFrame(type, TagLib::String::UTF8);
1171 frame->setText(list);
1172 tag->addFrame(frame);
1175 void WriteXiphComments(const MPD::MutableSong &s, TagLib::Ogg::XiphComment *tag)
1177 TagLib::StringList list;
1179 tag->addField("DISCNUMBER", ToWString(s.getDisc())); // disc
1181 tag->removeField("ALBUM ARTIST"); // album artist
1182 GetTagList(list, s, &MPD::Song::getAlbumArtist);
1183 for (TagLib::StringList::ConstIterator it = list.begin(); it != list.end(); ++it)
1184 tag->addField("ALBUM ARTIST", *it, 0);
1186 tag->removeField("COMPOSER"); // composer
1187 GetTagList(list, s, &MPD::Song::getComposer);
1188 for (TagLib::StringList::ConstIterator it = list.begin(); it != list.end(); ++it)
1189 tag->addField("COMPOSER", *it, 0);
1191 tag->removeField("PERFORMER"); // performer
1192 GetTagList(list, s, &MPD::Song::getPerformer);
1193 for (TagLib::StringList::ConstIterator it = list.begin(); it != list.end(); ++it)
1194 tag->addField("PERFORMER", *it, 0);
1197 void GetPatternList()
1199 if (Patterns.empty())
1201 std::ifstream input(PatternsFile.c_str());
1202 if (input.is_open())
1204 std::string line;
1205 while (getline(input, line))
1206 if (!line.empty())
1207 Patterns.push_back(line);
1208 input.close();
1213 void SavePatternList()
1215 std::ofstream output(PatternsFile.c_str());
1216 if (output.is_open())
1218 std::list<std::string>::const_iterator it = Patterns.begin();
1219 for (unsigned i = 30; it != Patterns.end() && i; ++it, --i)
1220 output << *it << std::endl;
1221 output.close();
1224 MPD::MutableSong::SetFunction IntoSetFunction(char c)
1226 switch (c)
1228 case 'a':
1229 return &MPD::MutableSong::setArtist;
1230 case 'A':
1231 return &MPD::MutableSong::setAlbumArtist;
1232 case 't':
1233 return &MPD::MutableSong::setTitle;
1234 case 'b':
1235 return &MPD::MutableSong::setAlbum;
1236 case 'y':
1237 return &MPD::MutableSong::setDate;
1238 case 'n':
1239 return &MPD::MutableSong::setTrack;
1240 case 'g':
1241 return &MPD::MutableSong::setGenre;
1242 case 'c':
1243 return &MPD::MutableSong::setComposer;
1244 case 'p':
1245 return &MPD::MutableSong::setPerformer;
1246 case 'd':
1247 return &MPD::MutableSong::setDisc;
1248 case 'C':
1249 return &MPD::MutableSong::setComment;
1250 default:
1251 return 0;
1255 std::string GenerateFilename(const MPD::MutableSong &s, const std::string &pattern)
1257 std::string result = s.toString(pattern);
1258 removeInvalidCharsFromFilename(result);
1259 return result;
1262 std::string ParseFilename(MPD::MutableSong &s, std::string mask, bool preview)
1264 std::ostringstream result;
1265 std::vector<std::string> separators;
1266 std::vector< std::pair<char, std::string> > tags;
1267 std::string file = s.getName().substr(0, s.getName().rfind("."));
1269 for (size_t i = mask.find("%"); i != std::string::npos; i = mask.find("%"))
1271 tags.push_back(std::make_pair(mask.at(i+1), ""));
1272 mask = mask.substr(i+2);
1273 i = mask.find("%");
1274 if (!mask.empty())
1275 separators.push_back(mask.substr(0, i));
1277 size_t i = 0;
1278 for (auto it = separators.begin(); it != separators.end(); ++it, ++i)
1280 size_t j = file.find(*it);
1281 tags.at(i).second = file.substr(0, j);
1282 if (j+it->length() > file.length())
1283 goto PARSE_FAILED;
1284 file = file.substr(j+it->length());
1286 if (!file.empty())
1288 if (i >= tags.size())
1289 goto PARSE_FAILED;
1290 tags.at(i).second = file;
1293 if (0) // tss...
1295 PARSE_FAILED:
1296 return "Error while parsing filename!\n";
1299 for (auto it = tags.begin(); it != tags.end(); ++it)
1301 for (std::string::iterator j = it->second.begin(); j != it->second.end(); ++j)
1302 if (*j == '_')
1303 *j = ' ';
1305 if (!preview)
1307 MPD::MutableSong::SetFunction set = IntoSetFunction(it->first);
1308 if (set)
1309 s.setTags(set, it->second);
1311 else
1312 result << "%" << it->first << ": " << it->second << "\n";
1314 return result.str();
1317 std::string SongToString(const MPD::MutableSong &s)
1319 std::string result;
1320 size_t i = myTagEditor->TagTypes->choice();
1321 if (i < 11)
1322 result = (s.*SongInfo::Tags[i].Get)(0);
1323 else if (i == 12)
1324 result = s.getNewURI().empty() ? s.getName() : s.getName() + " -> " + s.getNewURI();
1325 return result.empty() ? Config.empty_tag : result;
1328 bool DirEntryMatcher(const Regex &rx, const std::pair<std::string, std::string> &dir, bool filter)
1330 if (dir.first == "." || dir.first == "..")
1331 return filter;
1332 return rx.match(dir.first);
1335 bool SongEntryMatcher(const Regex &rx, const MPD::MutableSong &s)
1337 return rx.match(SongToString(s));
1342 #endif