song list: get rid of boost::zip_iterator and improve {Const,}SongIterator
[ncmpcpp.git] / src / tag_editor.cpp
blob645f8c91a998dc96a3a7dcaf78411ec808ad4519
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);
92 SongIterator TagsWindow::currentS()
94 return makeSongIterator(current());
97 ConstSongIterator TagsWindow::currentS() const
99 return makeConstSongIterator(current());
102 SongIterator TagsWindow::beginS()
104 return makeSongIterator(begin());
107 ConstSongIterator TagsWindow::beginS() const
109 return makeConstSongIterator(begin());
112 SongIterator TagsWindow::endS()
114 return makeSongIterator(end());
117 ConstSongIterator TagsWindow::endS() const
119 return makeConstSongIterator(end());
122 std::vector<MPD::Song> TagsWindow::getSelectedSongs()
124 return {}; // TODO
127 /**********************************************************************/
129 TagEditor::TagEditor() : FParser(0), FParserHelper(0), FParserLegend(0), FParserPreview(0), itsBrowsedDir("/")
131 PatternsFile = Config.ncmpcpp_directory + "patterns.list";
132 SetDimensions(0, COLS);
134 Dirs = new NC::Menu< std::pair<std::string, std::string> >(0, MainStartY, LeftColumnWidth, MainHeight, Config.titles_visibility ? "Directories" : "", Config.main_color, NC::Border());
135 Dirs->setHighlightColor(Config.active_column_color);
136 Dirs->cyclicScrolling(Config.use_cyclic_scrolling);
137 Dirs->centeredCursor(Config.centered_cursor);
138 Dirs->setItemDisplayer([](NC::Menu<std::pair<std::string, std::string>> &menu) {
139 menu << Charset::utf8ToLocale(menu.drawn()->value().first);
142 TagTypes = new NC::Menu<std::string>(MiddleColumnStartX, MainStartY, MiddleColumnWidth, MainHeight, Config.titles_visibility ? "Tag types" : "", Config.main_color, NC::Border());
143 TagTypes->setHighlightColor(Config.main_highlight_color);
144 TagTypes->cyclicScrolling(Config.use_cyclic_scrolling);
145 TagTypes->centeredCursor(Config.centered_cursor);
146 TagTypes->setItemDisplayer([](NC::Menu<std::string> &menu) {
147 menu << Charset::utf8ToLocale(menu.drawn()->value());
150 for (const SongInfo::Metadata *m = SongInfo::Tags; m->Name; ++m)
151 TagTypes->addItem(m->Name);
152 TagTypes->addSeparator();
153 TagTypes->addItem("Filename");
154 TagTypes->addSeparator();
155 if (Config.titles_visibility)
157 TagTypes->addItem("Options", NC::List::Properties::Bold | NC::List::Properties::Inactive);
158 TagTypes->addSeparator();
160 TagTypes->addItem("Capitalize First Letters");
161 TagTypes->addItem("lower all letters");
162 TagTypes->addSeparator();
163 TagTypes->addItem("Reset");
164 TagTypes->addItem("Save");
166 Tags = new TagsWindow(NC::Menu<MPD::MutableSong>(RightColumnStartX, MainStartY, RightColumnWidth, MainHeight, Config.titles_visibility ? "Tags" : "", Config.main_color, NC::Border()));
167 Tags->setHighlightColor(Config.main_highlight_color);
168 Tags->cyclicScrolling(Config.use_cyclic_scrolling);
169 Tags->centeredCursor(Config.centered_cursor);
170 Tags->setSelectedPrefix(Config.selected_item_prefix);
171 Tags->setSelectedSuffix(Config.selected_item_suffix);
172 Tags->setItemDisplayer(Display::Tags);
174 auto parser_display = [](NC::Menu<std::string> &menu) {
175 menu << Charset::utf8ToLocale(menu.drawn()->value());
178 FParserDialog = new NC::Menu<std::string>((COLS-FParserDialogWidth)/2, (MainHeight-FParserDialogHeight)/2+MainStartY, FParserDialogWidth, FParserDialogHeight, "", Config.main_color, Config.window_border);
179 FParserDialog->cyclicScrolling(Config.use_cyclic_scrolling);
180 FParserDialog->centeredCursor(Config.centered_cursor);
181 FParserDialog->setItemDisplayer(parser_display);
182 FParserDialog->addItem("Get tags from filename");
183 FParserDialog->addItem("Rename files");
184 FParserDialog->addItem("Cancel");
186 FParser = new NC::Menu<std::string>((COLS-FParserWidth)/2, (MainHeight-FParserHeight)/2+MainStartY, FParserWidthOne, FParserHeight, "_", Config.main_color, Config.active_window_border);
187 FParser->cyclicScrolling(Config.use_cyclic_scrolling);
188 FParser->centeredCursor(Config.centered_cursor);
189 FParser->setItemDisplayer(parser_display);
191 FParserLegend = new NC::Scrollpad((COLS-FParserWidth)/2+FParserWidthOne, (MainHeight-FParserHeight)/2+MainStartY, FParserWidthTwo, FParserHeight, "Legend", Config.main_color, Config.window_border);
193 FParserPreview = new NC::Scrollpad((COLS-FParserWidth)/2+FParserWidthOne, (MainHeight-FParserHeight)/2+MainStartY, FParserWidthTwo, FParserHeight, "Preview", Config.main_color, Config.window_border);
195 w = Dirs;
198 void TagEditor::SetDimensions(size_t x_offset, size_t width)
200 MiddleColumnWidth = std::min(26, COLS-2);
201 LeftColumnStartX = x_offset;
202 LeftColumnWidth = (width-MiddleColumnWidth)/2;
203 MiddleColumnStartX = LeftColumnStartX+LeftColumnWidth+1;
204 RightColumnWidth = width-LeftColumnWidth-MiddleColumnWidth-2;
205 RightColumnStartX = MiddleColumnStartX+MiddleColumnWidth+1;
207 FParserDialogWidth = std::min(30, COLS);
208 FParserDialogHeight = std::min(size_t(5), MainHeight);
209 FParserWidth = width*0.9;
210 FParserHeight = std::min(size_t(LINES*0.8), MainHeight);
211 FParserWidthOne = FParserWidth/2;
212 FParserWidthTwo = FParserWidth-FParserWidthOne;
215 void TagEditor::resize()
217 size_t x_offset, width;
218 getWindowResizeParams(x_offset, width);
219 SetDimensions(x_offset, width);
221 Dirs->resize(LeftColumnWidth, MainHeight);
222 TagTypes->resize(MiddleColumnWidth, MainHeight);
223 Tags->resize(RightColumnWidth, MainHeight);
224 FParserDialog->resize(FParserDialogWidth, FParserDialogHeight);
225 FParser->resize(FParserWidthOne, FParserHeight);
226 FParserLegend->resize(FParserWidthTwo, FParserHeight);
227 FParserPreview->resize(FParserWidthTwo, FParserHeight);
229 Dirs->moveTo(LeftColumnStartX, MainStartY);
230 TagTypes->moveTo(MiddleColumnStartX, MainStartY);
231 Tags->moveTo(RightColumnStartX, MainStartY);
233 FParserDialog->moveTo(x_offset+(width-FParserDialogWidth)/2, (MainHeight-FParserDialogHeight)/2+MainStartY);
234 FParser->moveTo(x_offset+(width-FParserWidth)/2, (MainHeight-FParserHeight)/2+MainStartY);
235 FParserLegend->moveTo(x_offset+(width-FParserWidth)/2+FParserWidthOne, (MainHeight-FParserHeight)/2+MainStartY);
236 FParserPreview->moveTo(x_offset+(width-FParserWidth)/2+FParserWidthOne, (MainHeight-FParserHeight)/2+MainStartY);
238 hasToBeResized = 0;
241 std::wstring TagEditor::title()
243 return L"Tag editor";
246 void TagEditor::switchTo()
248 SwitchTo::execute(this);
249 drawHeader();
250 refresh();
253 void TagEditor::refresh()
255 Dirs->display();
256 drawSeparator(MiddleColumnStartX-1);
257 TagTypes->display();
258 drawSeparator(RightColumnStartX-1);
259 Tags->display();
261 if (w == FParserDialog)
263 FParserDialog->display();
265 else if (w == FParser || w == FParserHelper)
267 FParser->display();
268 FParserHelper->display();
272 void TagEditor::update()
274 if (Dirs->empty())
276 Dirs->Window::clear();
277 Tags->clear();
279 if (itsBrowsedDir != "/")
280 Dirs->addItem(std::make_pair("..", getParentDirectory(itsBrowsedDir)));
281 else
282 Dirs->addItem(std::make_pair(".", "/"));
283 MPD::DirectoryIterator directory = Mpd.GetDirectories(itsBrowsedDir), end;
284 for (; directory != end; ++directory)
286 Dirs->addItem(std::make_pair(getBasename(directory->path()), directory->path()));
287 if (directory->path() == itsHighlightedDir)
288 Dirs->highlight(Dirs->size()-1);
290 std::sort(Dirs->beginV()+1, Dirs->endV(),
291 LocaleBasedSorting(std::locale(), Config.ignore_leading_the));
292 Dirs->display();
295 if (Tags->empty())
297 Tags->reset();
298 MPD::SongIterator s = Mpd.GetSongs(Dirs->current()->value().second), end;
299 for (; s != end; ++s)
300 Tags->addItem(std::move(*s));
301 std::sort(Tags->beginV(), Tags->endV(),
302 LocaleBasedSorting(std::locale(), Config.ignore_leading_the));
303 Tags->refresh();
306 if (w == TagTypes && TagTypes->choice() < 13)
308 Tags->refresh();
310 else if (TagTypes->choice() >= 13)
312 Tags->Window::clear();
313 Tags->Window::refresh();
317 bool TagEditor::enterDirectory()
319 bool result = false;
320 if (w == Dirs && !Dirs->empty())
322 MPD::DirectoryIterator directory = Mpd.GetDirectories(Dirs->current()->value().second), end;
323 bool has_subdirs = directory != end;
324 if (has_subdirs)
326 directory.finish();
327 itsHighlightedDir = itsBrowsedDir;
328 itsBrowsedDir = Dirs->current()->value().second;
329 Dirs->clear();
330 Dirs->reset();
331 result = true;
334 return result;
337 void TagEditor::mouseButtonPressed(MEVENT me)
339 auto tryPreviousColumn = [this]() -> bool {
340 bool result = true;
341 if (w != Dirs)
343 if (previousColumnAvailable())
344 previousColumn();
345 else
346 result = false;
348 return result;
350 auto tryNextColumn = [this]() -> bool {
351 bool result = true;
352 if (w != Tags)
354 if (nextColumnAvailable())
355 nextColumn();
356 else
357 result = false;
359 return result;
361 if (w == FParserDialog)
363 if (FParserDialog->hasCoords(me.x, me.y))
365 if (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED))
367 FParserDialog->Goto(me.y);
368 if (me.bstate & BUTTON3_PRESSED)
369 runAction();
371 else
372 Screen<WindowType>::mouseButtonPressed(me);
375 else if (w == FParser || w == FParserHelper)
377 if (FParser->hasCoords(me.x, me.y))
379 if (w != FParser)
381 if (previousColumnAvailable())
382 previousColumn();
383 else
384 return;
386 if (size_t(me.y) < FParser->size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)))
388 FParser->Goto(me.y);
389 if (me.bstate & BUTTON3_PRESSED)
390 runAction();
392 else
393 Screen<WindowType>::mouseButtonPressed(me);
395 else if (FParserHelper->hasCoords(me.x, me.y))
397 if (w != FParserHelper)
399 if (nextColumnAvailable())
400 nextColumn();
401 else
402 return;
404 scrollpadMouseButtonPressed(*FParserHelper, me);
407 else if (!Dirs->empty() && Dirs->hasCoords(me.x, me.y))
409 if (!tryPreviousColumn() || !tryPreviousColumn())
410 return;
411 if (size_t(me.y) < Dirs->size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)))
413 Dirs->Goto(me.y);
414 if (me.bstate & BUTTON1_PRESSED)
415 enterDirectory();
417 else
418 Screen<WindowType>::mouseButtonPressed(me);
419 Tags->clear();
421 else if (!TagTypes->empty() && TagTypes->hasCoords(me.x, me.y))
423 if (w != TagTypes)
425 bool success;
426 if (w == Dirs)
427 success = tryNextColumn();
428 else
429 success = tryPreviousColumn();
430 if (!success)
431 return;
433 if (size_t(me.y) < TagTypes->size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)))
435 if (!TagTypes->Goto(me.y))
436 return;
437 TagTypes->refresh();
438 Tags->refresh();
439 if (me.bstate & BUTTON3_PRESSED)
440 runAction();
442 else
443 Screen<WindowType>::mouseButtonPressed(me);
445 else if (!Tags->empty() && Tags->hasCoords(me.x, me.y))
447 if (!tryNextColumn() || !tryNextColumn())
448 return;
449 if (size_t(me.y) < Tags->size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)))
451 Tags->Goto(me.y);
452 Tags->refresh();
453 if (me.bstate & BUTTON3_PRESSED)
454 runAction();
456 else
457 Screen<WindowType>::mouseButtonPressed(me);
461 /***********************************************************************/
463 bool TagEditor::allowsSearching()
465 return w == Dirs || w == Tags;
468 const std::string &TagEditor::searchConstraint()
470 if (w == Dirs)
471 return m_directories_search_predicate.constraint();
472 else if (w == Tags)
473 return m_songs_search_predicate.constraint();
474 throw std::runtime_error("shouldn't happen due to condition in allowsSearching");
477 void TagEditor::setSearchConstraint(const std::string &constraint)
479 if (w == Dirs)
481 m_directories_search_predicate = Regex::Filter<std::pair<std::string, std::string>>(
482 constraint,
483 Config.regex_type,
484 std::bind(DirEntryMatcher, ph::_1, ph::_2, false));
486 else if (w == Tags)
488 m_songs_search_predicate = Regex::Filter<MPD::MutableSong>(
489 constraint,
490 Config.regex_type,
491 SongEntryMatcher);
495 void TagEditor::clearSearchConstraint()
497 if (w == Dirs)
498 m_directories_search_predicate.clear();
499 else if (w == Tags)
500 m_songs_search_predicate.clear();
503 bool TagEditor::search(SearchDirection direction, bool wrap, bool skip_current)
505 bool result = false;
506 if (w == Dirs)
507 result = ::search(*Dirs, m_directories_search_predicate, direction, wrap, skip_current);
508 else if (w == Tags)
509 result = ::search(*Tags, m_songs_search_predicate, direction, wrap, skip_current);
510 return result;
513 /***********************************************************************/
515 bool TagEditor::actionRunnable()
517 // TODO: put something more refined here. It requires reworking
518 // runAction though, i.e. splitting it into smaller parts.
519 return (w == Tags && !Tags->empty())
520 || w != Tags;
523 void TagEditor::runAction()
525 using Global::wFooter;
527 if (w == FParserDialog)
529 size_t choice = FParserDialog->choice();
530 if (choice == 2) // cancel
532 w = TagTypes;
533 refresh();
534 return;
536 GetPatternList();
538 // prepare additional windows
540 FParserLegend->clear();
541 *FParserLegend << "%a - artist\n";
542 *FParserLegend << "%A - album artist\n";
543 *FParserLegend << "%t - title\n";
544 *FParserLegend << "%b - album\n";
545 *FParserLegend << "%y - date\n";
546 *FParserLegend << "%n - track number\n";
547 *FParserLegend << "%g - genre\n";
548 *FParserLegend << "%c - composer\n";
549 *FParserLegend << "%p - performer\n";
550 *FParserLegend << "%d - disc\n";
551 *FParserLegend << "%C - comment\n\n";
552 *FParserLegend << NC::Format::Bold << "Files:\n" << NC::Format::NoBold;
553 for (auto it = EditedSongs.begin(); it != EditedSongs.end(); ++it)
554 *FParserLegend << Config.color2 << " * " << NC::Color::End << (*it)->getName() << '\n';
555 FParserLegend->flush();
557 if (!Patterns.empty())
558 Config.pattern = Patterns.front();
559 FParser->clear();
560 FParser->reset();
561 FParser->addItem("Pattern: " + Config.pattern);
562 FParser->addItem("Preview");
563 FParser->addItem("Legend");
564 FParser->addSeparator();
565 FParser->addItem("Proceed");
566 FParser->addItem("Cancel");
567 if (!Patterns.empty())
569 FParser->addSeparator();
570 FParser->addItem("Recent patterns", NC::List::Properties::Bold | NC::List::Properties::Inactive);
571 FParser->addSeparator();
572 for (std::list<std::string>::const_iterator it = Patterns.begin(); it != Patterns.end(); ++it)
573 FParser->addItem(*it);
576 FParser->setTitle(choice == 0 ? "Get tags from filename" : "Rename files");
577 w = FParser;
578 FParserUsePreview = 1;
579 FParserHelper = FParserLegend;
580 FParserHelper->display();
582 else if (w == FParser)
584 bool quit = 0;
585 size_t pos = FParser->choice();
587 if (pos == 4) // save
588 FParserUsePreview = 0;
590 if (pos == 0) // change pattern
592 std::string new_pattern;
594 Statusbar::ScopedLock slock;
595 Statusbar::put() << "Pattern: ";
596 new_pattern = wFooter->prompt(Config.pattern);
598 Config.pattern = new_pattern;
599 FParser->at(0).value() = "Pattern: ";
600 FParser->at(0).value() += Config.pattern;
602 else if (pos == 1 || pos == 4) // preview or proceed
604 bool success = 1;
605 Statusbar::print("Parsing...");
606 FParserPreview->clear();
607 for (auto it = EditedSongs.begin(); it != EditedSongs.end(); ++it)
609 MPD::MutableSong &s = **it;
610 if (FParserDialog->choice() == 0) // get tags from filename
612 if (FParserUsePreview)
614 *FParserPreview << NC::Format::Bold << s.getName() << ":\n" << NC::Format::NoBold;
615 *FParserPreview << ParseFilename(s, Config.pattern, FParserUsePreview) << '\n';
617 else
618 ParseFilename(s, Config.pattern, FParserUsePreview);
620 else // rename files
622 std::string file = s.getName();
623 size_t last_dot = file.rfind(".");
624 std::string extension = file.substr(last_dot);
625 std::string new_file = GenerateFilename(s, "{" + Config.pattern + "}");
626 if (new_file.empty() && !FParserUsePreview)
628 Statusbar::printf("File \"%1%\" would have an empty name", s.getName());
629 FParserUsePreview = 1;
630 success = 0;
632 if (!FParserUsePreview)
633 s.setNewName(new_file + extension);
634 *FParserPreview << file << Config.color2 << " -> " << NC::Color::End;
635 if (new_file.empty())
636 *FParserPreview << Config.empty_tags_color << Config.empty_tag << NC::Color::End;
637 else
638 *FParserPreview << new_file << extension;
639 *FParserPreview << "\n\n";
640 if (!success)
641 break;
644 if (FParserUsePreview)
646 FParserHelper = FParserPreview;
647 FParserHelper->flush();
648 FParserHelper->display();
650 else if (success)
652 Patterns.remove(Config.pattern);
653 Patterns.insert(Patterns.begin(), Config.pattern);
654 quit = 1;
656 if (pos != 4 || success)
657 Statusbar::print("Operation finished");
659 else if (pos == 2) // show legend
661 FParserHelper = FParserLegend;
662 FParserHelper->display();
664 else if (pos == 5) // cancel
666 quit = 1;
668 else // list of patterns
670 Config.pattern = FParser->current()->value();
671 FParser->at(0).value() = "Pattern: " + Config.pattern;
674 if (quit)
676 SavePatternList();
677 w = TagTypes;
678 refresh();
679 return;
683 if ((w != TagTypes && w != Tags) || Tags->empty()) // after this point we start dealing with tags
684 return;
686 EditedSongs.clear();
687 // if there are selected songs, perform operations only on them
688 if (hasSelected(Tags->begin(), Tags->end()))
690 for (auto it = Tags->begin(); it != Tags->end(); ++it)
691 if (it->isSelected())
692 EditedSongs.push_back(&it->value());
694 else
696 for (auto it = Tags->begin(); it != Tags->end(); ++it)
697 EditedSongs.push_back(&it->value());
700 size_t id = TagTypes->choice();
702 if (w == TagTypes && id == 5)
704 Actions::confirmAction("Number tracks?");
705 auto it = EditedSongs.begin();
706 for (unsigned i = 1; i <= EditedSongs.size(); ++i, ++it)
708 if (Config.tag_editor_extended_numeration)
709 (*it)->setTrack(boost::lexical_cast<std::string>(i) + "/" + boost::lexical_cast<std::string>(EditedSongs.size()));
710 else
711 (*it)->setTrack(boost::lexical_cast<std::string>(i));
712 // discard other track number tags
713 (*it)->setTrack("", 1);
715 Statusbar::print("Tracks numbered");
716 return;
719 if (id < 11)
721 MPD::Song::GetFunction get = SongInfo::Tags[id].Get;
722 MPD::MutableSong::SetFunction set = SongInfo::Tags[id].Set;
723 if (id > 0 && w == TagTypes)
725 Statusbar::ScopedLock slock;
726 Statusbar::put() << NC::Format::Bold << TagTypes->current()->value() << NC::Format::NoBold << ": ";
727 std::string new_tag = wFooter->prompt(Tags->current()->value().getTags(get));
728 for (auto it = EditedSongs.begin(); it != EditedSongs.end(); ++it)
729 (*it)->setTags(set, new_tag);
731 else if (w == Tags)
733 Statusbar::ScopedLock slock;
734 Statusbar::put() << NC::Format::Bold << TagTypes->current()->value() << NC::Format::NoBold << ": ";
735 std::string new_tag = wFooter->prompt(Tags->current()->value().getTags(get));
736 if (new_tag != Tags->current()->value().getTags(get))
737 Tags->current()->value().setTags(set, new_tag);
738 Tags->scroll(NC::Scroll::Down);
741 else
743 if (id == 12) // filename related options
745 if (w == TagTypes)
747 FParserDialog->reset();
748 w = FParserDialog;
750 else if (w == Tags)
752 Statusbar::ScopedLock slock;
753 MPD::MutableSong &s = Tags->current()->value();
754 std::string old_name = s.getNewName().empty() ? s.getName() : s.getNewName();
755 size_t last_dot = old_name.rfind(".");
756 std::string extension = old_name.substr(last_dot);
757 old_name = old_name.substr(0, last_dot);
758 Statusbar::put() << NC::Format::Bold << "New filename: " << NC::Format::NoBold;
759 std::string new_name = wFooter->prompt(old_name);
760 if (!new_name.empty())
761 s.setNewName(new_name + extension);
762 Tags->scroll(NC::Scroll::Down);
765 else if (id == TagTypes->size()-5) // capitalize first letters
767 Statusbar::print("Processing...");
768 for (auto it = EditedSongs.begin(); it != EditedSongs.end(); ++it)
769 CapitalizeFirstLetters(**it);
770 Statusbar::print("Done");
772 else if (id == TagTypes->size()-4) // lower all letters
774 Statusbar::print("Processing...");
775 for (auto it = EditedSongs.begin(); it != EditedSongs.end(); ++it)
776 LowerAllLetters(**it);
777 Statusbar::print("Done");
779 else if (id == TagTypes->size()-2) // reset
781 for (auto it = Tags->beginV(); it != Tags->endV(); ++it)
782 it->clearModifications();
783 Statusbar::print("Changes reset");
785 else if (id == TagTypes->size()-1) // save
787 bool success = 1;
788 Statusbar::print("Writing changes...");
789 for (auto it = EditedSongs.begin(); it != EditedSongs.end(); ++it)
791 Statusbar::printf("Writing tags in \"%1%\"...", (*it)->getName());
792 if (!Tags::write(**it))
794 Statusbar::printf("Error while writing tags to \"%1%\": %2%",
795 (*it)->getName(), strerror(errno));
796 success = 0;
797 break;
800 if (success)
802 Statusbar::print("Tags updated");
803 TagTypes->setHighlightColor(Config.main_highlight_color);
804 TagTypes->reset();
805 w->refresh();
806 w = Dirs;
807 Dirs->setHighlightColor(Config.active_column_color);
808 Mpd.UpdateDirectory(getSharedDirectory(Tags->beginV(), Tags->endV()));
810 else
811 Tags->clear();
817 /***********************************************************************/
819 bool TagEditor::itemAvailable()
821 if (w == Tags)
822 return !Tags->empty();
823 return false;
826 bool TagEditor::addItemToPlaylist(bool play)
828 return addSongToPlaylist(*Tags->currentV(), play);
831 std::vector<MPD::Song> TagEditor::getSelectedSongs()
833 std::vector<MPD::Song> result;
834 if (w == Tags)
836 for (auto it = Tags->begin(); it != Tags->end(); ++it)
837 if (it->isSelected())
838 result.push_back(it->value());
839 // if no song was selected, add current one
840 if (result.empty() && !Tags->empty())
841 result.push_back(Tags->current()->value());
843 return result;
846 /***********************************************************************/
848 bool TagEditor::previousColumnAvailable()
850 bool result = false;
851 if (w == Tags)
853 if (!TagTypes->empty() && !Dirs->empty())
854 result = true;
856 else if (w == TagTypes)
858 if (!Dirs->empty() && isAnyModified(*Tags))
859 Actions::confirmAction("There are pending changes, are you sure?");
860 result = true;
862 else if (w == FParserHelper)
863 result = true;
864 return result;
867 void TagEditor::previousColumn()
869 if (w == Tags)
871 Tags->setHighlightColor(Config.main_highlight_color);
872 w->refresh();
873 w = TagTypes;
874 TagTypes->setHighlightColor(Config.active_column_color);
876 else if (w == TagTypes)
878 TagTypes->setHighlightColor(Config.main_highlight_color);
879 w->refresh();
880 w = Dirs;
881 Dirs->setHighlightColor(Config.active_column_color);
883 else if (w == FParserHelper)
885 FParserHelper->setBorder(Config.window_border);
886 FParserHelper->display();
887 w = FParser;
888 FParser->setBorder(Config.active_window_border);
889 FParser->display();
893 bool TagEditor::nextColumnAvailable()
895 bool result = false;
896 if (w == Dirs)
898 if (!TagTypes->empty() && !Tags->empty())
899 result = true;
901 else if (w == TagTypes)
903 if (!Tags->empty())
904 result = true;
906 else if (w == FParser)
907 result = true;
908 return result;
911 void TagEditor::nextColumn()
913 if (w == Dirs)
915 Dirs->setHighlightColor(Config.main_highlight_color);
916 w->refresh();
917 w = TagTypes;
918 TagTypes->setHighlightColor(Config.active_column_color);
920 else if (w == TagTypes && TagTypes->choice() < 13 && !Tags->empty())
922 TagTypes->setHighlightColor(Config.main_highlight_color);
923 w->refresh();
924 w = Tags;
925 Tags->setHighlightColor(Config.active_column_color);
927 else if (w == FParser)
929 FParser->setBorder(Config.window_border);
930 FParser->display();
931 w = FParserHelper;
932 FParserHelper->setBorder(Config.active_window_border);
933 FParserHelper->display();
937 /***********************************************************************/
939 void TagEditor::LocateSong(const MPD::Song &s)
941 if (myScreen == this)
942 return;
944 if (s.getDirectory().empty())
945 return;
947 if (Global::myScreen != this)
948 switchTo();
950 // go to right directory
951 if (itsBrowsedDir != s.getDirectory())
953 itsBrowsedDir = s.getDirectory();
954 size_t last_slash = itsBrowsedDir.rfind('/');
955 if (last_slash != std::string::npos)
956 itsBrowsedDir = itsBrowsedDir.substr(0, last_slash);
957 else
958 itsBrowsedDir = "/";
959 if (itsBrowsedDir.empty())
960 itsBrowsedDir = "/";
961 Dirs->clear();
962 update();
964 if (itsBrowsedDir == "/")
965 Dirs->reset(); // go to the first pos, which is "." (music dir root)
967 // highlight directory we need and get files from it
968 std::string dir = getBasename(s.getDirectory());
969 for (size_t i = 0; i < Dirs->size(); ++i)
971 if ((*Dirs)[i].value().first == dir)
973 Dirs->highlight(i);
974 break;
977 // refresh window so we can be highlighted item
978 Dirs->refresh();
980 Tags->clear();
981 update();
983 // reset TagTypes since it can be under Filename
984 // and then songs in right column are not visible.
985 TagTypes->reset();
986 // go to the right column
987 nextColumn();
988 nextColumn();
990 // highlight our file
991 for (size_t i = 0; i < Tags->size(); ++i)
993 if ((*Tags)[i].value() == s)
995 Tags->highlight(i);
996 break;
1001 namespace {
1003 bool isAnyModified(const NC::Menu<MPD::MutableSong> &m)
1005 for (auto it = m.beginV(); it != m.endV(); ++it)
1006 if (it->isModified())
1007 return true;
1008 return false;
1011 std::string CapitalizeFirstLetters(const std::string &s)
1013 std::wstring ws = ToWString(s);
1014 wchar_t prev = 0;
1015 for (auto it = ws.begin(); it != ws.end(); ++it)
1017 if (!iswalpha(prev) && prev != L'\'')
1018 *it = towupper(*it);
1019 prev = *it;
1021 return ToString(ws);
1024 void CapitalizeFirstLetters(MPD::MutableSong &s)
1026 for (const SongInfo::Metadata *m = SongInfo::Tags; m->Name; ++m)
1028 unsigned i = 0;
1029 for (std::string tag; !(tag = (s.*m->Get)(i)).empty(); ++i)
1030 (s.*m->Set)(CapitalizeFirstLetters(tag), i);
1034 void LowerAllLetters(MPD::MutableSong &s)
1036 for (const SongInfo::Metadata *m = SongInfo::Tags; m->Name; ++m)
1038 unsigned i = 0;
1039 for (std::string tag; !(tag = (s.*m->Get)(i)).empty(); ++i)
1040 (s.*m->Set)(boost::locale::to_lower(tag), i);
1044 void GetPatternList()
1046 if (Patterns.empty())
1048 std::ifstream input(PatternsFile.c_str());
1049 if (input.is_open())
1051 std::string line;
1052 while (std::getline(input, line))
1053 if (!line.empty())
1054 Patterns.push_back(line);
1055 input.close();
1060 void SavePatternList()
1062 std::ofstream output(PatternsFile.c_str());
1063 if (output.is_open())
1065 std::list<std::string>::const_iterator it = Patterns.begin();
1066 for (unsigned i = 30; it != Patterns.end() && i; ++it, --i)
1067 output << *it << std::endl;
1068 output.close();
1071 MPD::MutableSong::SetFunction IntoSetFunction(char c)
1073 switch (c)
1075 case 'a':
1076 return &MPD::MutableSong::setArtist;
1077 case 'A':
1078 return &MPD::MutableSong::setAlbumArtist;
1079 case 't':
1080 return &MPD::MutableSong::setTitle;
1081 case 'b':
1082 return &MPD::MutableSong::setAlbum;
1083 case 'y':
1084 return &MPD::MutableSong::setDate;
1085 case 'n':
1086 return &MPD::MutableSong::setTrack;
1087 case 'g':
1088 return &MPD::MutableSong::setGenre;
1089 case 'c':
1090 return &MPD::MutableSong::setComposer;
1091 case 'p':
1092 return &MPD::MutableSong::setPerformer;
1093 case 'd':
1094 return &MPD::MutableSong::setDisc;
1095 case 'C':
1096 return &MPD::MutableSong::setComment;
1097 default:
1098 return 0;
1102 std::string GenerateFilename(const MPD::MutableSong &s, const std::string &pattern)
1104 std::string result = Format::stringify<char>(Format::parse(pattern), &s);
1105 removeInvalidCharsFromFilename(result, Config.generate_win32_compatible_filenames);
1106 return result;
1109 std::string ParseFilename(MPD::MutableSong &s, std::string mask, bool preview)
1111 std::ostringstream result;
1112 std::vector<std::string> separators;
1113 std::vector< std::pair<char, std::string> > tags;
1114 std::string file = s.getName().substr(0, s.getName().rfind("."));
1116 size_t i = mask.find("%");
1118 if (!mask.substr(0, i).empty())
1119 file = file.substr(i);
1121 for (; i != std::string::npos; i = mask.find("%"))
1123 tags.push_back(std::make_pair(mask.at(i+1), ""));
1124 mask = mask.substr(i+2);
1125 i = mask.find("%");
1126 if (!mask.empty())
1127 separators.push_back(mask.substr(0, i));
1129 i = 0;
1130 for (auto it = separators.begin(); it != separators.end(); ++it, ++i)
1132 size_t j = file.find(*it);
1133 tags.at(i).second = file.substr(0, j);
1134 if (j+it->length() > file.length())
1135 goto PARSE_FAILED;
1136 file = file.substr(j+it->length());
1138 if (!file.empty())
1140 if (i >= tags.size())
1141 goto PARSE_FAILED;
1142 tags.at(i).second = file;
1145 if (0) // tss...
1147 PARSE_FAILED:
1148 return "Error while parsing filename!\n";
1151 for (auto it = tags.begin(); it != tags.end(); ++it)
1153 for (std::string::iterator j = it->second.begin(); j != it->second.end(); ++j)
1154 if (*j == '_')
1155 *j = ' ';
1157 if (!preview)
1159 MPD::MutableSong::SetFunction set = IntoSetFunction(it->first);
1160 if (set)
1161 s.setTags(set, it->second);
1163 else
1164 result << "%" << it->first << ": " << it->second << "\n";
1166 return result.str();
1169 std::string SongToString(const MPD::MutableSong &s)
1171 std::string result;
1172 size_t i = myTagEditor->TagTypes->choice();
1173 if (i < 11)
1174 result = (s.*SongInfo::Tags[i].Get)(0);
1175 else if (i == 12)
1176 result = s.getNewName().empty() ? s.getName() : s.getName() + " -> " + s.getNewName();
1177 return result.empty() ? Config.empty_tag : result;
1180 bool DirEntryMatcher(const Regex::Regex &rx, const std::pair<std::string, std::string> &dir, bool filter)
1182 if (dir.first == "." || dir.first == "..")
1183 return filter;
1184 return Regex::search(dir.first, rx);
1187 bool SongEntryMatcher(const Regex::Regex &rx, const MPD::MutableSong &s)
1189 return Regex::search(SongToString(s), rx);
1194 #endif