change version to 0.7.3
[ncmpcpp.git] / src / tag_editor.cpp
blobcd4dd770e3b214f6d7db10a30a8700fd0304822b
1 /***************************************************************************
2 * Copyright (C) 2008-2014 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 <boost/locale/conversion.hpp>
26 #include <algorithm>
27 #include <fstream>
29 #include "actions.h"
30 #include "browser.h"
31 #include "charset.h"
32 #include "display.h"
33 #include "global.h"
34 #include "helpers.h"
35 #include "menu_impl.h"
36 #include "playlist.h"
37 #include "song_info.h"
38 #include "statusbar.h"
39 #include "helpers/song_iterator_maker.h"
40 #include "utility/functional.h"
41 #include "utility/comparators.h"
42 #include "title.h"
43 #include "tags.h"
44 #include "screen_switcher.h"
46 using Global::myScreen;
47 using Global::MainHeight;
48 using Global::MainStartY;
50 namespace ph = std::placeholders;
52 TagEditor *myTagEditor;
54 namespace {
56 size_t LeftColumnWidth;
57 size_t LeftColumnStartX;
58 size_t MiddleColumnWidth;
59 size_t MiddleColumnStartX;
60 size_t RightColumnWidth;
61 size_t RightColumnStartX;
63 size_t FParserDialogWidth;
64 size_t FParserDialogHeight;
65 size_t FParserWidth;
66 size_t FParserWidthOne;
67 size_t FParserWidthTwo;
68 size_t FParserHeight;
70 std::list<std::string> Patterns;
71 std::string PatternsFile = "patterns.list";
73 bool isAnyModified(const NC::Menu<MPD::MutableSong> &m);
75 std::string CapitalizeFirstLetters(const std::string &s);
76 void CapitalizeFirstLetters(MPD::MutableSong &s);
77 void LowerAllLetters(MPD::MutableSong &s);
79 void GetPatternList();
80 void SavePatternList();
82 MPD::MutableSong::SetFunction IntoSetFunction(char c);
83 std::string GenerateFilename(const MPD::MutableSong &s, const std::string &pattern);
84 std::string ParseFilename(MPD::MutableSong &s, std::string mask, bool preview);
86 std::string SongToString(const MPD::MutableSong &s);
87 bool DirEntryMatcher(const Regex::Regex &rx, const std::pair<std::string, std::string> &dir, bool filter);
88 bool SongEntryMatcher(const Regex::Regex &rx, const MPD::MutableSong &s);
90 template <bool Const>
91 struct SongExtractor
93 typedef SongExtractor type;
95 typedef typename NC::Menu<MPD::MutableSong>::Item MenuItem;
96 typedef typename std::conditional<Const, const MenuItem, MenuItem>::type Item;
97 typedef typename std::conditional<Const, const MPD::Song, MPD::Song>::type Song;
99 Song *operator()(Item &item) const
101 return &item.value();
107 SongIterator TagsWindow::currentS()
109 return makeSongIterator_<MPD::MutableSong>(current(), SongExtractor<false>());
112 ConstSongIterator TagsWindow::currentS() const
114 return makeConstSongIterator_<MPD::MutableSong>(current(), SongExtractor<true>());
117 SongIterator TagsWindow::beginS()
119 return makeSongIterator_<MPD::MutableSong>(begin(), SongExtractor<false>());
122 ConstSongIterator TagsWindow::beginS() const
124 return makeConstSongIterator_<MPD::MutableSong>(begin(), SongExtractor<true>());
127 SongIterator TagsWindow::endS()
129 return makeSongIterator_<MPD::MutableSong>(end(), SongExtractor<false>());
132 ConstSongIterator TagsWindow::endS() const
134 return makeConstSongIterator_<MPD::MutableSong>(end(), SongExtractor<true>());
137 std::vector<MPD::Song> TagsWindow::getSelectedSongs()
139 return {}; // TODO
142 /**********************************************************************/
144 TagEditor::TagEditor() : FParser(0), FParserHelper(0), FParserLegend(0), FParserPreview(0), itsBrowsedDir("/")
146 PatternsFile = Config.ncmpcpp_directory + "patterns.list";
147 SetDimensions(0, COLS);
149 Dirs = new NC::Menu< std::pair<std::string, std::string> >(0, MainStartY, LeftColumnWidth, MainHeight, Config.titles_visibility ? "Directories" : "", Config.main_color, NC::Border());
150 Dirs->setHighlightColor(Config.active_column_color);
151 Dirs->cyclicScrolling(Config.use_cyclic_scrolling);
152 Dirs->centeredCursor(Config.centered_cursor);
153 Dirs->setItemDisplayer([](NC::Menu<std::pair<std::string, std::string>> &menu) {
154 menu << Charset::utf8ToLocale(menu.drawn()->value().first);
157 TagTypes = new NC::Menu<std::string>(MiddleColumnStartX, MainStartY, MiddleColumnWidth, MainHeight, Config.titles_visibility ? "Tag types" : "", Config.main_color, NC::Border());
158 TagTypes->setHighlightColor(Config.main_highlight_color);
159 TagTypes->cyclicScrolling(Config.use_cyclic_scrolling);
160 TagTypes->centeredCursor(Config.centered_cursor);
161 TagTypes->setItemDisplayer([](NC::Menu<std::string> &menu) {
162 menu << Charset::utf8ToLocale(menu.drawn()->value());
165 for (const SongInfo::Metadata *m = SongInfo::Tags; m->Name; ++m)
166 TagTypes->addItem(m->Name);
167 TagTypes->addSeparator();
168 TagTypes->addItem("Filename");
169 TagTypes->addSeparator();
170 if (Config.titles_visibility)
172 TagTypes->addItem("Options", NC::List::Properties::Bold | NC::List::Properties::Inactive);
173 TagTypes->addSeparator();
175 TagTypes->addItem("Capitalize First Letters");
176 TagTypes->addItem("lower all letters");
177 TagTypes->addSeparator();
178 TagTypes->addItem("Reset");
179 TagTypes->addItem("Save");
181 Tags = new TagsWindow(NC::Menu<MPD::MutableSong>(RightColumnStartX, MainStartY, RightColumnWidth, MainHeight, Config.titles_visibility ? "Tags" : "", Config.main_color, NC::Border()));
182 Tags->setHighlightColor(Config.main_highlight_color);
183 Tags->cyclicScrolling(Config.use_cyclic_scrolling);
184 Tags->centeredCursor(Config.centered_cursor);
185 Tags->setSelectedPrefix(Config.selected_item_prefix);
186 Tags->setSelectedSuffix(Config.selected_item_suffix);
187 Tags->setItemDisplayer(Display::Tags);
189 auto parser_display = [](NC::Menu<std::string> &menu) {
190 menu << Charset::utf8ToLocale(menu.drawn()->value());
193 FParserDialog = new NC::Menu<std::string>((COLS-FParserDialogWidth)/2, (MainHeight-FParserDialogHeight)/2+MainStartY, FParserDialogWidth, FParserDialogHeight, "", Config.main_color, Config.window_border);
194 FParserDialog->cyclicScrolling(Config.use_cyclic_scrolling);
195 FParserDialog->centeredCursor(Config.centered_cursor);
196 FParserDialog->setItemDisplayer(parser_display);
197 FParserDialog->addItem("Get tags from filename");
198 FParserDialog->addItem("Rename files");
199 FParserDialog->addItem("Cancel");
201 FParser = new NC::Menu<std::string>((COLS-FParserWidth)/2, (MainHeight-FParserHeight)/2+MainStartY, FParserWidthOne, FParserHeight, "_", Config.main_color, Config.active_window_border);
202 FParser->cyclicScrolling(Config.use_cyclic_scrolling);
203 FParser->centeredCursor(Config.centered_cursor);
204 FParser->setItemDisplayer(parser_display);
206 FParserLegend = new NC::Scrollpad((COLS-FParserWidth)/2+FParserWidthOne, (MainHeight-FParserHeight)/2+MainStartY, FParserWidthTwo, FParserHeight, "Legend", Config.main_color, Config.window_border);
208 FParserPreview = new NC::Scrollpad((COLS-FParserWidth)/2+FParserWidthOne, (MainHeight-FParserHeight)/2+MainStartY, FParserWidthTwo, FParserHeight, "Preview", Config.main_color, Config.window_border);
210 w = Dirs;
213 void TagEditor::SetDimensions(size_t x_offset, size_t width)
215 MiddleColumnWidth = std::min(26, COLS-2);
216 LeftColumnStartX = x_offset;
217 LeftColumnWidth = (width-MiddleColumnWidth)/2;
218 MiddleColumnStartX = LeftColumnStartX+LeftColumnWidth+1;
219 RightColumnWidth = width-LeftColumnWidth-MiddleColumnWidth-2;
220 RightColumnStartX = MiddleColumnStartX+MiddleColumnWidth+1;
222 FParserDialogWidth = std::min(30, COLS);
223 FParserDialogHeight = std::min(size_t(5), MainHeight);
224 FParserWidth = width*0.9;
225 FParserHeight = std::min(size_t(LINES*0.8), MainHeight);
226 FParserWidthOne = FParserWidth/2;
227 FParserWidthTwo = FParserWidth-FParserWidthOne;
230 void TagEditor::resize()
232 size_t x_offset, width;
233 getWindowResizeParams(x_offset, width);
234 SetDimensions(x_offset, width);
236 Dirs->resize(LeftColumnWidth, MainHeight);
237 TagTypes->resize(MiddleColumnWidth, MainHeight);
238 Tags->resize(RightColumnWidth, MainHeight);
239 FParserDialog->resize(FParserDialogWidth, FParserDialogHeight);
240 FParser->resize(FParserWidthOne, FParserHeight);
241 FParserLegend->resize(FParserWidthTwo, FParserHeight);
242 FParserPreview->resize(FParserWidthTwo, FParserHeight);
244 Dirs->moveTo(LeftColumnStartX, MainStartY);
245 TagTypes->moveTo(MiddleColumnStartX, MainStartY);
246 Tags->moveTo(RightColumnStartX, MainStartY);
248 FParserDialog->moveTo(x_offset+(width-FParserDialogWidth)/2, (MainHeight-FParserDialogHeight)/2+MainStartY);
249 FParser->moveTo(x_offset+(width-FParserWidth)/2, (MainHeight-FParserHeight)/2+MainStartY);
250 FParserLegend->moveTo(x_offset+(width-FParserWidth)/2+FParserWidthOne, (MainHeight-FParserHeight)/2+MainStartY);
251 FParserPreview->moveTo(x_offset+(width-FParserWidth)/2+FParserWidthOne, (MainHeight-FParserHeight)/2+MainStartY);
253 hasToBeResized = 0;
256 std::wstring TagEditor::title()
258 return L"Tag editor";
261 void TagEditor::switchTo()
263 SwitchTo::execute(this);
264 drawHeader();
265 refresh();
268 void TagEditor::refresh()
270 Dirs->display();
271 drawSeparator(MiddleColumnStartX-1);
272 TagTypes->display();
273 drawSeparator(RightColumnStartX-1);
274 Tags->display();
276 if (w == FParserDialog)
278 FParserDialog->display();
280 else if (w == FParser || w == FParserHelper)
282 FParser->display();
283 FParserHelper->display();
287 void TagEditor::update()
289 if (Dirs->empty())
291 Dirs->Window::clear();
292 Tags->clear();
294 if (itsBrowsedDir != "/")
295 Dirs->addItem(std::make_pair("..", getParentDirectory(itsBrowsedDir)));
296 else
297 Dirs->addItem(std::make_pair(".", "/"));
298 MPD::DirectoryIterator directory = Mpd.GetDirectories(itsBrowsedDir), end;
299 for (; directory != end; ++directory)
301 Dirs->addItem(std::make_pair(getBasename(directory->path()), directory->path()));
302 if (directory->path() == itsHighlightedDir)
303 Dirs->highlight(Dirs->size()-1);
305 std::sort(Dirs->beginV()+1, Dirs->endV(),
306 LocaleBasedSorting(std::locale(), Config.ignore_leading_the));
307 Dirs->display();
310 if (Tags->empty())
312 Tags->reset();
313 MPD::SongIterator s = Mpd.GetSongs(Dirs->current()->value().second), end;
314 for (; s != end; ++s)
315 Tags->addItem(std::move(*s));
316 std::sort(Tags->beginV(), Tags->endV(),
317 LocaleBasedSorting(std::locale(), Config.ignore_leading_the));
318 Tags->refresh();
321 if (w == TagTypes && TagTypes->choice() < 13)
323 Tags->refresh();
325 else if (TagTypes->choice() >= 13)
327 Tags->Window::clear();
328 Tags->Window::refresh();
332 bool TagEditor::enterDirectory()
334 bool result = false;
335 if (w == Dirs && !Dirs->empty())
337 MPD::DirectoryIterator directory = Mpd.GetDirectories(Dirs->current()->value().second), end;
338 bool has_subdirs = directory != end;
339 if (has_subdirs)
341 directory.finish();
342 itsHighlightedDir = itsBrowsedDir;
343 itsBrowsedDir = Dirs->current()->value().second;
344 Dirs->clear();
345 Dirs->reset();
346 result = true;
349 return result;
352 void TagEditor::mouseButtonPressed(MEVENT me)
354 auto tryPreviousColumn = [this]() -> bool {
355 bool result = true;
356 if (w != Dirs)
358 if (previousColumnAvailable())
359 previousColumn();
360 else
361 result = false;
363 return result;
365 auto tryNextColumn = [this]() -> bool {
366 bool result = true;
367 if (w != Tags)
369 if (nextColumnAvailable())
370 nextColumn();
371 else
372 result = false;
374 return result;
376 if (w == FParserDialog)
378 if (FParserDialog->hasCoords(me.x, me.y))
380 if (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED))
382 FParserDialog->Goto(me.y);
383 if (me.bstate & BUTTON3_PRESSED)
384 runAction();
386 else
387 Screen<WindowType>::mouseButtonPressed(me);
390 else if (w == FParser || w == FParserHelper)
392 if (FParser->hasCoords(me.x, me.y))
394 if (w != FParser)
396 if (previousColumnAvailable())
397 previousColumn();
398 else
399 return;
401 if (size_t(me.y) < FParser->size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)))
403 FParser->Goto(me.y);
404 if (me.bstate & BUTTON3_PRESSED)
405 runAction();
407 else
408 Screen<WindowType>::mouseButtonPressed(me);
410 else if (FParserHelper->hasCoords(me.x, me.y))
412 if (w != FParserHelper)
414 if (nextColumnAvailable())
415 nextColumn();
416 else
417 return;
419 scrollpadMouseButtonPressed(*FParserHelper, me);
422 else if (!Dirs->empty() && Dirs->hasCoords(me.x, me.y))
424 if (!tryPreviousColumn() || !tryPreviousColumn())
425 return;
426 if (size_t(me.y) < Dirs->size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)))
428 Dirs->Goto(me.y);
429 if (me.bstate & BUTTON1_PRESSED)
430 enterDirectory();
432 else
433 Screen<WindowType>::mouseButtonPressed(me);
434 Tags->clear();
436 else if (!TagTypes->empty() && TagTypes->hasCoords(me.x, me.y))
438 if (w != TagTypes)
440 bool success;
441 if (w == Dirs)
442 success = tryNextColumn();
443 else
444 success = tryPreviousColumn();
445 if (!success)
446 return;
448 if (size_t(me.y) < TagTypes->size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)))
450 if (!TagTypes->Goto(me.y))
451 return;
452 TagTypes->refresh();
453 Tags->refresh();
454 if (me.bstate & BUTTON3_PRESSED)
455 runAction();
457 else
458 Screen<WindowType>::mouseButtonPressed(me);
460 else if (!Tags->empty() && Tags->hasCoords(me.x, me.y))
462 if (!tryNextColumn() || !tryNextColumn())
463 return;
464 if (size_t(me.y) < Tags->size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)))
466 Tags->Goto(me.y);
467 Tags->refresh();
468 if (me.bstate & BUTTON3_PRESSED)
469 runAction();
471 else
472 Screen<WindowType>::mouseButtonPressed(me);
476 /***********************************************************************/
478 bool TagEditor::allowsSearching()
480 return w == Dirs || w == Tags;
483 void TagEditor::setSearchConstraint(const std::string &constraint)
485 if (w == Dirs)
487 m_directories_search_predicate = Regex::Filter<std::pair<std::string, std::string>>(
488 Regex::make(constraint, Config.regex_type),
489 std::bind(DirEntryMatcher, ph::_1, ph::_2, false)
492 else if (w == Tags)
494 m_songs_search_predicate = Regex::Filter<MPD::MutableSong>(
495 Regex::make(constraint, Config.regex_type),
496 SongEntryMatcher
501 void TagEditor::clearConstraint()
503 if (w == Dirs)
504 m_directories_search_predicate.clear();
505 else if (w == Tags)
506 m_songs_search_predicate.clear();
509 bool TagEditor::find(SearchDirection direction, bool wrap, bool skip_current)
511 bool result = false;
512 if (w == Dirs)
513 result = search(*Dirs, m_directories_search_predicate, direction, wrap, skip_current);
514 else if (w == Tags)
515 result = search(*Tags, m_songs_search_predicate, direction, wrap, skip_current);
516 return result;
519 /***********************************************************************/
521 bool TagEditor::actionRunnable()
523 // TODO: put something more refined here. It requires reworking
524 // runAction though, i.e. splitting it into smaller parts.
525 return (w == Tags && !Tags->empty())
526 || w != Tags;
529 void TagEditor::runAction()
531 using Global::wFooter;
533 if (w == FParserDialog)
535 size_t choice = FParserDialog->choice();
536 if (choice == 2) // cancel
538 w = TagTypes;
539 refresh();
540 return;
542 GetPatternList();
544 // prepare additional windows
546 FParserLegend->clear();
547 *FParserLegend << "%a - artist\n";
548 *FParserLegend << "%A - album artist\n";
549 *FParserLegend << "%t - title\n";
550 *FParserLegend << "%b - album\n";
551 *FParserLegend << "%y - date\n";
552 *FParserLegend << "%n - track number\n";
553 *FParserLegend << "%g - genre\n";
554 *FParserLegend << "%c - composer\n";
555 *FParserLegend << "%p - performer\n";
556 *FParserLegend << "%d - disc\n";
557 *FParserLegend << "%C - comment\n\n";
558 *FParserLegend << NC::Format::Bold << "Files:\n" << NC::Format::NoBold;
559 for (auto it = EditedSongs.begin(); it != EditedSongs.end(); ++it)
560 *FParserLegend << Config.color2 << " * " << NC::Color::End << (*it)->getName() << '\n';
561 FParserLegend->flush();
563 if (!Patterns.empty())
564 Config.pattern = Patterns.front();
565 FParser->clear();
566 FParser->reset();
567 FParser->addItem("Pattern: " + Config.pattern);
568 FParser->addItem("Preview");
569 FParser->addItem("Legend");
570 FParser->addSeparator();
571 FParser->addItem("Proceed");
572 FParser->addItem("Cancel");
573 if (!Patterns.empty())
575 FParser->addSeparator();
576 FParser->addItem("Recent patterns", NC::List::Properties::Bold | NC::List::Properties::Inactive);
577 FParser->addSeparator();
578 for (std::list<std::string>::const_iterator it = Patterns.begin(); it != Patterns.end(); ++it)
579 FParser->addItem(*it);
582 FParser->setTitle(choice == 0 ? "Get tags from filename" : "Rename files");
583 w = FParser;
584 FParserUsePreview = 1;
585 FParserHelper = FParserLegend;
586 FParserHelper->display();
588 else if (w == FParser)
590 bool quit = 0;
591 size_t pos = FParser->choice();
593 if (pos == 4) // save
594 FParserUsePreview = 0;
596 if (pos == 0) // change pattern
598 std::string new_pattern;
600 Statusbar::ScopedLock slock;
601 Statusbar::put() << "Pattern: ";
602 new_pattern = wFooter->prompt(Config.pattern);
604 Config.pattern = new_pattern;
605 FParser->at(0).value() = "Pattern: ";
606 FParser->at(0).value() += Config.pattern;
608 else if (pos == 1 || pos == 4) // preview or proceed
610 bool success = 1;
611 Statusbar::print("Parsing...");
612 FParserPreview->clear();
613 for (auto it = EditedSongs.begin(); it != EditedSongs.end(); ++it)
615 MPD::MutableSong &s = **it;
616 if (FParserDialog->choice() == 0) // get tags from filename
618 if (FParserUsePreview)
620 *FParserPreview << NC::Format::Bold << s.getName() << ":\n" << NC::Format::NoBold;
621 *FParserPreview << ParseFilename(s, Config.pattern, FParserUsePreview) << '\n';
623 else
624 ParseFilename(s, Config.pattern, FParserUsePreview);
626 else // rename files
628 std::string file = s.getName();
629 size_t last_dot = file.rfind(".");
630 std::string extension = file.substr(last_dot);
631 std::string new_file = GenerateFilename(s, "{" + Config.pattern + "}");
632 if (new_file.empty() && !FParserUsePreview)
634 Statusbar::printf("File \"%1%\" would have an empty name", s.getName());
635 FParserUsePreview = 1;
636 success = 0;
638 if (!FParserUsePreview)
639 s.setNewName(new_file + extension);
640 *FParserPreview << file << Config.color2 << " -> " << NC::Color::End;
641 if (new_file.empty())
642 *FParserPreview << Config.empty_tags_color << Config.empty_tag << NC::Color::End;
643 else
644 *FParserPreview << new_file << extension;
645 *FParserPreview << "\n\n";
646 if (!success)
647 break;
650 if (FParserUsePreview)
652 FParserHelper = FParserPreview;
653 FParserHelper->flush();
654 FParserHelper->display();
656 else if (success)
658 Patterns.remove(Config.pattern);
659 Patterns.insert(Patterns.begin(), Config.pattern);
660 quit = 1;
662 if (pos != 4 || success)
663 Statusbar::print("Operation finished");
665 else if (pos == 2) // show legend
667 FParserHelper = FParserLegend;
668 FParserHelper->display();
670 else if (pos == 5) // cancel
672 quit = 1;
674 else // list of patterns
676 Config.pattern = FParser->current()->value();
677 FParser->at(0).value() = "Pattern: " + Config.pattern;
680 if (quit)
682 SavePatternList();
683 w = TagTypes;
684 refresh();
685 return;
689 if ((w != TagTypes && w != Tags) || Tags->empty()) // after this point we start dealing with tags
690 return;
692 EditedSongs.clear();
693 // if there are selected songs, perform operations only on them
694 if (hasSelected(Tags->begin(), Tags->end()))
696 for (auto it = Tags->begin(); it != Tags->end(); ++it)
697 if (it->isSelected())
698 EditedSongs.push_back(&it->value());
700 else
702 for (auto it = Tags->begin(); it != Tags->end(); ++it)
703 EditedSongs.push_back(&it->value());
706 size_t id = TagTypes->choice();
708 if (w == TagTypes && id == 5)
710 Actions::confirmAction("Number tracks?");
711 auto it = EditedSongs.begin();
712 for (unsigned i = 1; i <= EditedSongs.size(); ++i, ++it)
714 if (Config.tag_editor_extended_numeration)
715 (*it)->setTrack(boost::lexical_cast<std::string>(i) + "/" + boost::lexical_cast<std::string>(EditedSongs.size()));
716 else
717 (*it)->setTrack(boost::lexical_cast<std::string>(i));
718 // discard other track number tags
719 (*it)->setTrack("", 1);
721 Statusbar::print("Tracks numbered");
722 return;
725 if (id < 11)
727 MPD::Song::GetFunction get = SongInfo::Tags[id].Get;
728 MPD::MutableSong::SetFunction set = SongInfo::Tags[id].Set;
729 if (id > 0 && w == TagTypes)
731 Statusbar::ScopedLock slock;
732 Statusbar::put() << NC::Format::Bold << TagTypes->current()->value() << NC::Format::NoBold << ": ";
733 std::string new_tag = wFooter->prompt(Tags->current()->value().getTags(get));
734 for (auto it = EditedSongs.begin(); it != EditedSongs.end(); ++it)
735 (*it)->setTags(set, new_tag);
737 else if (w == Tags)
739 Statusbar::ScopedLock slock;
740 Statusbar::put() << NC::Format::Bold << TagTypes->current()->value() << NC::Format::NoBold << ": ";
741 std::string new_tag = wFooter->prompt(Tags->current()->value().getTags(get));
742 if (new_tag != Tags->current()->value().getTags(get))
743 Tags->current()->value().setTags(set, new_tag);
744 Tags->scroll(NC::Scroll::Down);
747 else
749 if (id == 12) // filename related options
751 if (w == TagTypes)
753 FParserDialog->reset();
754 w = FParserDialog;
756 else if (w == Tags)
758 Statusbar::ScopedLock slock;
759 MPD::MutableSong &s = Tags->current()->value();
760 std::string old_name = s.getNewName().empty() ? s.getName() : s.getNewName();
761 size_t last_dot = old_name.rfind(".");
762 std::string extension = old_name.substr(last_dot);
763 old_name = old_name.substr(0, last_dot);
764 Statusbar::put() << NC::Format::Bold << "New filename: " << NC::Format::NoBold;
765 std::string new_name = wFooter->prompt(old_name);
766 if (!new_name.empty())
767 s.setNewName(new_name + extension);
768 Tags->scroll(NC::Scroll::Down);
771 else if (id == TagTypes->size()-5) // capitalize first letters
773 Statusbar::print("Processing...");
774 for (auto it = EditedSongs.begin(); it != EditedSongs.end(); ++it)
775 CapitalizeFirstLetters(**it);
776 Statusbar::print("Done");
778 else if (id == TagTypes->size()-4) // lower all letters
780 Statusbar::print("Processing...");
781 for (auto it = EditedSongs.begin(); it != EditedSongs.end(); ++it)
782 LowerAllLetters(**it);
783 Statusbar::print("Done");
785 else if (id == TagTypes->size()-2) // reset
787 for (auto it = Tags->beginV(); it != Tags->endV(); ++it)
788 it->clearModifications();
789 Statusbar::print("Changes reset");
791 else if (id == TagTypes->size()-1) // save
793 bool success = 1;
794 Statusbar::print("Writing changes...");
795 for (auto it = EditedSongs.begin(); it != EditedSongs.end(); ++it)
797 Statusbar::printf("Writing tags in \"%1%\"...", (*it)->getName());
798 if (!Tags::write(**it))
800 const char msg[] = "Error while writing tags in \"%1%\"";
801 Statusbar::printf(msg, wideShorten((*it)->getURI(), COLS-const_strlen(msg)).c_str());
802 success = 0;
803 break;
806 if (success)
808 Statusbar::print("Tags updated");
809 TagTypes->setHighlightColor(Config.main_highlight_color);
810 TagTypes->reset();
811 w->refresh();
812 w = Dirs;
813 Dirs->setHighlightColor(Config.active_column_color);
814 Mpd.UpdateDirectory(getSharedDirectory(Tags->beginV(), Tags->endV()));
816 else
817 Tags->clear();
823 /***********************************************************************/
825 bool TagEditor::itemAvailable()
827 if (w == Tags)
828 return !Tags->empty();
829 return false;
832 bool TagEditor::addItemToPlaylist(bool play)
834 return addSongToPlaylist(*Tags->currentV(), play);
837 std::vector<MPD::Song> TagEditor::getSelectedSongs()
839 std::vector<MPD::Song> result;
840 if (w == Tags)
842 for (auto it = Tags->begin(); it != Tags->end(); ++it)
843 if (it->isSelected())
844 result.push_back(it->value());
845 // if no song was selected, add current one
846 if (result.empty() && !Tags->empty())
847 result.push_back(Tags->current()->value());
849 return result;
852 /***********************************************************************/
854 bool TagEditor::previousColumnAvailable()
856 bool result = false;
857 if (w == Tags)
859 if (!TagTypes->empty() && !Dirs->empty())
860 result = true;
862 else if (w == TagTypes)
864 if (!Dirs->empty() && isAnyModified(*Tags))
865 Actions::confirmAction("There are pending changes, are you sure?");
866 result = true;
868 else if (w == FParserHelper)
869 result = true;
870 return result;
873 void TagEditor::previousColumn()
875 if (w == Tags)
877 Tags->setHighlightColor(Config.main_highlight_color);
878 w->refresh();
879 w = TagTypes;
880 TagTypes->setHighlightColor(Config.active_column_color);
882 else if (w == TagTypes)
884 TagTypes->setHighlightColor(Config.main_highlight_color);
885 w->refresh();
886 w = Dirs;
887 Dirs->setHighlightColor(Config.active_column_color);
889 else if (w == FParserHelper)
891 FParserHelper->setBorder(Config.window_border);
892 FParserHelper->display();
893 w = FParser;
894 FParser->setBorder(Config.active_window_border);
895 FParser->display();
899 bool TagEditor::nextColumnAvailable()
901 bool result = false;
902 if (w == Dirs)
904 if (!TagTypes->empty() && !Tags->empty())
905 result = true;
907 else if (w == TagTypes)
909 if (!Tags->empty())
910 result = true;
912 else if (w == FParser)
913 result = true;
914 return result;
917 void TagEditor::nextColumn()
919 if (w == Dirs)
921 Dirs->setHighlightColor(Config.main_highlight_color);
922 w->refresh();
923 w = TagTypes;
924 TagTypes->setHighlightColor(Config.active_column_color);
926 else if (w == TagTypes && TagTypes->choice() < 13 && !Tags->empty())
928 TagTypes->setHighlightColor(Config.main_highlight_color);
929 w->refresh();
930 w = Tags;
931 Tags->setHighlightColor(Config.active_column_color);
933 else if (w == FParser)
935 FParser->setBorder(Config.window_border);
936 FParser->display();
937 w = FParserHelper;
938 FParserHelper->setBorder(Config.active_window_border);
939 FParserHelper->display();
943 /***********************************************************************/
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() == s)
1001 Tags->highlight(i);
1002 break;
1007 namespace {
1009 bool isAnyModified(const NC::Menu<MPD::MutableSong> &m)
1011 for (auto it = m.beginV(); it != m.endV(); ++it)
1012 if (it->isModified())
1013 return true;
1014 return false;
1017 std::string CapitalizeFirstLetters(const std::string &s)
1019 std::wstring ws = ToWString(s);
1020 wchar_t prev = 0;
1021 for (auto it = ws.begin(); it != ws.end(); ++it)
1023 if (!iswalpha(prev) && prev != L'\'')
1024 *it = towupper(*it);
1025 prev = *it;
1027 return ToString(ws);
1030 void CapitalizeFirstLetters(MPD::MutableSong &s)
1032 for (const SongInfo::Metadata *m = SongInfo::Tags; m->Name; ++m)
1034 unsigned i = 0;
1035 for (std::string tag; !(tag = (s.*m->Get)(i)).empty(); ++i)
1036 (s.*m->Set)(CapitalizeFirstLetters(tag), i);
1040 void LowerAllLetters(MPD::MutableSong &s)
1042 for (const SongInfo::Metadata *m = SongInfo::Tags; m->Name; ++m)
1044 unsigned i = 0;
1045 for (std::string tag; !(tag = (s.*m->Get)(i)).empty(); ++i)
1046 (s.*m->Set)(boost::locale::to_lower(tag), i);
1050 void GetPatternList()
1052 if (Patterns.empty())
1054 std::ifstream input(PatternsFile.c_str());
1055 if (input.is_open())
1057 std::string line;
1058 while (std::getline(input, line))
1059 if (!line.empty())
1060 Patterns.push_back(line);
1061 input.close();
1066 void 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();
1077 MPD::MutableSong::SetFunction IntoSetFunction(char c)
1079 switch (c)
1081 case 'a':
1082 return &MPD::MutableSong::setArtist;
1083 case 'A':
1084 return &MPD::MutableSong::setAlbumArtist;
1085 case 't':
1086 return &MPD::MutableSong::setTitle;
1087 case 'b':
1088 return &MPD::MutableSong::setAlbum;
1089 case 'y':
1090 return &MPD::MutableSong::setDate;
1091 case 'n':
1092 return &MPD::MutableSong::setTrack;
1093 case 'g':
1094 return &MPD::MutableSong::setGenre;
1095 case 'c':
1096 return &MPD::MutableSong::setComposer;
1097 case 'p':
1098 return &MPD::MutableSong::setPerformer;
1099 case 'd':
1100 return &MPD::MutableSong::setDisc;
1101 case 'C':
1102 return &MPD::MutableSong::setComment;
1103 default:
1104 return 0;
1108 std::string GenerateFilename(const MPD::MutableSong &s, const std::string &pattern)
1110 std::string result = Format::stringify<char>(Format::parse(pattern), &s);
1111 removeInvalidCharsFromFilename(result, Config.generate_win32_compatible_filenames);
1112 return result;
1115 std::string ParseFilename(MPD::MutableSong &s, std::string mask, bool preview)
1117 std::ostringstream result;
1118 std::vector<std::string> separators;
1119 std::vector< std::pair<char, std::string> > tags;
1120 std::string file = s.getName().substr(0, s.getName().rfind("."));
1122 size_t i = mask.find("%");
1124 if (!mask.substr(0, i).empty())
1125 file = file.substr(i);
1127 for (; i != std::string::npos; i = mask.find("%"))
1129 tags.push_back(std::make_pair(mask.at(i+1), ""));
1130 mask = mask.substr(i+2);
1131 i = mask.find("%");
1132 if (!mask.empty())
1133 separators.push_back(mask.substr(0, i));
1135 i = 0;
1136 for (auto it = separators.begin(); it != separators.end(); ++it, ++i)
1138 size_t j = file.find(*it);
1139 tags.at(i).second = file.substr(0, j);
1140 if (j+it->length() > file.length())
1141 goto PARSE_FAILED;
1142 file = file.substr(j+it->length());
1144 if (!file.empty())
1146 if (i >= tags.size())
1147 goto PARSE_FAILED;
1148 tags.at(i).second = file;
1151 if (0) // tss...
1153 PARSE_FAILED:
1154 return "Error while parsing filename!\n";
1157 for (auto it = tags.begin(); it != tags.end(); ++it)
1159 for (std::string::iterator j = it->second.begin(); j != it->second.end(); ++j)
1160 if (*j == '_')
1161 *j = ' ';
1163 if (!preview)
1165 MPD::MutableSong::SetFunction set = IntoSetFunction(it->first);
1166 if (set)
1167 s.setTags(set, it->second);
1169 else
1170 result << "%" << it->first << ": " << it->second << "\n";
1172 return result.str();
1175 std::string SongToString(const MPD::MutableSong &s)
1177 std::string result;
1178 size_t i = myTagEditor->TagTypes->choice();
1179 if (i < 11)
1180 result = (s.*SongInfo::Tags[i].Get)(0);
1181 else if (i == 12)
1182 result = s.getNewName().empty() ? s.getName() : s.getName() + " -> " + s.getNewName();
1183 return result.empty() ? Config.empty_tag : result;
1186 bool DirEntryMatcher(const Regex::Regex &rx, const std::pair<std::string, std::string> &dir, bool filter)
1188 if (dir.first == "." || dir.first == "..")
1189 return filter;
1190 return Regex::search(dir.first, rx);
1193 bool SongEntryMatcher(const Regex::Regex &rx, const MPD::MutableSong &s)
1195 return Regex::search(SongToString(s), rx);
1200 #endif