actions: use unique_ptr for storing actions
[ncmpcpp.git] / src / tag_editor.cpp
blob3dcaac5f65f998c1d36acf04b9363ac75254b264
1 /***************************************************************************
2 * Copyright (C) 2008-2016 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 const std::string &TagEditor::searchConstraint()
485 if (w == Dirs)
486 return m_directories_search_predicate.constraint();
487 else if (w == Tags)
488 return m_songs_search_predicate.constraint();
489 throw std::runtime_error("shouldn't happen due to condition in allowsSearching");
492 void TagEditor::setSearchConstraint(const std::string &constraint)
494 if (w == Dirs)
496 m_directories_search_predicate = Regex::Filter<std::pair<std::string, std::string>>(
497 constraint,
498 Config.regex_type,
499 std::bind(DirEntryMatcher, ph::_1, ph::_2, false));
501 else if (w == Tags)
503 m_songs_search_predicate = Regex::Filter<MPD::MutableSong>(
504 constraint,
505 Config.regex_type,
506 SongEntryMatcher);
510 void TagEditor::clearSearchConstraint()
512 if (w == Dirs)
513 m_directories_search_predicate.clear();
514 else if (w == Tags)
515 m_songs_search_predicate.clear();
518 bool TagEditor::search(SearchDirection direction, bool wrap, bool skip_current)
520 bool result = false;
521 if (w == Dirs)
522 result = ::search(*Dirs, m_directories_search_predicate, direction, wrap, skip_current);
523 else if (w == Tags)
524 result = ::search(*Tags, m_songs_search_predicate, direction, wrap, skip_current);
525 return result;
528 /***********************************************************************/
530 bool TagEditor::actionRunnable()
532 // TODO: put something more refined here. It requires reworking
533 // runAction though, i.e. splitting it into smaller parts.
534 return (w == Tags && !Tags->empty())
535 || w != Tags;
538 void TagEditor::runAction()
540 using Global::wFooter;
542 if (w == FParserDialog)
544 size_t choice = FParserDialog->choice();
545 if (choice == 2) // cancel
547 w = TagTypes;
548 refresh();
549 return;
551 GetPatternList();
553 // prepare additional windows
555 FParserLegend->clear();
556 *FParserLegend << "%a - artist\n";
557 *FParserLegend << "%A - album artist\n";
558 *FParserLegend << "%t - title\n";
559 *FParserLegend << "%b - album\n";
560 *FParserLegend << "%y - date\n";
561 *FParserLegend << "%n - track number\n";
562 *FParserLegend << "%g - genre\n";
563 *FParserLegend << "%c - composer\n";
564 *FParserLegend << "%p - performer\n";
565 *FParserLegend << "%d - disc\n";
566 *FParserLegend << "%C - comment\n\n";
567 *FParserLegend << NC::Format::Bold << "Files:\n" << NC::Format::NoBold;
568 for (auto it = EditedSongs.begin(); it != EditedSongs.end(); ++it)
569 *FParserLegend << Config.color2 << " * " << NC::Color::End << (*it)->getName() << '\n';
570 FParserLegend->flush();
572 if (!Patterns.empty())
573 Config.pattern = Patterns.front();
574 FParser->clear();
575 FParser->reset();
576 FParser->addItem("Pattern: " + Config.pattern);
577 FParser->addItem("Preview");
578 FParser->addItem("Legend");
579 FParser->addSeparator();
580 FParser->addItem("Proceed");
581 FParser->addItem("Cancel");
582 if (!Patterns.empty())
584 FParser->addSeparator();
585 FParser->addItem("Recent patterns", NC::List::Properties::Bold | NC::List::Properties::Inactive);
586 FParser->addSeparator();
587 for (std::list<std::string>::const_iterator it = Patterns.begin(); it != Patterns.end(); ++it)
588 FParser->addItem(*it);
591 FParser->setTitle(choice == 0 ? "Get tags from filename" : "Rename files");
592 w = FParser;
593 FParserUsePreview = 1;
594 FParserHelper = FParserLegend;
595 FParserHelper->display();
597 else if (w == FParser)
599 bool quit = 0;
600 size_t pos = FParser->choice();
602 if (pos == 4) // save
603 FParserUsePreview = 0;
605 if (pos == 0) // change pattern
607 std::string new_pattern;
609 Statusbar::ScopedLock slock;
610 Statusbar::put() << "Pattern: ";
611 new_pattern = wFooter->prompt(Config.pattern);
613 Config.pattern = new_pattern;
614 FParser->at(0).value() = "Pattern: ";
615 FParser->at(0).value() += Config.pattern;
617 else if (pos == 1 || pos == 4) // preview or proceed
619 bool success = 1;
620 Statusbar::print("Parsing...");
621 FParserPreview->clear();
622 for (auto it = EditedSongs.begin(); it != EditedSongs.end(); ++it)
624 MPD::MutableSong &s = **it;
625 if (FParserDialog->choice() == 0) // get tags from filename
627 if (FParserUsePreview)
629 *FParserPreview << NC::Format::Bold << s.getName() << ":\n" << NC::Format::NoBold;
630 *FParserPreview << ParseFilename(s, Config.pattern, FParserUsePreview) << '\n';
632 else
633 ParseFilename(s, Config.pattern, FParserUsePreview);
635 else // rename files
637 std::string file = s.getName();
638 size_t last_dot = file.rfind(".");
639 std::string extension = file.substr(last_dot);
640 std::string new_file = GenerateFilename(s, "{" + Config.pattern + "}");
641 if (new_file.empty() && !FParserUsePreview)
643 Statusbar::printf("File \"%1%\" would have an empty name", s.getName());
644 FParserUsePreview = 1;
645 success = 0;
647 if (!FParserUsePreview)
648 s.setNewName(new_file + extension);
649 *FParserPreview << file << Config.color2 << " -> " << NC::Color::End;
650 if (new_file.empty())
651 *FParserPreview << Config.empty_tags_color << Config.empty_tag << NC::Color::End;
652 else
653 *FParserPreview << new_file << extension;
654 *FParserPreview << "\n\n";
655 if (!success)
656 break;
659 if (FParserUsePreview)
661 FParserHelper = FParserPreview;
662 FParserHelper->flush();
663 FParserHelper->display();
665 else if (success)
667 Patterns.remove(Config.pattern);
668 Patterns.insert(Patterns.begin(), Config.pattern);
669 quit = 1;
671 if (pos != 4 || success)
672 Statusbar::print("Operation finished");
674 else if (pos == 2) // show legend
676 FParserHelper = FParserLegend;
677 FParserHelper->display();
679 else if (pos == 5) // cancel
681 quit = 1;
683 else // list of patterns
685 Config.pattern = FParser->current()->value();
686 FParser->at(0).value() = "Pattern: " + Config.pattern;
689 if (quit)
691 SavePatternList();
692 w = TagTypes;
693 refresh();
694 return;
698 if ((w != TagTypes && w != Tags) || Tags->empty()) // after this point we start dealing with tags
699 return;
701 EditedSongs.clear();
702 // if there are selected songs, perform operations only on them
703 if (hasSelected(Tags->begin(), Tags->end()))
705 for (auto it = Tags->begin(); it != Tags->end(); ++it)
706 if (it->isSelected())
707 EditedSongs.push_back(&it->value());
709 else
711 for (auto it = Tags->begin(); it != Tags->end(); ++it)
712 EditedSongs.push_back(&it->value());
715 size_t id = TagTypes->choice();
717 if (w == TagTypes && id == 5)
719 Actions::confirmAction("Number tracks?");
720 auto it = EditedSongs.begin();
721 for (unsigned i = 1; i <= EditedSongs.size(); ++i, ++it)
723 if (Config.tag_editor_extended_numeration)
724 (*it)->setTrack(boost::lexical_cast<std::string>(i) + "/" + boost::lexical_cast<std::string>(EditedSongs.size()));
725 else
726 (*it)->setTrack(boost::lexical_cast<std::string>(i));
727 // discard other track number tags
728 (*it)->setTrack("", 1);
730 Statusbar::print("Tracks numbered");
731 return;
734 if (id < 11)
736 MPD::Song::GetFunction get = SongInfo::Tags[id].Get;
737 MPD::MutableSong::SetFunction set = SongInfo::Tags[id].Set;
738 if (id > 0 && w == TagTypes)
740 Statusbar::ScopedLock slock;
741 Statusbar::put() << NC::Format::Bold << TagTypes->current()->value() << NC::Format::NoBold << ": ";
742 std::string new_tag = wFooter->prompt(Tags->current()->value().getTags(get));
743 for (auto it = EditedSongs.begin(); it != EditedSongs.end(); ++it)
744 (*it)->setTags(set, new_tag);
746 else if (w == Tags)
748 Statusbar::ScopedLock slock;
749 Statusbar::put() << NC::Format::Bold << TagTypes->current()->value() << NC::Format::NoBold << ": ";
750 std::string new_tag = wFooter->prompt(Tags->current()->value().getTags(get));
751 if (new_tag != Tags->current()->value().getTags(get))
752 Tags->current()->value().setTags(set, new_tag);
753 Tags->scroll(NC::Scroll::Down);
756 else
758 if (id == 12) // filename related options
760 if (w == TagTypes)
762 FParserDialog->reset();
763 w = FParserDialog;
765 else if (w == Tags)
767 Statusbar::ScopedLock slock;
768 MPD::MutableSong &s = Tags->current()->value();
769 std::string old_name = s.getNewName().empty() ? s.getName() : s.getNewName();
770 size_t last_dot = old_name.rfind(".");
771 std::string extension = old_name.substr(last_dot);
772 old_name = old_name.substr(0, last_dot);
773 Statusbar::put() << NC::Format::Bold << "New filename: " << NC::Format::NoBold;
774 std::string new_name = wFooter->prompt(old_name);
775 if (!new_name.empty())
776 s.setNewName(new_name + extension);
777 Tags->scroll(NC::Scroll::Down);
780 else if (id == TagTypes->size()-5) // capitalize first letters
782 Statusbar::print("Processing...");
783 for (auto it = EditedSongs.begin(); it != EditedSongs.end(); ++it)
784 CapitalizeFirstLetters(**it);
785 Statusbar::print("Done");
787 else if (id == TagTypes->size()-4) // lower all letters
789 Statusbar::print("Processing...");
790 for (auto it = EditedSongs.begin(); it != EditedSongs.end(); ++it)
791 LowerAllLetters(**it);
792 Statusbar::print("Done");
794 else if (id == TagTypes->size()-2) // reset
796 for (auto it = Tags->beginV(); it != Tags->endV(); ++it)
797 it->clearModifications();
798 Statusbar::print("Changes reset");
800 else if (id == TagTypes->size()-1) // save
802 bool success = 1;
803 Statusbar::print("Writing changes...");
804 for (auto it = EditedSongs.begin(); it != EditedSongs.end(); ++it)
806 Statusbar::printf("Writing tags in \"%1%\"...", (*it)->getName());
807 if (!Tags::write(**it))
809 Statusbar::printf("Error while writing tags to \"%1%\": %2%",
810 (*it)->getName(), strerror(errno));
811 success = 0;
812 break;
815 if (success)
817 Statusbar::print("Tags updated");
818 TagTypes->setHighlightColor(Config.main_highlight_color);
819 TagTypes->reset();
820 w->refresh();
821 w = Dirs;
822 Dirs->setHighlightColor(Config.active_column_color);
823 Mpd.UpdateDirectory(getSharedDirectory(Tags->beginV(), Tags->endV()));
825 else
826 Tags->clear();
832 /***********************************************************************/
834 bool TagEditor::itemAvailable()
836 if (w == Tags)
837 return !Tags->empty();
838 return false;
841 bool TagEditor::addItemToPlaylist(bool play)
843 return addSongToPlaylist(*Tags->currentV(), play);
846 std::vector<MPD::Song> TagEditor::getSelectedSongs()
848 std::vector<MPD::Song> result;
849 if (w == Tags)
851 for (auto it = Tags->begin(); it != Tags->end(); ++it)
852 if (it->isSelected())
853 result.push_back(it->value());
854 // if no song was selected, add current one
855 if (result.empty() && !Tags->empty())
856 result.push_back(Tags->current()->value());
858 return result;
861 /***********************************************************************/
863 bool TagEditor::previousColumnAvailable()
865 bool result = false;
866 if (w == Tags)
868 if (!TagTypes->empty() && !Dirs->empty())
869 result = true;
871 else if (w == TagTypes)
873 if (!Dirs->empty() && isAnyModified(*Tags))
874 Actions::confirmAction("There are pending changes, are you sure?");
875 result = true;
877 else if (w == FParserHelper)
878 result = true;
879 return result;
882 void TagEditor::previousColumn()
884 if (w == Tags)
886 Tags->setHighlightColor(Config.main_highlight_color);
887 w->refresh();
888 w = TagTypes;
889 TagTypes->setHighlightColor(Config.active_column_color);
891 else if (w == TagTypes)
893 TagTypes->setHighlightColor(Config.main_highlight_color);
894 w->refresh();
895 w = Dirs;
896 Dirs->setHighlightColor(Config.active_column_color);
898 else if (w == FParserHelper)
900 FParserHelper->setBorder(Config.window_border);
901 FParserHelper->display();
902 w = FParser;
903 FParser->setBorder(Config.active_window_border);
904 FParser->display();
908 bool TagEditor::nextColumnAvailable()
910 bool result = false;
911 if (w == Dirs)
913 if (!TagTypes->empty() && !Tags->empty())
914 result = true;
916 else if (w == TagTypes)
918 if (!Tags->empty())
919 result = true;
921 else if (w == FParser)
922 result = true;
923 return result;
926 void TagEditor::nextColumn()
928 if (w == Dirs)
930 Dirs->setHighlightColor(Config.main_highlight_color);
931 w->refresh();
932 w = TagTypes;
933 TagTypes->setHighlightColor(Config.active_column_color);
935 else if (w == TagTypes && TagTypes->choice() < 13 && !Tags->empty())
937 TagTypes->setHighlightColor(Config.main_highlight_color);
938 w->refresh();
939 w = Tags;
940 Tags->setHighlightColor(Config.active_column_color);
942 else if (w == FParser)
944 FParser->setBorder(Config.window_border);
945 FParser->display();
946 w = FParserHelper;
947 FParserHelper->setBorder(Config.active_window_border);
948 FParserHelper->display();
952 /***********************************************************************/
954 void TagEditor::LocateSong(const MPD::Song &s)
956 if (myScreen == this)
957 return;
959 if (s.getDirectory().empty())
960 return;
962 if (Global::myScreen != this)
963 switchTo();
965 // go to right directory
966 if (itsBrowsedDir != s.getDirectory())
968 itsBrowsedDir = s.getDirectory();
969 size_t last_slash = itsBrowsedDir.rfind('/');
970 if (last_slash != std::string::npos)
971 itsBrowsedDir = itsBrowsedDir.substr(0, last_slash);
972 else
973 itsBrowsedDir = "/";
974 if (itsBrowsedDir.empty())
975 itsBrowsedDir = "/";
976 Dirs->clear();
977 update();
979 if (itsBrowsedDir == "/")
980 Dirs->reset(); // go to the first pos, which is "." (music dir root)
982 // highlight directory we need and get files from it
983 std::string dir = getBasename(s.getDirectory());
984 for (size_t i = 0; i < Dirs->size(); ++i)
986 if ((*Dirs)[i].value().first == dir)
988 Dirs->highlight(i);
989 break;
992 // refresh window so we can be highlighted item
993 Dirs->refresh();
995 Tags->clear();
996 update();
998 // reset TagTypes since it can be under Filename
999 // and then songs in right column are not visible.
1000 TagTypes->reset();
1001 // go to the right column
1002 nextColumn();
1003 nextColumn();
1005 // highlight our file
1006 for (size_t i = 0; i < Tags->size(); ++i)
1008 if ((*Tags)[i].value() == s)
1010 Tags->highlight(i);
1011 break;
1016 namespace {
1018 bool isAnyModified(const NC::Menu<MPD::MutableSong> &m)
1020 for (auto it = m.beginV(); it != m.endV(); ++it)
1021 if (it->isModified())
1022 return true;
1023 return false;
1026 std::string CapitalizeFirstLetters(const std::string &s)
1028 std::wstring ws = ToWString(s);
1029 wchar_t prev = 0;
1030 for (auto it = ws.begin(); it != ws.end(); ++it)
1032 if (!iswalpha(prev) && prev != L'\'')
1033 *it = towupper(*it);
1034 prev = *it;
1036 return ToString(ws);
1039 void CapitalizeFirstLetters(MPD::MutableSong &s)
1041 for (const SongInfo::Metadata *m = SongInfo::Tags; m->Name; ++m)
1043 unsigned i = 0;
1044 for (std::string tag; !(tag = (s.*m->Get)(i)).empty(); ++i)
1045 (s.*m->Set)(CapitalizeFirstLetters(tag), i);
1049 void LowerAllLetters(MPD::MutableSong &s)
1051 for (const SongInfo::Metadata *m = SongInfo::Tags; m->Name; ++m)
1053 unsigned i = 0;
1054 for (std::string tag; !(tag = (s.*m->Get)(i)).empty(); ++i)
1055 (s.*m->Set)(boost::locale::to_lower(tag), i);
1059 void GetPatternList()
1061 if (Patterns.empty())
1063 std::ifstream input(PatternsFile.c_str());
1064 if (input.is_open())
1066 std::string line;
1067 while (std::getline(input, line))
1068 if (!line.empty())
1069 Patterns.push_back(line);
1070 input.close();
1075 void SavePatternList()
1077 std::ofstream output(PatternsFile.c_str());
1078 if (output.is_open())
1080 std::list<std::string>::const_iterator it = Patterns.begin();
1081 for (unsigned i = 30; it != Patterns.end() && i; ++it, --i)
1082 output << *it << std::endl;
1083 output.close();
1086 MPD::MutableSong::SetFunction IntoSetFunction(char c)
1088 switch (c)
1090 case 'a':
1091 return &MPD::MutableSong::setArtist;
1092 case 'A':
1093 return &MPD::MutableSong::setAlbumArtist;
1094 case 't':
1095 return &MPD::MutableSong::setTitle;
1096 case 'b':
1097 return &MPD::MutableSong::setAlbum;
1098 case 'y':
1099 return &MPD::MutableSong::setDate;
1100 case 'n':
1101 return &MPD::MutableSong::setTrack;
1102 case 'g':
1103 return &MPD::MutableSong::setGenre;
1104 case 'c':
1105 return &MPD::MutableSong::setComposer;
1106 case 'p':
1107 return &MPD::MutableSong::setPerformer;
1108 case 'd':
1109 return &MPD::MutableSong::setDisc;
1110 case 'C':
1111 return &MPD::MutableSong::setComment;
1112 default:
1113 return 0;
1117 std::string GenerateFilename(const MPD::MutableSong &s, const std::string &pattern)
1119 std::string result = Format::stringify<char>(Format::parse(pattern), &s);
1120 removeInvalidCharsFromFilename(result, Config.generate_win32_compatible_filenames);
1121 return result;
1124 std::string ParseFilename(MPD::MutableSong &s, std::string mask, bool preview)
1126 std::ostringstream result;
1127 std::vector<std::string> separators;
1128 std::vector< std::pair<char, std::string> > tags;
1129 std::string file = s.getName().substr(0, s.getName().rfind("."));
1131 size_t i = mask.find("%");
1133 if (!mask.substr(0, i).empty())
1134 file = file.substr(i);
1136 for (; i != std::string::npos; i = mask.find("%"))
1138 tags.push_back(std::make_pair(mask.at(i+1), ""));
1139 mask = mask.substr(i+2);
1140 i = mask.find("%");
1141 if (!mask.empty())
1142 separators.push_back(mask.substr(0, i));
1144 i = 0;
1145 for (auto it = separators.begin(); it != separators.end(); ++it, ++i)
1147 size_t j = file.find(*it);
1148 tags.at(i).second = file.substr(0, j);
1149 if (j+it->length() > file.length())
1150 goto PARSE_FAILED;
1151 file = file.substr(j+it->length());
1153 if (!file.empty())
1155 if (i >= tags.size())
1156 goto PARSE_FAILED;
1157 tags.at(i).second = file;
1160 if (0) // tss...
1162 PARSE_FAILED:
1163 return "Error while parsing filename!\n";
1166 for (auto it = tags.begin(); it != tags.end(); ++it)
1168 for (std::string::iterator j = it->second.begin(); j != it->second.end(); ++j)
1169 if (*j == '_')
1170 *j = ' ';
1172 if (!preview)
1174 MPD::MutableSong::SetFunction set = IntoSetFunction(it->first);
1175 if (set)
1176 s.setTags(set, it->second);
1178 else
1179 result << "%" << it->first << ": " << it->second << "\n";
1181 return result.str();
1184 std::string SongToString(const MPD::MutableSong &s)
1186 std::string result;
1187 size_t i = myTagEditor->TagTypes->choice();
1188 if (i < 11)
1189 result = (s.*SongInfo::Tags[i].Get)(0);
1190 else if (i == 12)
1191 result = s.getNewName().empty() ? s.getName() : s.getName() + " -> " + s.getNewName();
1192 return result.empty() ? Config.empty_tag : result;
1195 bool DirEntryMatcher(const Regex::Regex &rx, const std::pair<std::string, std::string> &dir, bool filter)
1197 if (dir.first == "." || dir.first == "..")
1198 return filter;
1199 return Regex::search(dir.first, rx);
1202 bool SongEntryMatcher(const Regex::Regex &rx, const MPD::MutableSong &s)
1204 return Regex::search(SongToString(s), rx);
1209 #endif