1 /***************************************************************************
2 * Copyright (C) 2008-2014 by Andrzej Rybczak *
3 * electricityispower@gmail.com *
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. *
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. *
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"
25 #include <boost/locale/conversion.hpp>
35 #include "menu_impl.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"
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
;
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
;
66 size_t FParserWidthOne
;
67 size_t FParserWidthTwo
;
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
);
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()
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
);
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
);
256 std::wstring
TagEditor::title()
258 return L
"Tag editor";
261 void TagEditor::switchTo()
263 SwitchTo::execute(this);
268 void TagEditor::refresh()
271 drawSeparator(MiddleColumnStartX
-1);
273 drawSeparator(RightColumnStartX
-1);
276 if (w
== FParserDialog
)
278 FParserDialog
->display();
280 else if (w
== FParser
|| w
== FParserHelper
)
283 FParserHelper
->display();
287 void TagEditor::update()
291 Dirs
->Window::clear();
294 if (itsBrowsedDir
!= "/")
295 Dirs
->addItem(std::make_pair("..", getParentDirectory(itsBrowsedDir
)));
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
));
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
));
321 if (w
== TagTypes
&& TagTypes
->choice() < 13)
325 else if (TagTypes
->choice() >= 13)
327 Tags
->Window::clear();
328 Tags
->Window::refresh();
332 bool TagEditor::enterDirectory()
335 if (w
== Dirs
&& !Dirs
->empty())
337 MPD::DirectoryIterator directory
= Mpd
.GetDirectories(Dirs
->current()->value().second
), end
;
338 bool has_subdirs
= directory
!= end
;
342 itsHighlightedDir
= itsBrowsedDir
;
343 itsBrowsedDir
= Dirs
->current()->value().second
;
352 void TagEditor::mouseButtonPressed(MEVENT me
)
354 auto tryPreviousColumn
= [this]() -> bool {
358 if (previousColumnAvailable())
365 auto tryNextColumn
= [this]() -> bool {
369 if (nextColumnAvailable())
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
)
387 Screen
<WindowType
>::mouseButtonPressed(me
);
390 else if (w
== FParser
|| w
== FParserHelper
)
392 if (FParser
->hasCoords(me
.x
, me
.y
))
396 if (previousColumnAvailable())
401 if (size_t(me
.y
) < FParser
->size() && (me
.bstate
& (BUTTON1_PRESSED
| BUTTON3_PRESSED
)))
404 if (me
.bstate
& BUTTON3_PRESSED
)
408 Screen
<WindowType
>::mouseButtonPressed(me
);
410 else if (FParserHelper
->hasCoords(me
.x
, me
.y
))
412 if (w
!= FParserHelper
)
414 if (nextColumnAvailable())
419 scrollpadMouseButtonPressed(*FParserHelper
, me
);
422 else if (!Dirs
->empty() && Dirs
->hasCoords(me
.x
, me
.y
))
424 if (!tryPreviousColumn() || !tryPreviousColumn())
426 if (size_t(me
.y
) < Dirs
->size() && (me
.bstate
& (BUTTON1_PRESSED
| BUTTON3_PRESSED
)))
429 if (me
.bstate
& BUTTON1_PRESSED
)
433 Screen
<WindowType
>::mouseButtonPressed(me
);
436 else if (!TagTypes
->empty() && TagTypes
->hasCoords(me
.x
, me
.y
))
442 success
= tryNextColumn();
444 success
= tryPreviousColumn();
448 if (size_t(me
.y
) < TagTypes
->size() && (me
.bstate
& (BUTTON1_PRESSED
| BUTTON3_PRESSED
)))
450 if (!TagTypes
->Goto(me
.y
))
454 if (me
.bstate
& BUTTON3_PRESSED
)
458 Screen
<WindowType
>::mouseButtonPressed(me
);
460 else if (!Tags
->empty() && Tags
->hasCoords(me
.x
, me
.y
))
462 if (!tryNextColumn() || !tryNextColumn())
464 if (size_t(me
.y
) < Tags
->size() && (me
.bstate
& (BUTTON1_PRESSED
| BUTTON3_PRESSED
)))
468 if (me
.bstate
& BUTTON3_PRESSED
)
472 Screen
<WindowType
>::mouseButtonPressed(me
);
476 /***********************************************************************/
478 bool TagEditor::allowsSearching()
480 return w
== Dirs
|| w
== Tags
;
483 void TagEditor::setSearchConstraint(const std::string
&constraint
)
487 m_directories_search_predicate
= Regex::Filter
<std::pair
<std::string
, std::string
>>(
488 Regex::make(constraint
, Config
.regex_type
),
489 std::bind(DirEntryMatcher
, ph::_1
, ph::_2
, false)
494 m_songs_search_predicate
= Regex::Filter
<MPD::MutableSong
>(
495 Regex::make(constraint
, Config
.regex_type
),
501 void TagEditor::clearConstraint()
504 m_directories_search_predicate
.clear();
506 m_songs_search_predicate
.clear();
509 bool TagEditor::find(SearchDirection direction
, bool wrap
, bool skip_current
)
513 result
= search(*Dirs
, m_directories_search_predicate
, direction
, wrap
, skip_current
);
515 result
= search(*Tags
, m_songs_search_predicate
, direction
, wrap
, skip_current
);
519 /***********************************************************************/
521 bool TagEditor::actionRunnable()
523 // TODO: put something more refined here. It requires reworking
524 // runAction though, i.e. splitting it into smaller parts.
525 return (w
== Tags
&& !Tags
->empty())
529 void TagEditor::runAction()
531 using Global::wFooter
;
533 if (w
== FParserDialog
)
535 size_t choice
= FParserDialog
->choice();
536 if (choice
== 2) // cancel
544 // prepare additional windows
546 FParserLegend
->clear();
547 *FParserLegend
<< "%a - artist\n";
548 *FParserLegend
<< "%A - album artist\n";
549 *FParserLegend
<< "%t - title\n";
550 *FParserLegend
<< "%b - album\n";
551 *FParserLegend
<< "%y - date\n";
552 *FParserLegend
<< "%n - track number\n";
553 *FParserLegend
<< "%g - genre\n";
554 *FParserLegend
<< "%c - composer\n";
555 *FParserLegend
<< "%p - performer\n";
556 *FParserLegend
<< "%d - disc\n";
557 *FParserLegend
<< "%C - comment\n\n";
558 *FParserLegend
<< NC::Format::Bold
<< "Files:\n" << NC::Format::NoBold
;
559 for (auto it
= EditedSongs
.begin(); it
!= EditedSongs
.end(); ++it
)
560 *FParserLegend
<< Config
.color2
<< " * " << NC::Color::End
<< (*it
)->getName() << '\n';
561 FParserLegend
->flush();
563 if (!Patterns
.empty())
564 Config
.pattern
= Patterns
.front();
567 FParser
->addItem("Pattern: " + Config
.pattern
);
568 FParser
->addItem("Preview");
569 FParser
->addItem("Legend");
570 FParser
->addSeparator();
571 FParser
->addItem("Proceed");
572 FParser
->addItem("Cancel");
573 if (!Patterns
.empty())
575 FParser
->addSeparator();
576 FParser
->addItem("Recent patterns", NC::List::Properties::Bold
| NC::List::Properties::Inactive
);
577 FParser
->addSeparator();
578 for (std::list
<std::string
>::const_iterator it
= Patterns
.begin(); it
!= Patterns
.end(); ++it
)
579 FParser
->addItem(*it
);
582 FParser
->setTitle(choice
== 0 ? "Get tags from filename" : "Rename files");
584 FParserUsePreview
= 1;
585 FParserHelper
= FParserLegend
;
586 FParserHelper
->display();
588 else if (w
== FParser
)
591 size_t pos
= FParser
->choice();
593 if (pos
== 4) // save
594 FParserUsePreview
= 0;
596 if (pos
== 0) // change pattern
598 std::string new_pattern
;
600 Statusbar::ScopedLock slock
;
601 Statusbar::put() << "Pattern: ";
602 new_pattern
= wFooter
->prompt(Config
.pattern
);
604 Config
.pattern
= new_pattern
;
605 FParser
->at(0).value() = "Pattern: ";
606 FParser
->at(0).value() += Config
.pattern
;
608 else if (pos
== 1 || pos
== 4) // preview or proceed
611 Statusbar::print("Parsing...");
612 FParserPreview
->clear();
613 for (auto it
= EditedSongs
.begin(); it
!= EditedSongs
.end(); ++it
)
615 MPD::MutableSong
&s
= **it
;
616 if (FParserDialog
->choice() == 0) // get tags from filename
618 if (FParserUsePreview
)
620 *FParserPreview
<< NC::Format::Bold
<< s
.getName() << ":\n" << NC::Format::NoBold
;
621 *FParserPreview
<< ParseFilename(s
, Config
.pattern
, FParserUsePreview
) << '\n';
624 ParseFilename(s
, Config
.pattern
, FParserUsePreview
);
628 std::string file
= s
.getName();
629 size_t last_dot
= file
.rfind(".");
630 std::string extension
= file
.substr(last_dot
);
631 std::string new_file
= GenerateFilename(s
, "{" + Config
.pattern
+ "}");
632 if (new_file
.empty() && !FParserUsePreview
)
634 Statusbar::printf("File \"%1%\" would have an empty name", s
.getName());
635 FParserUsePreview
= 1;
638 if (!FParserUsePreview
)
639 s
.setNewName(new_file
+ extension
);
640 *FParserPreview
<< file
<< Config
.color2
<< " -> " << NC::Color::End
;
641 if (new_file
.empty())
642 *FParserPreview
<< Config
.empty_tags_color
<< Config
.empty_tag
<< NC::Color::End
;
644 *FParserPreview
<< new_file
<< extension
;
645 *FParserPreview
<< "\n\n";
650 if (FParserUsePreview
)
652 FParserHelper
= FParserPreview
;
653 FParserHelper
->flush();
654 FParserHelper
->display();
658 Patterns
.remove(Config
.pattern
);
659 Patterns
.insert(Patterns
.begin(), Config
.pattern
);
662 if (pos
!= 4 || success
)
663 Statusbar::print("Operation finished");
665 else if (pos
== 2) // show legend
667 FParserHelper
= FParserLegend
;
668 FParserHelper
->display();
670 else if (pos
== 5) // cancel
674 else // list of patterns
676 Config
.pattern
= FParser
->current()->value();
677 FParser
->at(0).value() = "Pattern: " + Config
.pattern
;
689 if ((w
!= TagTypes
&& w
!= Tags
) || Tags
->empty()) // after this point we start dealing with tags
693 // if there are selected songs, perform operations only on them
694 if (hasSelected(Tags
->begin(), Tags
->end()))
696 for (auto it
= Tags
->begin(); it
!= Tags
->end(); ++it
)
697 if (it
->isSelected())
698 EditedSongs
.push_back(&it
->value());
702 for (auto it
= Tags
->begin(); it
!= Tags
->end(); ++it
)
703 EditedSongs
.push_back(&it
->value());
706 size_t id
= TagTypes
->choice();
708 if (w
== TagTypes
&& id
== 5)
710 Actions::confirmAction("Number tracks?");
711 auto it
= EditedSongs
.begin();
712 for (unsigned i
= 1; i
<= EditedSongs
.size(); ++i
, ++it
)
714 if (Config
.tag_editor_extended_numeration
)
715 (*it
)->setTrack(boost::lexical_cast
<std::string
>(i
) + "/" + boost::lexical_cast
<std::string
>(EditedSongs
.size()));
717 (*it
)->setTrack(boost::lexical_cast
<std::string
>(i
));
718 // discard other track number tags
719 (*it
)->setTrack("", 1);
721 Statusbar::print("Tracks numbered");
727 MPD::Song::GetFunction get
= SongInfo::Tags
[id
].Get
;
728 MPD::MutableSong::SetFunction set
= SongInfo::Tags
[id
].Set
;
729 if (id
> 0 && w
== TagTypes
)
731 Statusbar::ScopedLock slock
;
732 Statusbar::put() << NC::Format::Bold
<< TagTypes
->current()->value() << NC::Format::NoBold
<< ": ";
733 std::string new_tag
= wFooter
->prompt(Tags
->current()->value().getTags(get
));
734 for (auto it
= EditedSongs
.begin(); it
!= EditedSongs
.end(); ++it
)
735 (*it
)->setTags(set
, new_tag
);
739 Statusbar::ScopedLock slock
;
740 Statusbar::put() << NC::Format::Bold
<< TagTypes
->current()->value() << NC::Format::NoBold
<< ": ";
741 std::string new_tag
= wFooter
->prompt(Tags
->current()->value().getTags(get
));
742 if (new_tag
!= Tags
->current()->value().getTags(get
))
743 Tags
->current()->value().setTags(set
, new_tag
);
744 Tags
->scroll(NC::Scroll::Down
);
749 if (id
== 12) // filename related options
753 FParserDialog
->reset();
758 Statusbar::ScopedLock slock
;
759 MPD::MutableSong
&s
= Tags
->current()->value();
760 std::string old_name
= s
.getNewName().empty() ? s
.getName() : s
.getNewName();
761 size_t last_dot
= old_name
.rfind(".");
762 std::string extension
= old_name
.substr(last_dot
);
763 old_name
= old_name
.substr(0, last_dot
);
764 Statusbar::put() << NC::Format::Bold
<< "New filename: " << NC::Format::NoBold
;
765 std::string new_name
= wFooter
->prompt(old_name
);
766 if (!new_name
.empty())
767 s
.setNewName(new_name
+ extension
);
768 Tags
->scroll(NC::Scroll::Down
);
771 else if (id
== TagTypes
->size()-5) // capitalize first letters
773 Statusbar::print("Processing...");
774 for (auto it
= EditedSongs
.begin(); it
!= EditedSongs
.end(); ++it
)
775 CapitalizeFirstLetters(**it
);
776 Statusbar::print("Done");
778 else if (id
== TagTypes
->size()-4) // lower all letters
780 Statusbar::print("Processing...");
781 for (auto it
= EditedSongs
.begin(); it
!= EditedSongs
.end(); ++it
)
782 LowerAllLetters(**it
);
783 Statusbar::print("Done");
785 else if (id
== TagTypes
->size()-2) // reset
787 for (auto it
= Tags
->beginV(); it
!= Tags
->endV(); ++it
)
788 it
->clearModifications();
789 Statusbar::print("Changes reset");
791 else if (id
== TagTypes
->size()-1) // save
794 Statusbar::print("Writing changes...");
795 for (auto it
= EditedSongs
.begin(); it
!= EditedSongs
.end(); ++it
)
797 Statusbar::printf("Writing tags in \"%1%\"...", (*it
)->getName());
798 if (!Tags::write(**it
))
800 const char msg
[] = "Error while writing tags in \"%1%\"";
801 Statusbar::printf(msg
, wideShorten((*it
)->getURI(), COLS
-const_strlen(msg
)).c_str());
808 Statusbar::print("Tags updated");
809 TagTypes
->setHighlightColor(Config
.main_highlight_color
);
813 Dirs
->setHighlightColor(Config
.active_column_color
);
814 Mpd
.UpdateDirectory(getSharedDirectory(Tags
->beginV(), Tags
->endV()));
823 /***********************************************************************/
825 bool TagEditor::itemAvailable()
828 return !Tags
->empty();
832 bool TagEditor::addItemToPlaylist(bool play
)
834 return addSongToPlaylist(*Tags
->currentV(), play
);
837 std::vector
<MPD::Song
> TagEditor::getSelectedSongs()
839 std::vector
<MPD::Song
> result
;
842 for (auto it
= Tags
->begin(); it
!= Tags
->end(); ++it
)
843 if (it
->isSelected())
844 result
.push_back(it
->value());
845 // if no song was selected, add current one
846 if (result
.empty() && !Tags
->empty())
847 result
.push_back(Tags
->current()->value());
852 /***********************************************************************/
854 bool TagEditor::previousColumnAvailable()
859 if (!TagTypes
->empty() && !Dirs
->empty())
862 else if (w
== TagTypes
)
864 if (!Dirs
->empty() && isAnyModified(*Tags
))
865 Actions::confirmAction("There are pending changes, are you sure?");
868 else if (w
== FParserHelper
)
873 void TagEditor::previousColumn()
877 Tags
->setHighlightColor(Config
.main_highlight_color
);
880 TagTypes
->setHighlightColor(Config
.active_column_color
);
882 else if (w
== TagTypes
)
884 TagTypes
->setHighlightColor(Config
.main_highlight_color
);
887 Dirs
->setHighlightColor(Config
.active_column_color
);
889 else if (w
== FParserHelper
)
891 FParserHelper
->setBorder(Config
.window_border
);
892 FParserHelper
->display();
894 FParser
->setBorder(Config
.active_window_border
);
899 bool TagEditor::nextColumnAvailable()
904 if (!TagTypes
->empty() && !Tags
->empty())
907 else if (w
== TagTypes
)
912 else if (w
== FParser
)
917 void TagEditor::nextColumn()
921 Dirs
->setHighlightColor(Config
.main_highlight_color
);
924 TagTypes
->setHighlightColor(Config
.active_column_color
);
926 else if (w
== TagTypes
&& TagTypes
->choice() < 13 && !Tags
->empty())
928 TagTypes
->setHighlightColor(Config
.main_highlight_color
);
931 Tags
->setHighlightColor(Config
.active_column_color
);
933 else if (w
== FParser
)
935 FParser
->setBorder(Config
.window_border
);
938 FParserHelper
->setBorder(Config
.active_window_border
);
939 FParserHelper
->display();
943 /***********************************************************************/
945 void TagEditor::LocateSong(const MPD::Song
&s
)
947 if (myScreen
== this)
950 if (s
.getDirectory().empty())
953 if (Global::myScreen
!= this)
956 // go to right directory
957 if (itsBrowsedDir
!= s
.getDirectory())
959 itsBrowsedDir
= s
.getDirectory();
960 size_t last_slash
= itsBrowsedDir
.rfind('/');
961 if (last_slash
!= std::string::npos
)
962 itsBrowsedDir
= itsBrowsedDir
.substr(0, last_slash
);
965 if (itsBrowsedDir
.empty())
970 if (itsBrowsedDir
== "/")
971 Dirs
->reset(); // go to the first pos, which is "." (music dir root)
973 // highlight directory we need and get files from it
974 std::string dir
= getBasename(s
.getDirectory());
975 for (size_t i
= 0; i
< Dirs
->size(); ++i
)
977 if ((*Dirs
)[i
].value().first
== dir
)
983 // refresh window so we can be highlighted item
989 // reset TagTypes since it can be under Filename
990 // and then songs in right column are not visible.
992 // go to the right column
996 // highlight our file
997 for (size_t i
= 0; i
< Tags
->size(); ++i
)
999 if ((*Tags
)[i
].value() == s
)
1009 bool isAnyModified(const NC::Menu
<MPD::MutableSong
> &m
)
1011 for (auto it
= m
.beginV(); it
!= m
.endV(); ++it
)
1012 if (it
->isModified())
1017 std::string
CapitalizeFirstLetters(const std::string
&s
)
1019 std::wstring ws
= ToWString(s
);
1021 for (auto it
= ws
.begin(); it
!= ws
.end(); ++it
)
1023 if (!iswalpha(prev
) && prev
!= L
'\'')
1024 *it
= towupper(*it
);
1027 return ToString(ws
);
1030 void CapitalizeFirstLetters(MPD::MutableSong
&s
)
1032 for (const SongInfo::Metadata
*m
= SongInfo::Tags
; m
->Name
; ++m
)
1035 for (std::string tag
; !(tag
= (s
.*m
->Get
)(i
)).empty(); ++i
)
1036 (s
.*m
->Set
)(CapitalizeFirstLetters(tag
), i
);
1040 void LowerAllLetters(MPD::MutableSong
&s
)
1042 for (const SongInfo::Metadata
*m
= SongInfo::Tags
; m
->Name
; ++m
)
1045 for (std::string tag
; !(tag
= (s
.*m
->Get
)(i
)).empty(); ++i
)
1046 (s
.*m
->Set
)(boost::locale::to_lower(tag
), i
);
1050 void GetPatternList()
1052 if (Patterns
.empty())
1054 std::ifstream
input(PatternsFile
.c_str());
1055 if (input
.is_open())
1058 while (std::getline(input
, line
))
1060 Patterns
.push_back(line
);
1066 void SavePatternList()
1068 std::ofstream
output(PatternsFile
.c_str());
1069 if (output
.is_open())
1071 std::list
<std::string
>::const_iterator it
= Patterns
.begin();
1072 for (unsigned i
= 30; it
!= Patterns
.end() && i
; ++it
, --i
)
1073 output
<< *it
<< std::endl
;
1077 MPD::MutableSong::SetFunction
IntoSetFunction(char c
)
1082 return &MPD::MutableSong::setArtist
;
1084 return &MPD::MutableSong::setAlbumArtist
;
1086 return &MPD::MutableSong::setTitle
;
1088 return &MPD::MutableSong::setAlbum
;
1090 return &MPD::MutableSong::setDate
;
1092 return &MPD::MutableSong::setTrack
;
1094 return &MPD::MutableSong::setGenre
;
1096 return &MPD::MutableSong::setComposer
;
1098 return &MPD::MutableSong::setPerformer
;
1100 return &MPD::MutableSong::setDisc
;
1102 return &MPD::MutableSong::setComment
;
1108 std::string
GenerateFilename(const MPD::MutableSong
&s
, const std::string
&pattern
)
1110 std::string result
= Format::stringify
<char>(Format::parse(pattern
), &s
);
1111 removeInvalidCharsFromFilename(result
, Config
.generate_win32_compatible_filenames
);
1115 std::string
ParseFilename(MPD::MutableSong
&s
, std::string mask
, bool preview
)
1117 std::ostringstream result
;
1118 std::vector
<std::string
> separators
;
1119 std::vector
< std::pair
<char, std::string
> > tags
;
1120 std::string file
= s
.getName().substr(0, s
.getName().rfind("."));
1122 size_t i
= mask
.find("%");
1124 if (!mask
.substr(0, i
).empty())
1125 file
= file
.substr(i
);
1127 for (; i
!= std::string::npos
; i
= mask
.find("%"))
1129 tags
.push_back(std::make_pair(mask
.at(i
+1), ""));
1130 mask
= mask
.substr(i
+2);
1133 separators
.push_back(mask
.substr(0, i
));
1136 for (auto it
= separators
.begin(); it
!= separators
.end(); ++it
, ++i
)
1138 size_t j
= file
.find(*it
);
1139 tags
.at(i
).second
= file
.substr(0, j
);
1140 if (j
+it
->length() > file
.length())
1142 file
= file
.substr(j
+it
->length());
1146 if (i
>= tags
.size())
1148 tags
.at(i
).second
= file
;
1154 return "Error while parsing filename!\n";
1157 for (auto it
= tags
.begin(); it
!= tags
.end(); ++it
)
1159 for (std::string::iterator j
= it
->second
.begin(); j
!= it
->second
.end(); ++j
)
1165 MPD::MutableSong::SetFunction set
= IntoSetFunction(it
->first
);
1167 s
.setTags(set
, it
->second
);
1170 result
<< "%" << it
->first
<< ": " << it
->second
<< "\n";
1172 return result
.str();
1175 std::string
SongToString(const MPD::MutableSong
&s
)
1178 size_t i
= myTagEditor
->TagTypes
->choice();
1180 result
= (s
.*SongInfo::Tags
[i
].Get
)(0);
1182 result
= s
.getNewName().empty() ? s
.getName() : s
.getName() + " -> " + s
.getNewName();
1183 return result
.empty() ? Config
.empty_tag
: result
;
1186 bool DirEntryMatcher(const Regex::Regex
&rx
, const std::pair
<std::string
, std::string
> &dir
, bool filter
)
1188 if (dir
.first
== "." || dir
.first
== "..")
1190 return Regex::search(dir
.first
, rx
);
1193 bool SongEntryMatcher(const Regex::Regex
&rx
, const MPD::MutableSong
&s
)
1195 return Regex::search(SongToString(s
), rx
);