tag_editor.cpp: Fix misleading indentation
[ncmpcpp.git] / src / screens / tag_editor.cpp
blob3311131c1590797ab50ce99df95637169fdd22c6
1 /***************************************************************************
2 * Copyright (C) 2008-2017 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 "screens/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 "screens/browser.h"
31 #include "charset.h"
32 #include "display.h"
33 #include "global.h"
34 #include "helpers.h"
35 #include "format_impl.h"
36 #include "curses/menu_impl.h"
37 #include "screens/playlist.h"
38 #include "screens/song_info.h"
39 #include "statusbar.h"
40 #include "helpers/song_iterator_maker.h"
41 #include "utility/functional.h"
42 #include "utility/comparators.h"
43 #include "title.h"
44 #include "tags.h"
45 #include "screens/screen_switcher.h"
47 using Global::myScreen;
48 using Global::MainHeight;
49 using Global::MainStartY;
51 namespace ph = std::placeholders;
53 TagEditor *myTagEditor;
55 namespace {
57 size_t LeftColumnWidth;
58 size_t LeftColumnStartX;
59 size_t MiddleColumnWidth;
60 size_t MiddleColumnStartX;
61 size_t RightColumnWidth;
62 size_t RightColumnStartX;
64 size_t FParserDialogWidth;
65 size_t FParserDialogHeight;
66 size_t FParserWidth;
67 size_t FParserWidthOne;
68 size_t FParserWidthTwo;
69 size_t FParserHeight;
71 std::list<std::string> Patterns;
72 std::string PatternsFile = "patterns.list";
74 bool isAnyModified(const NC::Menu<MPD::MutableSong> &m);
76 std::string CapitalizeFirstLetters(const std::string &s);
77 void CapitalizeFirstLetters(MPD::MutableSong &s);
78 void LowerAllLetters(MPD::MutableSong &s);
80 void GetPatternList();
81 void SavePatternList();
83 MPD::MutableSong::SetFunction IntoSetFunction(char c);
84 std::string GenerateFilename(const MPD::MutableSong &s, const std::string &pattern);
85 std::string ParseFilename(MPD::MutableSong &s, std::string mask, bool preview);
87 std::string SongToString(const MPD::MutableSong &s);
88 bool DirEntryMatcher(const Regex::Regex &rx, const std::pair<std::string, std::string> &dir, bool filter);
89 bool SongEntryMatcher(const Regex::Regex &rx, const MPD::MutableSong &s);
93 SongIterator TagsWindow::currentS()
95 return makeSongIterator(current());
98 ConstSongIterator TagsWindow::currentS() const
100 return makeConstSongIterator(current());
103 SongIterator TagsWindow::beginS()
105 return makeSongIterator(begin());
108 ConstSongIterator TagsWindow::beginS() const
110 return makeConstSongIterator(begin());
113 SongIterator TagsWindow::endS()
115 return makeSongIterator(end());
118 ConstSongIterator TagsWindow::endS() const
120 return makeConstSongIterator(end());
123 std::vector<MPD::Song> TagsWindow::getSelectedSongs()
125 return {}; // TODO
128 /**********************************************************************/
130 TagEditor::TagEditor() : FParser(0), FParserHelper(0), FParserLegend(0), FParserPreview(0), itsBrowsedDir("/")
132 PatternsFile = Config.ncmpcpp_directory + "patterns.list";
133 SetDimensions(0, COLS);
135 Dirs = new NC::Menu< std::pair<std::string, std::string> >(0, MainStartY, LeftColumnWidth, MainHeight, Config.titles_visibility ? "Directories" : "", Config.main_color, NC::Border());
136 setHighlightFixes(*Dirs);
137 Dirs->cyclicScrolling(Config.use_cyclic_scrolling);
138 Dirs->centeredCursor(Config.centered_cursor);
139 Dirs->setItemDisplayer([](NC::Menu<std::pair<std::string, std::string>> &menu) {
140 menu << Charset::utf8ToLocale(menu.drawn()->value().first);
143 TagTypes = new NC::Menu<std::string>(MiddleColumnStartX, MainStartY, MiddleColumnWidth, MainHeight, Config.titles_visibility ? "Tag types" : "", Config.main_color, NC::Border());
144 setHighlightInactiveColumnFixes(*TagTypes);
145 TagTypes->cyclicScrolling(Config.use_cyclic_scrolling);
146 TagTypes->centeredCursor(Config.centered_cursor);
147 TagTypes->setItemDisplayer([](NC::Menu<std::string> &menu) {
148 menu << Charset::utf8ToLocale(menu.drawn()->value());
151 for (const SongInfo::Metadata *m = SongInfo::Tags; m->Name; ++m)
152 TagTypes->addItem(m->Name);
153 TagTypes->addSeparator();
154 TagTypes->addItem("Filename");
155 TagTypes->addSeparator();
156 if (Config.titles_visibility)
158 TagTypes->addItem("Options", NC::List::Properties::Inactive);
159 TagTypes->addSeparator();
161 TagTypes->addItem("Capitalize First Letters");
162 TagTypes->addItem("lower all letters");
163 TagTypes->addSeparator();
164 TagTypes->addItem("Reset");
165 TagTypes->addItem("Save");
167 Tags = new TagsWindow(NC::Menu<MPD::MutableSong>(RightColumnStartX, MainStartY, RightColumnWidth, MainHeight, Config.titles_visibility ? "Tags" : "", Config.main_color, NC::Border()));
168 setHighlightInactiveColumnFixes(*Tags);
169 Tags->cyclicScrolling(Config.use_cyclic_scrolling);
170 Tags->centeredCursor(Config.centered_cursor);
171 Tags->setSelectedPrefix(Config.selected_item_prefix);
172 Tags->setSelectedSuffix(Config.selected_item_suffix);
173 Tags->setItemDisplayer(Display::Tags);
175 auto parser_display = [](NC::Menu<std::string> &menu) {
176 menu << Charset::utf8ToLocale(menu.drawn()->value());
179 FParserDialog = new NC::Menu<std::string>((COLS-FParserDialogWidth)/2, (MainHeight-FParserDialogHeight)/2+MainStartY, FParserDialogWidth, FParserDialogHeight, "", Config.main_color, Config.window_border);
180 FParserDialog->cyclicScrolling(Config.use_cyclic_scrolling);
181 FParserDialog->centeredCursor(Config.centered_cursor);
182 FParserDialog->setItemDisplayer(parser_display);
183 FParserDialog->addItem("Get tags from filename");
184 FParserDialog->addItem("Rename files");
185 FParserDialog->addItem("Cancel");
187 FParser = new NC::Menu<std::string>((COLS-FParserWidth)/2, (MainHeight-FParserHeight)/2+MainStartY, FParserWidthOne, FParserHeight, "_", Config.main_color, Config.active_window_border);
188 FParser->cyclicScrolling(Config.use_cyclic_scrolling);
189 FParser->centeredCursor(Config.centered_cursor);
190 FParser->setItemDisplayer(parser_display);
192 FParserLegend = new NC::Scrollpad((COLS-FParserWidth)/2+FParserWidthOne, (MainHeight-FParserHeight)/2+MainStartY, FParserWidthTwo, FParserHeight, "Legend", Config.main_color, Config.window_border);
194 FParserPreview = new NC::Scrollpad((COLS-FParserWidth)/2+FParserWidthOne, (MainHeight-FParserHeight)/2+MainStartY, FParserWidthTwo, FParserHeight, "Preview", Config.main_color, Config.window_border);
196 w = Dirs;
199 void TagEditor::SetDimensions(size_t x_offset, size_t width)
201 MiddleColumnWidth = std::min(26, COLS-2);
202 LeftColumnStartX = x_offset;
203 LeftColumnWidth = (width-MiddleColumnWidth)/2;
204 MiddleColumnStartX = LeftColumnStartX+LeftColumnWidth+1;
205 RightColumnWidth = width-LeftColumnWidth-MiddleColumnWidth-2;
206 RightColumnStartX = MiddleColumnStartX+MiddleColumnWidth+1;
208 FParserDialogWidth = std::min(30, COLS);
209 FParserDialogHeight = std::min(size_t(5), MainHeight);
210 FParserWidth = width*0.9;
211 FParserHeight = std::min(size_t(LINES*0.8), MainHeight);
212 FParserWidthOne = FParserWidth/2;
213 FParserWidthTwo = FParserWidth-FParserWidthOne;
216 void TagEditor::resize()
218 size_t x_offset, width;
219 getWindowResizeParams(x_offset, width);
220 SetDimensions(x_offset, width);
222 Dirs->resize(LeftColumnWidth, MainHeight);
223 TagTypes->resize(MiddleColumnWidth, MainHeight);
224 Tags->resize(RightColumnWidth, MainHeight);
225 FParserDialog->resize(FParserDialogWidth, FParserDialogHeight);
226 FParser->resize(FParserWidthOne, FParserHeight);
227 FParserLegend->resize(FParserWidthTwo, FParserHeight);
228 FParserPreview->resize(FParserWidthTwo, FParserHeight);
230 Dirs->moveTo(LeftColumnStartX, MainStartY);
231 TagTypes->moveTo(MiddleColumnStartX, MainStartY);
232 Tags->moveTo(RightColumnStartX, MainStartY);
234 FParserDialog->moveTo(x_offset+(width-FParserDialogWidth)/2, (MainHeight-FParserDialogHeight)/2+MainStartY);
235 FParser->moveTo(x_offset+(width-FParserWidth)/2, (MainHeight-FParserHeight)/2+MainStartY);
236 FParserLegend->moveTo(x_offset+(width-FParserWidth)/2+FParserWidthOne, (MainHeight-FParserHeight)/2+MainStartY);
237 FParserPreview->moveTo(x_offset+(width-FParserWidth)/2+FParserWidthOne, (MainHeight-FParserHeight)/2+MainStartY);
239 hasToBeResized = 0;
242 std::wstring TagEditor::title()
244 return L"Tag editor";
247 void TagEditor::switchTo()
249 SwitchTo::execute(this);
250 drawHeader();
251 refresh();
254 void TagEditor::refresh()
256 Dirs->display();
257 drawSeparator(MiddleColumnStartX-1);
258 TagTypes->display();
259 drawSeparator(RightColumnStartX-1);
260 Tags->display();
262 if (w == FParserDialog)
264 FParserDialog->display();
266 else if (w == FParser || w == FParserHelper)
268 FParser->display();
269 FParserHelper->display();
273 void TagEditor::update()
275 if (Dirs->empty())
277 Dirs->Window::clear();
278 Tags->clear();
280 if (itsBrowsedDir != "/")
281 Dirs->addItem(std::make_pair("..", getParentDirectory(itsBrowsedDir)));
282 else
283 Dirs->addItem(std::make_pair(".", "/"));
284 MPD::DirectoryIterator directory = Mpd.GetDirectories(itsBrowsedDir), end;
285 for (; directory != end; ++directory)
287 Dirs->addItem(std::make_pair(getBasename(directory->path()), directory->path()));
288 if (directory->path() == itsHighlightedDir)
289 Dirs->highlight(Dirs->size()-1);
291 std::sort(Dirs->beginV()+1, Dirs->endV(),
292 LocaleBasedSorting(std::locale(), Config.ignore_leading_the));
293 Dirs->display();
296 if (Tags->empty())
298 Tags->reset();
299 MPD::SongIterator s = Mpd.GetSongs(Dirs->current()->value().second), end;
300 for (; s != end; ++s)
301 Tags->addItem(std::move(*s));
302 std::sort(Tags->beginV(), Tags->endV(),
303 LocaleBasedSorting(std::locale(), Config.ignore_leading_the));
304 Tags->refresh();
307 if (w == TagTypes && TagTypes->choice() < 13)
309 Tags->refresh();
311 else if (TagTypes->choice() >= 13)
313 Tags->Window::clear();
314 Tags->Window::refresh();
318 bool TagEditor::enterDirectory()
320 bool result = false;
321 if (w == Dirs && !Dirs->empty())
323 MPD::DirectoryIterator directory = Mpd.GetDirectories(Dirs->current()->value().second), end;
324 bool has_subdirs = directory != end;
325 if (has_subdirs)
327 directory.finish();
328 itsHighlightedDir = itsBrowsedDir;
329 itsBrowsedDir = Dirs->current()->value().second;
330 Dirs->clear();
331 Dirs->reset();
332 result = true;
335 return result;
338 void TagEditor::mouseButtonPressed(MEVENT me)
340 auto tryPreviousColumn = [this]() -> bool {
341 bool result = true;
342 if (w != Dirs)
344 if (previousColumnAvailable())
345 previousColumn();
346 else
347 result = false;
349 return result;
351 auto tryNextColumn = [this]() -> bool {
352 bool result = true;
353 if (w != Tags)
355 if (nextColumnAvailable())
356 nextColumn();
357 else
358 result = false;
360 return result;
362 if (w == FParserDialog)
364 if (FParserDialog->hasCoords(me.x, me.y))
366 if (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED))
368 FParserDialog->Goto(me.y);
369 if (me.bstate & BUTTON3_PRESSED)
370 runAction();
372 else
373 Screen<WindowType>::mouseButtonPressed(me);
376 else if (w == FParser || w == FParserHelper)
378 if (FParser->hasCoords(me.x, me.y))
380 if (w != FParser)
382 if (previousColumnAvailable())
383 previousColumn();
384 else
385 return;
387 if (size_t(me.y) < FParser->size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)))
389 FParser->Goto(me.y);
390 if (me.bstate & BUTTON3_PRESSED)
391 runAction();
393 else
394 Screen<WindowType>::mouseButtonPressed(me);
396 else if (FParserHelper->hasCoords(me.x, me.y))
398 if (w != FParserHelper)
400 if (nextColumnAvailable())
401 nextColumn();
402 else
403 return;
405 scrollpadMouseButtonPressed(*FParserHelper, me);
408 else if (!Dirs->empty() && Dirs->hasCoords(me.x, me.y))
410 if (!tryPreviousColumn() || !tryPreviousColumn())
411 return;
412 if (size_t(me.y) < Dirs->size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)))
414 Dirs->Goto(me.y);
415 if (me.bstate & BUTTON1_PRESSED)
416 enterDirectory();
418 else
419 Screen<WindowType>::mouseButtonPressed(me);
420 Tags->clear();
422 else if (!TagTypes->empty() && TagTypes->hasCoords(me.x, me.y))
424 if (w != TagTypes)
426 bool success;
427 if (w == Dirs)
428 success = tryNextColumn();
429 else
430 success = tryPreviousColumn();
431 if (!success)
432 return;
434 if (size_t(me.y) < TagTypes->size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)))
436 if (!TagTypes->Goto(me.y))
437 return;
438 TagTypes->refresh();
439 Tags->refresh();
440 if (me.bstate & BUTTON3_PRESSED)
441 runAction();
443 else
444 Screen<WindowType>::mouseButtonPressed(me);
446 else if (!Tags->empty() && Tags->hasCoords(me.x, me.y))
448 if (!tryNextColumn() || !tryNextColumn())
449 return;
450 if (size_t(me.y) < Tags->size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)))
452 Tags->Goto(me.y);
453 Tags->refresh();
454 if (me.bstate & BUTTON3_PRESSED)
455 runAction();
457 else
458 Screen<WindowType>::mouseButtonPressed(me);
462 /***********************************************************************/
464 bool TagEditor::allowsSearching()
466 return w == Dirs || w == Tags;
469 const std::string &TagEditor::searchConstraint()
471 if (w == Dirs)
472 return m_directories_search_predicate.constraint();
473 else if (w == Tags)
474 return m_songs_search_predicate.constraint();
475 throw std::runtime_error("shouldn't happen due to condition in allowsSearching");
478 void TagEditor::setSearchConstraint(const std::string &constraint)
480 if (w == Dirs)
482 m_directories_search_predicate = Regex::Filter<std::pair<std::string, std::string>>(
483 constraint,
484 Config.regex_type,
485 std::bind(DirEntryMatcher, ph::_1, ph::_2, false));
487 else if (w == Tags)
489 m_songs_search_predicate = Regex::Filter<MPD::MutableSong>(
490 constraint,
491 Config.regex_type,
492 SongEntryMatcher);
496 void TagEditor::clearSearchConstraint()
498 if (w == Dirs)
499 m_directories_search_predicate.clear();
500 else if (w == Tags)
501 m_songs_search_predicate.clear();
504 bool TagEditor::search(SearchDirection direction, bool wrap, bool skip_current)
506 bool result = false;
507 if (w == Dirs)
508 result = ::search(*Dirs, m_directories_search_predicate, direction, wrap, skip_current);
509 else if (w == Tags)
510 result = ::search(*Tags, m_songs_search_predicate, direction, wrap, skip_current);
511 return result;
514 /***********************************************************************/
516 bool TagEditor::actionRunnable()
518 // TODO: put something more refined here. It requires reworking
519 // runAction though, i.e. splitting it into smaller parts.
520 return (w == Tags && !Tags->empty())
521 || w != Tags;
524 void TagEditor::runAction()
526 using Global::wFooter;
528 if (w == FParserDialog)
530 size_t choice = FParserDialog->choice();
531 if (choice == 2) // cancel
533 w = TagTypes;
534 refresh();
535 return;
537 GetPatternList();
539 // prepare additional windows
541 FParserLegend->clear();
542 *FParserLegend << "%a - artist\n";
543 *FParserLegend << "%A - album artist\n";
544 *FParserLegend << "%t - title\n";
545 *FParserLegend << "%b - album\n";
546 *FParserLegend << "%y - date\n";
547 *FParserLegend << "%n - track number\n";
548 *FParserLegend << "%g - genre\n";
549 *FParserLegend << "%c - composer\n";
550 *FParserLegend << "%p - performer\n";
551 *FParserLegend << "%d - disc\n";
552 *FParserLegend << "%C - comment\n\n";
553 *FParserLegend << NC::Format::Bold << "Files:\n" << NC::Format::NoBold;
554 for (auto it = EditedSongs.begin(); it != EditedSongs.end(); ++it)
555 *FParserLegend << Config.color2
556 << " * "
557 << NC::FormattedColor::End<>(Config.color2)
558 << (*it)->getName()
559 << "\n";
560 FParserLegend->flush();
562 if (!Patterns.empty())
563 Config.pattern = Patterns.front();
564 FParser->clear();
565 FParser->reset();
566 FParser->addItem("Pattern: " + Config.pattern);
567 FParser->addItem("Preview");
568 FParser->addItem("Legend");
569 FParser->addSeparator();
570 FParser->addItem("Proceed");
571 FParser->addItem("Cancel");
572 if (!Patterns.empty())
574 FParser->addSeparator();
575 FParser->addItem("Recent patterns", NC::List::Properties::Inactive);
576 FParser->addSeparator();
577 for (std::list<std::string>::const_iterator it = Patterns.begin(); it != Patterns.end(); ++it)
578 FParser->addItem(*it);
581 FParser->setTitle(choice == 0 ? "Get tags from filename" : "Rename files");
582 w = FParser;
583 FParserUsePreview = 1;
584 FParserHelper = FParserLegend;
585 FParserHelper->display();
587 else if (w == FParser)
589 bool quit = 0;
590 size_t pos = FParser->choice();
592 if (pos == 4) // save
593 FParserUsePreview = 0;
595 if (pos == 0) // change pattern
597 std::string new_pattern;
599 Statusbar::ScopedLock slock;
600 Statusbar::put() << "Pattern: ";
601 new_pattern = wFooter->prompt(Config.pattern);
603 Config.pattern = new_pattern;
604 FParser->at(0).value() = "Pattern: ";
605 FParser->at(0).value() += Config.pattern;
607 else if (pos == 1 || pos == 4) // preview or proceed
609 bool success = 1;
610 Statusbar::print("Parsing...");
611 FParserPreview->clear();
612 for (auto it = EditedSongs.begin(); it != EditedSongs.end(); ++it)
614 MPD::MutableSong &s = **it;
615 if (FParserDialog->choice() == 0) // get tags from filename
617 if (FParserUsePreview)
619 *FParserPreview << NC::Format::Bold << s.getName() << ":\n" << NC::Format::NoBold;
620 *FParserPreview << ParseFilename(s, Config.pattern, FParserUsePreview) << '\n';
622 else
623 ParseFilename(s, Config.pattern, FParserUsePreview);
625 else // rename files
627 std::string file = s.getName();
628 size_t last_dot = file.rfind(".");
629 std::string extension = file.substr(last_dot);
630 std::string new_file = GenerateFilename(s, "{" + Config.pattern + "}");
631 if (new_file.empty() && !FParserUsePreview)
633 Statusbar::printf("File \"%1%\" would have an empty name", s.getName());
634 FParserUsePreview = 1;
635 success = 0;
637 if (!FParserUsePreview)
638 s.setNewName(new_file + extension);
639 *FParserPreview << file
640 << Config.color2
641 << " -> "
642 << NC::FormattedColor::End<>(Config.color2);
643 if (new_file.empty())
644 *FParserPreview << Config.empty_tags_color
645 << Config.empty_tag
646 << NC::FormattedColor::End<>(Config.empty_tags_color);
647 else
648 *FParserPreview << new_file << extension;
649 *FParserPreview << "\n\n";
650 if (!success)
651 break;
654 if (FParserUsePreview)
656 FParserHelper = FParserPreview;
657 FParserHelper->flush();
658 FParserHelper->display();
660 else if (success)
662 Patterns.remove(Config.pattern);
663 Patterns.insert(Patterns.begin(), Config.pattern);
664 quit = 1;
666 if (pos != 4 || success)
667 Statusbar::print("Operation finished");
669 else if (pos == 2) // show legend
671 FParserHelper = FParserLegend;
672 FParserHelper->display();
674 else if (pos == 5) // cancel
676 quit = 1;
678 else // list of patterns
680 Config.pattern = FParser->current()->value();
681 FParser->at(0).value() = "Pattern: " + Config.pattern;
684 if (quit)
686 SavePatternList();
687 w = TagTypes;
688 refresh();
689 return;
693 if ((w != TagTypes && w != Tags) || Tags->empty()) // after this point we start dealing with tags
694 return;
696 EditedSongs.clear();
697 // if there are selected songs, perform operations only on them
698 if (hasSelected(Tags->begin(), Tags->end()))
700 for (auto it = Tags->begin(); it != Tags->end(); ++it)
701 if (it->isSelected())
702 EditedSongs.push_back(&it->value());
704 else
706 for (auto it = Tags->begin(); it != Tags->end(); ++it)
707 EditedSongs.push_back(&it->value());
710 size_t id = TagTypes->choice();
712 if (w == TagTypes && id == 5)
714 Actions::confirmAction("Number tracks?");
715 auto it = EditedSongs.begin();
716 for (unsigned i = 1; i <= EditedSongs.size(); ++i, ++it)
718 if (Config.tag_editor_extended_numeration)
719 (*it)->setTrack(boost::lexical_cast<std::string>(i) + "/" + boost::lexical_cast<std::string>(EditedSongs.size()));
720 else
721 (*it)->setTrack(boost::lexical_cast<std::string>(i));
722 // discard other track number tags
723 (*it)->setTrack("", 1);
725 Statusbar::print("Tracks numbered");
726 return;
729 if (id < 11)
731 MPD::Song::GetFunction get = SongInfo::Tags[id].Get;
732 MPD::MutableSong::SetFunction set = SongInfo::Tags[id].Set;
733 if (id > 0 && w == TagTypes)
735 Statusbar::ScopedLock slock;
736 Statusbar::put() << NC::Format::Bold << TagTypes->current()->value() << NC::Format::NoBold << ": ";
737 std::string new_tag = wFooter->prompt(Tags->current()->value().getTags(get));
738 for (auto it = EditedSongs.begin(); it != EditedSongs.end(); ++it)
739 (*it)->setTags(set, new_tag);
741 else if (w == Tags)
743 Statusbar::ScopedLock slock;
744 Statusbar::put() << NC::Format::Bold << TagTypes->current()->value() << NC::Format::NoBold << ": ";
745 std::string new_tag = wFooter->prompt(Tags->current()->value().getTags(get));
746 if (new_tag != Tags->current()->value().getTags(get))
747 Tags->current()->value().setTags(set, new_tag);
748 Tags->scroll(NC::Scroll::Down);
751 else
753 if (id == 12) // filename related options
755 if (w == TagTypes)
757 FParserDialog->reset();
758 w = FParserDialog;
760 else if (w == Tags)
762 Statusbar::ScopedLock slock;
763 MPD::MutableSong &s = Tags->current()->value();
764 std::string old_name = s.getNewName().empty() ? s.getName() : s.getNewName();
765 size_t last_dot = old_name.rfind(".");
766 std::string extension = old_name.substr(last_dot);
767 old_name = old_name.substr(0, last_dot);
768 Statusbar::put() << NC::Format::Bold << "New filename: " << NC::Format::NoBold;
769 std::string new_name = wFooter->prompt(old_name);
770 if (!new_name.empty())
771 s.setNewName(new_name + extension);
772 Tags->scroll(NC::Scroll::Down);
775 else if (id == TagTypes->size()-5) // capitalize first letters
777 Statusbar::print("Processing...");
778 for (auto it = EditedSongs.begin(); it != EditedSongs.end(); ++it)
779 CapitalizeFirstLetters(**it);
780 Statusbar::print("Done");
782 else if (id == TagTypes->size()-4) // lower all letters
784 Statusbar::print("Processing...");
785 for (auto it = EditedSongs.begin(); it != EditedSongs.end(); ++it)
786 LowerAllLetters(**it);
787 Statusbar::print("Done");
789 else if (id == TagTypes->size()-2) // reset
791 for (auto it = Tags->beginV(); it != Tags->endV(); ++it)
792 it->clearModifications();
793 Statusbar::print("Changes reset");
795 else if (id == TagTypes->size()-1) // save
797 bool success = 1;
798 Statusbar::print("Writing changes...");
799 for (auto it = EditedSongs.begin(); it != EditedSongs.end(); ++it)
801 Statusbar::printf("Writing tags in \"%1%\"...", (*it)->getName());
802 if (!Tags::write(**it))
804 Statusbar::printf("Error while writing tags to \"%1%\": %2%",
805 (*it)->getName(), strerror(errno));
806 success = 0;
807 break;
810 if (success)
812 Statusbar::print("Tags updated");
813 setHighlightInactiveColumnFixes(*TagTypes);
814 TagTypes->reset();
815 w->refresh();
816 w = Dirs;
817 setHighlightFixes(*Dirs);
818 Mpd.UpdateDirectory(getSharedDirectory(Tags->beginV(), Tags->endV()));
820 else
821 Tags->clear();
827 /***********************************************************************/
829 bool TagEditor::itemAvailable()
831 if (w == Tags)
832 return !Tags->empty();
833 return false;
836 bool TagEditor::addItemToPlaylist(bool play)
838 return addSongToPlaylist(*Tags->currentV(), play);
841 std::vector<MPD::Song> TagEditor::getSelectedSongs()
843 std::vector<MPD::Song> result;
844 if (w == Tags)
846 for (auto it = Tags->begin(); it != Tags->end(); ++it)
847 if (it->isSelected())
848 result.push_back(it->value());
849 // if no song was selected, add current one
850 if (result.empty() && !Tags->empty())
851 result.push_back(Tags->current()->value());
853 return result;
856 /***********************************************************************/
858 bool TagEditor::previousColumnAvailable()
860 bool result = false;
861 if (w == Tags)
863 if (!TagTypes->empty() && !Dirs->empty())
864 result = true;
866 else if (w == TagTypes)
868 if (!Dirs->empty() && isAnyModified(*Tags))
869 Actions::confirmAction("There are pending changes, are you sure?");
870 result = true;
872 else if (w == FParserHelper)
873 result = true;
874 return result;
877 void TagEditor::previousColumn()
879 if (w == Tags)
881 setHighlightInactiveColumnFixes(*Tags);
882 w->refresh();
883 w = TagTypes;
884 setHighlightFixes(*TagTypes);
886 else if (w == TagTypes)
888 setHighlightInactiveColumnFixes(*TagTypes);
889 w->refresh();
890 w = Dirs;
891 setHighlightFixes(*Dirs);
893 else if (w == FParserHelper)
895 FParserHelper->setBorder(Config.window_border);
896 FParserHelper->display();
897 w = FParser;
898 FParser->setBorder(Config.active_window_border);
899 FParser->display();
903 bool TagEditor::nextColumnAvailable()
905 bool result = false;
906 if (w == Dirs)
908 if (!TagTypes->empty() && !Tags->empty())
909 result = true;
911 else if (w == TagTypes)
913 if (!Tags->empty())
914 result = true;
916 else if (w == FParser)
917 result = true;
918 return result;
921 void TagEditor::nextColumn()
923 if (w == Dirs)
925 setHighlightInactiveColumnFixes(*Dirs);
926 w->refresh();
927 w = TagTypes;
928 setHighlightFixes(*TagTypes);
930 else if (w == TagTypes && TagTypes->choice() < 13 && !Tags->empty())
932 setHighlightInactiveColumnFixes(*TagTypes);
933 w->refresh();
934 w = Tags;
935 setHighlightFixes(*Tags);
937 else if (w == FParser)
939 FParser->setBorder(Config.window_border);
940 FParser->display();
941 w = FParserHelper;
942 FParserHelper->setBorder(Config.active_window_border);
943 FParserHelper->display();
947 /***********************************************************************/
949 void TagEditor::LocateSong(const MPD::Song &s)
951 if (myScreen == this)
952 return;
954 if (s.getDirectory().empty())
955 return;
957 if (Global::myScreen != this)
958 switchTo();
960 // go to right directory
961 if (itsBrowsedDir != s.getDirectory())
963 itsBrowsedDir = s.getDirectory();
964 size_t last_slash = itsBrowsedDir.rfind('/');
965 if (last_slash != std::string::npos)
966 itsBrowsedDir = itsBrowsedDir.substr(0, last_slash);
967 else
968 itsBrowsedDir = "/";
969 if (itsBrowsedDir.empty())
970 itsBrowsedDir = "/";
971 Dirs->clear();
972 update();
974 if (itsBrowsedDir == "/")
975 Dirs->reset(); // go to the first pos, which is "." (music dir root)
977 // highlight directory we need and get files from it
978 std::string dir = getBasename(s.getDirectory());
979 for (size_t i = 0; i < Dirs->size(); ++i)
981 if ((*Dirs)[i].value().first == dir)
983 Dirs->highlight(i);
984 break;
987 // refresh window so we can be highlighted item
988 Dirs->refresh();
990 Tags->clear();
991 update();
993 // reset TagTypes since it can be under Filename
994 // and then songs in right column are not visible.
995 TagTypes->reset();
996 // go to the right column
997 nextColumn();
998 nextColumn();
1000 // highlight our file
1001 for (size_t i = 0; i < Tags->size(); ++i)
1003 if ((*Tags)[i].value() == s)
1005 Tags->highlight(i);
1006 break;
1011 namespace {
1013 bool isAnyModified(const NC::Menu<MPD::MutableSong> &m)
1015 for (auto it = m.beginV(); it != m.endV(); ++it)
1016 if (it->isModified())
1017 return true;
1018 return false;
1021 std::string CapitalizeFirstLetters(const std::string &s)
1023 std::wstring ws = ToWString(s);
1024 wchar_t prev = 0;
1025 for (auto it = ws.begin(); it != ws.end(); ++it)
1027 if (!iswalpha(prev) && prev != L'\'')
1028 *it = towupper(*it);
1029 prev = *it;
1031 return ToString(ws);
1034 void CapitalizeFirstLetters(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)(CapitalizeFirstLetters(tag), i);
1044 void LowerAllLetters(MPD::MutableSong &s)
1046 for (const SongInfo::Metadata *m = SongInfo::Tags; m->Name; ++m)
1048 unsigned i = 0;
1049 for (std::string tag; !(tag = (s.*m->Get)(i)).empty(); ++i)
1050 (s.*m->Set)(boost::locale::to_lower(tag), i);
1054 void GetPatternList()
1056 if (Patterns.empty())
1058 std::ifstream input(PatternsFile.c_str());
1059 if (input.is_open())
1061 std::string line;
1062 while (std::getline(input, line))
1063 if (!line.empty())
1064 Patterns.push_back(line);
1065 input.close();
1070 void SavePatternList()
1072 std::ofstream output(PatternsFile.c_str());
1073 if (output.is_open())
1075 std::list<std::string>::const_iterator it = Patterns.begin();
1076 for (unsigned i = 30; it != Patterns.end() && i; ++it, --i)
1077 output << *it << std::endl;
1078 output.close();
1081 MPD::MutableSong::SetFunction IntoSetFunction(char c)
1083 switch (c)
1085 case 'a':
1086 return &MPD::MutableSong::setArtist;
1087 case 'A':
1088 return &MPD::MutableSong::setAlbumArtist;
1089 case 't':
1090 return &MPD::MutableSong::setTitle;
1091 case 'b':
1092 return &MPD::MutableSong::setAlbum;
1093 case 'y':
1094 return &MPD::MutableSong::setDate;
1095 case 'n':
1096 return &MPD::MutableSong::setTrack;
1097 case 'g':
1098 return &MPD::MutableSong::setGenre;
1099 case 'c':
1100 return &MPD::MutableSong::setComposer;
1101 case 'p':
1102 return &MPD::MutableSong::setPerformer;
1103 case 'd':
1104 return &MPD::MutableSong::setDisc;
1105 case 'C':
1106 return &MPD::MutableSong::setComment;
1107 default:
1108 return 0;
1112 std::string GenerateFilename(const MPD::MutableSong &s, const std::string &pattern)
1114 std::string result = Format::stringify<char>(Format::parse(pattern), &s);
1115 removeInvalidCharsFromFilename(result, Config.generate_win32_compatible_filenames);
1116 return result;
1119 std::string ParseFilename(MPD::MutableSong &s, std::string mask, bool preview)
1121 std::ostringstream result;
1122 std::vector<std::string> separators;
1123 std::vector< std::pair<char, std::string> > tags;
1124 std::string file = s.getName().substr(0, s.getName().rfind("."));
1126 size_t i = mask.find("%");
1128 if (!mask.substr(0, i).empty())
1129 file = file.substr(i);
1131 for (; i != std::string::npos; i = mask.find("%"))
1133 tags.push_back(std::make_pair(mask.at(i+1), ""));
1134 mask = mask.substr(i+2);
1135 i = mask.find("%");
1136 if (!mask.empty())
1137 separators.push_back(mask.substr(0, i));
1139 i = 0;
1140 for (auto it = separators.begin(); it != separators.end(); ++it, ++i)
1142 size_t j = file.find(*it);
1143 tags.at(i).second = file.substr(0, j);
1144 if (j+it->length() > file.length())
1145 goto PARSE_FAILED;
1146 file = file.substr(j+it->length());
1148 if (!file.empty())
1150 if (i >= tags.size())
1151 goto PARSE_FAILED;
1152 tags.at(i).second = file;
1155 if (0) // tss...
1157 PARSE_FAILED:
1158 return "Error while parsing filename!\n";
1161 for (auto it = tags.begin(); it != tags.end(); ++it)
1163 for (std::string::iterator j = it->second.begin(); j != it->second.end(); ++j)
1164 if (*j == '_')
1165 *j = ' ';
1167 if (!preview)
1169 MPD::MutableSong::SetFunction set = IntoSetFunction(it->first);
1170 if (set)
1171 s.setTags(set, it->second);
1173 else
1174 result << "%" << it->first << ": " << it->second << "\n";
1176 return result.str();
1179 std::string SongToString(const MPD::MutableSong &s)
1181 std::string result;
1182 size_t i = myTagEditor->TagTypes->choice();
1183 if (i < 11)
1184 result = (s.*SongInfo::Tags[i].Get)(0);
1185 else if (i == 12)
1186 result = s.getNewName().empty() ? s.getName() : s.getName() + " -> " + s.getNewName();
1187 return result.empty() ? Config.empty_tag : result;
1190 bool DirEntryMatcher(const Regex::Regex &rx, const std::pair<std::string, std::string> &dir, bool filter)
1192 if (dir.first == "." || dir.first == "..")
1193 return filter;
1194 return Regex::search(dir.first, rx, Config.ignore_diacritics);
1197 bool SongEntryMatcher(const Regex::Regex &rx, const MPD::MutableSong &s)
1199 return Regex::search(SongToString(s), rx, Config.ignore_diacritics);
1204 #endif