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