1 /***************************************************************************
2 * Copyright (C) 2008-2017 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 "screens/tag_editor.h"
25 #include <boost/locale/conversion.hpp>
30 #include "screens/browser.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"
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
;
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
;
67 size_t FParserWidthOne
;
68 size_t FParserWidthTwo
;
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()
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
);
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
);
242 std::wstring
TagEditor::title()
244 return L
"Tag editor";
247 void TagEditor::switchTo()
249 SwitchTo::execute(this);
254 void TagEditor::refresh()
257 drawSeparator(MiddleColumnStartX
-1);
259 drawSeparator(RightColumnStartX
-1);
262 if (w
== FParserDialog
)
264 FParserDialog
->display();
266 else if (w
== FParser
|| w
== FParserHelper
)
269 FParserHelper
->display();
273 void TagEditor::update()
277 Dirs
->Window::clear();
280 if (itsBrowsedDir
!= "/")
281 Dirs
->addItem(std::make_pair("..", getParentDirectory(itsBrowsedDir
)));
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
));
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
));
307 if (w
== TagTypes
&& TagTypes
->choice() < 13)
311 else if (TagTypes
->choice() >= 13)
313 Tags
->Window::clear();
314 Tags
->Window::refresh();
318 bool TagEditor::enterDirectory()
321 if (w
== Dirs
&& !Dirs
->empty())
323 MPD::DirectoryIterator directory
= Mpd
.GetDirectories(Dirs
->current()->value().second
), end
;
324 bool has_subdirs
= directory
!= end
;
328 itsHighlightedDir
= itsBrowsedDir
;
329 itsBrowsedDir
= Dirs
->current()->value().second
;
338 void TagEditor::mouseButtonPressed(MEVENT me
)
340 auto tryPreviousColumn
= [this]() -> bool {
344 if (previousColumnAvailable())
351 auto tryNextColumn
= [this]() -> bool {
355 if (nextColumnAvailable())
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
)
373 Screen
<WindowType
>::mouseButtonPressed(me
);
376 else if (w
== FParser
|| w
== FParserHelper
)
378 if (FParser
->hasCoords(me
.x
, me
.y
))
382 if (previousColumnAvailable())
387 if (size_t(me
.y
) < FParser
->size() && (me
.bstate
& (BUTTON1_PRESSED
| BUTTON3_PRESSED
)))
390 if (me
.bstate
& BUTTON3_PRESSED
)
394 Screen
<WindowType
>::mouseButtonPressed(me
);
396 else if (FParserHelper
->hasCoords(me
.x
, me
.y
))
398 if (w
!= FParserHelper
)
400 if (nextColumnAvailable())
405 scrollpadMouseButtonPressed(*FParserHelper
, me
);
408 else if (!Dirs
->empty() && Dirs
->hasCoords(me
.x
, me
.y
))
410 if (!tryPreviousColumn() || !tryPreviousColumn())
412 if (size_t(me
.y
) < Dirs
->size() && (me
.bstate
& (BUTTON1_PRESSED
| BUTTON3_PRESSED
)))
415 if (me
.bstate
& BUTTON1_PRESSED
)
419 Screen
<WindowType
>::mouseButtonPressed(me
);
422 else if (!TagTypes
->empty() && TagTypes
->hasCoords(me
.x
, me
.y
))
428 success
= tryNextColumn();
430 success
= tryPreviousColumn();
434 if (size_t(me
.y
) < TagTypes
->size() && (me
.bstate
& (BUTTON1_PRESSED
| BUTTON3_PRESSED
)))
436 if (!TagTypes
->Goto(me
.y
))
440 if (me
.bstate
& BUTTON3_PRESSED
)
444 Screen
<WindowType
>::mouseButtonPressed(me
);
446 else if (!Tags
->empty() && Tags
->hasCoords(me
.x
, me
.y
))
448 if (!tryNextColumn() || !tryNextColumn())
450 if (size_t(me
.y
) < Tags
->size() && (me
.bstate
& (BUTTON1_PRESSED
| BUTTON3_PRESSED
)))
454 if (me
.bstate
& BUTTON3_PRESSED
)
458 Screen
<WindowType
>::mouseButtonPressed(me
);
462 /***********************************************************************/
464 bool TagEditor::allowsSearching()
466 return w
== Dirs
|| w
== Tags
;
469 const std::string
&TagEditor::searchConstraint()
472 return m_directories_search_predicate
.constraint();
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
)
482 m_directories_search_predicate
= Regex::Filter
<std::pair
<std::string
, std::string
>>(
485 std::bind(DirEntryMatcher
, ph::_1
, ph::_2
, false));
489 m_songs_search_predicate
= Regex::Filter
<MPD::MutableSong
>(
496 void TagEditor::clearSearchConstraint()
499 m_directories_search_predicate
.clear();
501 m_songs_search_predicate
.clear();
504 bool TagEditor::search(SearchDirection direction
, bool wrap
, bool skip_current
)
508 result
= ::search(*Dirs
, m_directories_search_predicate
, direction
, wrap
, skip_current
);
510 result
= ::search(*Tags
, m_songs_search_predicate
, direction
, wrap
, skip_current
);
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())
524 void TagEditor::runAction()
526 using Global::wFooter
;
528 if (w
== FParserDialog
)
530 size_t choice
= FParserDialog
->choice();
531 if (choice
== 2) // cancel
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
557 << NC::FormattedColor::End
<>(Config
.color2
)
560 FParserLegend
->flush();
562 if (!Patterns
.empty())
563 Config
.pattern
= Patterns
.front();
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");
583 FParserUsePreview
= 1;
584 FParserHelper
= FParserLegend
;
585 FParserHelper
->display();
587 else if (w
== FParser
)
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
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';
623 ParseFilename(s
, Config
.pattern
, FParserUsePreview
);
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;
637 if (!FParserUsePreview
)
638 s
.setNewName(new_file
+ extension
);
639 *FParserPreview
<< file
642 << NC::FormattedColor::End
<>(Config
.color2
);
643 if (new_file
.empty())
644 *FParserPreview
<< Config
.empty_tags_color
646 << NC::FormattedColor::End
<>(Config
.empty_tags_color
);
648 *FParserPreview
<< new_file
<< extension
;
649 *FParserPreview
<< "\n\n";
654 if (FParserUsePreview
)
656 FParserHelper
= FParserPreview
;
657 FParserHelper
->flush();
658 FParserHelper
->display();
662 Patterns
.remove(Config
.pattern
);
663 Patterns
.insert(Patterns
.begin(), Config
.pattern
);
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
678 else // list of patterns
680 Config
.pattern
= FParser
->current()->value();
681 FParser
->at(0).value() = "Pattern: " + Config
.pattern
;
693 if ((w
!= TagTypes
&& w
!= Tags
) || Tags
->empty()) // after this point we start dealing with tags
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());
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()));
721 (*it
)->setTrack(boost::lexical_cast
<std::string
>(i
));
722 // discard other track number tags
723 (*it
)->setTrack("", 1);
725 Statusbar::print("Tracks numbered");
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
);
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
);
753 if (id
== 12) // filename related options
757 FParserDialog
->reset();
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
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
));
812 Statusbar::print("Tags updated");
813 setHighlightInactiveColumnFixes(*TagTypes
);
817 setHighlightFixes(*Dirs
);
818 Mpd
.UpdateDirectory(getSharedDirectory(Tags
->beginV(), Tags
->endV()));
827 /***********************************************************************/
829 bool TagEditor::itemAvailable()
832 return !Tags
->empty();
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
;
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());
856 /***********************************************************************/
858 bool TagEditor::previousColumnAvailable()
863 if (!TagTypes
->empty() && !Dirs
->empty())
866 else if (w
== TagTypes
)
868 if (!Dirs
->empty() && isAnyModified(*Tags
))
869 Actions::confirmAction("There are pending changes, are you sure?");
872 else if (w
== FParserHelper
)
877 void TagEditor::previousColumn()
881 setHighlightInactiveColumnFixes(*Tags
);
884 setHighlightFixes(*TagTypes
);
886 else if (w
== TagTypes
)
888 setHighlightInactiveColumnFixes(*TagTypes
);
891 setHighlightFixes(*Dirs
);
893 else if (w
== FParserHelper
)
895 FParserHelper
->setBorder(Config
.window_border
);
896 FParserHelper
->display();
898 FParser
->setBorder(Config
.active_window_border
);
903 bool TagEditor::nextColumnAvailable()
908 if (!TagTypes
->empty() && !Tags
->empty())
911 else if (w
== TagTypes
)
916 else if (w
== FParser
)
921 void TagEditor::nextColumn()
925 setHighlightInactiveColumnFixes(*Dirs
);
928 setHighlightFixes(*TagTypes
);
930 else if (w
== TagTypes
&& TagTypes
->choice() < 13 && !Tags
->empty())
932 setHighlightInactiveColumnFixes(*TagTypes
);
935 setHighlightFixes(*Tags
);
937 else if (w
== FParser
)
939 FParser
->setBorder(Config
.window_border
);
942 FParserHelper
->setBorder(Config
.active_window_border
);
943 FParserHelper
->display();
947 /***********************************************************************/
949 void TagEditor::LocateSong(const MPD::Song
&s
)
951 if (myScreen
== this)
954 if (s
.getDirectory().empty())
957 if (Global::myScreen
!= this)
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
);
969 if (itsBrowsedDir
.empty())
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
)
987 // refresh window so we can be highlighted item
993 // reset TagTypes since it can be under Filename
994 // and then songs in right column are not visible.
996 // go to the right column
1000 // highlight our file
1001 for (size_t i
= 0; i
< Tags
->size(); ++i
)
1003 if ((*Tags
)[i
].value() == s
)
1013 bool isAnyModified(const NC::Menu
<MPD::MutableSong
> &m
)
1015 for (auto it
= m
.beginV(); it
!= m
.endV(); ++it
)
1016 if (it
->isModified())
1021 std::string
CapitalizeFirstLetters(const std::string
&s
)
1023 std::wstring ws
= ToWString(s
);
1025 for (auto it
= ws
.begin(); it
!= ws
.end(); ++it
)
1027 if (!iswalpha(prev
) && prev
!= L
'\'')
1028 *it
= towupper(*it
);
1031 return ToString(ws
);
1034 void CapitalizeFirstLetters(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
)(CapitalizeFirstLetters(tag
), i
);
1044 void LowerAllLetters(MPD::MutableSong
&s
)
1046 for (const SongInfo::Metadata
*m
= SongInfo::Tags
; m
->Name
; ++m
)
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())
1062 while (std::getline(input
, line
))
1064 Patterns
.push_back(line
);
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
;
1081 MPD::MutableSong::SetFunction
IntoSetFunction(char c
)
1086 return &MPD::MutableSong::setArtist
;
1088 return &MPD::MutableSong::setAlbumArtist
;
1090 return &MPD::MutableSong::setTitle
;
1092 return &MPD::MutableSong::setAlbum
;
1094 return &MPD::MutableSong::setDate
;
1096 return &MPD::MutableSong::setTrack
;
1098 return &MPD::MutableSong::setGenre
;
1100 return &MPD::MutableSong::setComposer
;
1102 return &MPD::MutableSong::setPerformer
;
1104 return &MPD::MutableSong::setDisc
;
1106 return &MPD::MutableSong::setComment
;
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
);
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);
1137 separators
.push_back(mask
.substr(0, i
));
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())
1146 file
= file
.substr(j
+it
->length());
1150 if (i
>= tags
.size())
1152 tags
.at(i
).second
= file
;
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
)
1169 MPD::MutableSong::SetFunction set
= IntoSetFunction(it
->first
);
1171 s
.setTags(set
, it
->second
);
1174 result
<< "%" << it
->first
<< ": " << it
->second
<< "\n";
1176 return result
.str();
1179 std::string
SongToString(const MPD::MutableSong
&s
)
1182 size_t i
= myTagEditor
->TagTypes
->choice();
1184 result
= (s
.*SongInfo::Tags
[i
].Get
)(0);
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
== "..")
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
);