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