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/bind.hpp>
26 #include <boost/locale/conversion.hpp>
37 #include "song_info.h"
38 #include "statusbar.h"
39 #include "utility/comparators.h"
42 #include "screen_switcher.h"
44 using Global::myScreen
;
45 using Global::MainHeight
;
46 using Global::MainStartY
;
48 TagEditor
*myTagEditor
;
52 size_t LeftColumnWidth
;
53 size_t LeftColumnStartX
;
54 size_t MiddleColumnWidth
;
55 size_t MiddleColumnStartX
;
56 size_t RightColumnWidth
;
57 size_t RightColumnStartX
;
59 size_t FParserDialogWidth
;
60 size_t FParserDialogHeight
;
62 size_t FParserWidthOne
;
63 size_t FParserWidthTwo
;
66 std::list
<std::string
> Patterns
;
67 std::string PatternsFile
= "patterns.list";
69 bool isAnyModified(const NC::Menu
<MPD::MutableSong
> &m
);
71 std::string
CapitalizeFirstLetters(const std::string
&s
);
72 void CapitalizeFirstLetters(MPD::MutableSong
&s
);
73 void LowerAllLetters(MPD::MutableSong
&s
);
75 void GetPatternList();
76 void SavePatternList();
78 MPD::MutableSong::SetFunction
IntoSetFunction(char c
);
79 std::string
GenerateFilename(const MPD::MutableSong
&s
, const std::string
&pattern
);
80 std::string
ParseFilename(MPD::MutableSong
&s
, std::string mask
, bool preview
);
82 std::string
SongToString(const MPD::MutableSong
&s
);
83 bool DirEntryMatcher(const boost::regex
&rx
, const std::pair
<std::string
, std::string
> &dir
, bool filter
);
84 bool SongEntryMatcher(const boost::regex
&rx
, const MPD::MutableSong
&s
);
88 TagEditor::TagEditor() : FParser(0), FParserHelper(0), FParserLegend(0), FParserPreview(0), itsBrowsedDir("/")
90 PatternsFile
= Config
.ncmpcpp_directory
+ "patterns.list";
91 SetDimensions(0, COLS
);
93 Dirs
= new NC::Menu
< std::pair
<std::string
, std::string
> >(0, MainStartY
, LeftColumnWidth
, MainHeight
, Config
.titles_visibility
? "Directories" : "", Config
.main_color
, NC::Border::None
);
94 Dirs
->setHighlightColor(Config
.active_column_color
);
95 Dirs
->cyclicScrolling(Config
.use_cyclic_scrolling
);
96 Dirs
->centeredCursor(Config
.centered_cursor
);
97 Dirs
->setItemDisplayer([](NC::Menu
<std::pair
<std::string
, std::string
>> &menu
) {
98 menu
<< Charset::utf8ToLocale(menu
.drawn()->value().first
);
101 TagTypes
= new NC::Menu
<std::string
>(MiddleColumnStartX
, MainStartY
, MiddleColumnWidth
, MainHeight
, Config
.titles_visibility
? "Tag types" : "", Config
.main_color
, NC::Border::None
);
102 TagTypes
->setHighlightColor(Config
.main_highlight_color
);
103 TagTypes
->cyclicScrolling(Config
.use_cyclic_scrolling
);
104 TagTypes
->centeredCursor(Config
.centered_cursor
);
105 TagTypes
->setItemDisplayer([](NC::Menu
<std::string
> &menu
) {
106 menu
<< Charset::utf8ToLocale(menu
.drawn()->value());
109 for (const SongInfo::Metadata
*m
= SongInfo::Tags
; m
->Name
; ++m
)
110 TagTypes
->addItem(m
->Name
);
111 TagTypes
->addSeparator();
112 TagTypes
->addItem("Filename");
113 TagTypes
->addSeparator();
114 if (Config
.titles_visibility
)
116 TagTypes
->addItem("Options", 1, 1);
117 TagTypes
->addSeparator();
119 TagTypes
->addItem("Capitalize First Letters");
120 TagTypes
->addItem("lower all letters");
121 TagTypes
->addSeparator();
122 TagTypes
->addItem("Reset");
123 TagTypes
->addItem("Save");
125 Tags
= new NC::Menu
<MPD::MutableSong
>(RightColumnStartX
, MainStartY
, RightColumnWidth
, MainHeight
, Config
.titles_visibility
? "Tags" : "", Config
.main_color
, NC::Border::None
);
126 Tags
->setHighlightColor(Config
.main_highlight_color
);
127 Tags
->cyclicScrolling(Config
.use_cyclic_scrolling
);
128 Tags
->centeredCursor(Config
.centered_cursor
);
129 Tags
->setSelectedPrefix(Config
.selected_item_prefix
);
130 Tags
->setSelectedSuffix(Config
.selected_item_suffix
);
131 Tags
->setItemDisplayer(Display::Tags
);
133 auto parser_display
= [](NC::Menu
<std::string
> &menu
) {
134 menu
<< Charset::utf8ToLocale(menu
.drawn()->value());
137 FParserDialog
= new NC::Menu
<std::string
>((COLS
-FParserDialogWidth
)/2, (MainHeight
-FParserDialogHeight
)/2+MainStartY
, FParserDialogWidth
, FParserDialogHeight
, "", Config
.main_color
, Config
.window_border
);
138 FParserDialog
->cyclicScrolling(Config
.use_cyclic_scrolling
);
139 FParserDialog
->centeredCursor(Config
.centered_cursor
);
140 FParserDialog
->setItemDisplayer(parser_display
);
141 FParserDialog
->addItem("Get tags from filename");
142 FParserDialog
->addItem("Rename files");
143 FParserDialog
->addItem("Cancel");
145 FParser
= new NC::Menu
<std::string
>((COLS
-FParserWidth
)/2, (MainHeight
-FParserHeight
)/2+MainStartY
, FParserWidthOne
, FParserHeight
, "_", Config
.main_color
, Config
.active_window_border
);
146 FParser
->cyclicScrolling(Config
.use_cyclic_scrolling
);
147 FParser
->centeredCursor(Config
.centered_cursor
);
148 FParser
->setItemDisplayer(parser_display
);
150 FParserLegend
= new NC::Scrollpad((COLS
-FParserWidth
)/2+FParserWidthOne
, (MainHeight
-FParserHeight
)/2+MainStartY
, FParserWidthTwo
, FParserHeight
, "Legend", Config
.main_color
, Config
.window_border
);
152 FParserPreview
= new NC::Scrollpad((COLS
-FParserWidth
)/2+FParserWidthOne
, (MainHeight
-FParserHeight
)/2+MainStartY
, FParserWidthTwo
, FParserHeight
, "Preview", Config
.main_color
, Config
.window_border
);
157 void TagEditor::SetDimensions(size_t x_offset
, size_t width
)
159 MiddleColumnWidth
= std::min(26, COLS
-2);
160 LeftColumnStartX
= x_offset
;
161 LeftColumnWidth
= (width
-MiddleColumnWidth
)/2;
162 MiddleColumnStartX
= LeftColumnStartX
+LeftColumnWidth
+1;
163 RightColumnWidth
= width
-LeftColumnWidth
-MiddleColumnWidth
-2;
164 RightColumnStartX
= MiddleColumnStartX
+MiddleColumnWidth
+1;
166 FParserDialogWidth
= std::min(30, COLS
);
167 FParserDialogHeight
= std::min(size_t(5), MainHeight
);
168 FParserWidth
= width
*0.9;
169 FParserHeight
= std::min(size_t(LINES
*0.8), MainHeight
);
170 FParserWidthOne
= FParserWidth
/2;
171 FParserWidthTwo
= FParserWidth
-FParserWidthOne
;
174 void TagEditor::resize()
176 size_t x_offset
, width
;
177 getWindowResizeParams(x_offset
, width
);
178 SetDimensions(x_offset
, width
);
180 Dirs
->resize(LeftColumnWidth
, MainHeight
);
181 TagTypes
->resize(MiddleColumnWidth
, MainHeight
);
182 Tags
->resize(RightColumnWidth
, MainHeight
);
183 FParserDialog
->resize(FParserDialogWidth
, FParserDialogHeight
);
184 FParser
->resize(FParserWidthOne
, FParserHeight
);
185 FParserLegend
->resize(FParserWidthTwo
, FParserHeight
);
186 FParserPreview
->resize(FParserWidthTwo
, FParserHeight
);
188 Dirs
->moveTo(LeftColumnStartX
, MainStartY
);
189 TagTypes
->moveTo(MiddleColumnStartX
, MainStartY
);
190 Tags
->moveTo(RightColumnStartX
, MainStartY
);
192 FParserDialog
->moveTo(x_offset
+(width
-FParserDialogWidth
)/2, (MainHeight
-FParserDialogHeight
)/2+MainStartY
);
193 FParser
->moveTo(x_offset
+(width
-FParserWidth
)/2, (MainHeight
-FParserHeight
)/2+MainStartY
);
194 FParserLegend
->moveTo(x_offset
+(width
-FParserWidth
)/2+FParserWidthOne
, (MainHeight
-FParserHeight
)/2+MainStartY
);
195 FParserPreview
->moveTo(x_offset
+(width
-FParserWidth
)/2+FParserWidthOne
, (MainHeight
-FParserHeight
)/2+MainStartY
);
200 std::wstring
TagEditor::title()
202 return L
"Tag editor";
205 void TagEditor::switchTo()
207 SwitchTo::execute(this);
212 void TagEditor::refresh()
215 mvvline(MainStartY
, MiddleColumnStartX
-1, 0, MainHeight
);
217 mvvline(MainStartY
, RightColumnStartX
-1, 0, MainHeight
);
220 if (w
== FParserDialog
)
222 FParserDialog
->display();
224 else if (w
== FParser
|| w
== FParserHelper
)
227 FParserHelper
->display();
231 void TagEditor::update()
233 if (Dirs
->reallyEmpty())
235 Dirs
->Window::clear();
238 if (itsBrowsedDir
!= "/")
239 Dirs
->addItem(std::make_pair("..", getParentDirectory(itsBrowsedDir
)));
241 Dirs
->addItem(std::make_pair(".", "/"));
242 Mpd
.GetDirectories(itsBrowsedDir
, [this](std::string directory
) {
243 Dirs
->addItem(std::make_pair(getBasename(directory
), directory
));
244 if (directory
== itsHighlightedDir
)
245 Dirs
->highlight(Dirs
->size()-1);
247 std::sort(Dirs
->beginV()+1, Dirs
->endV(),
248 LocaleBasedSorting(std::locale(), Config
.ignore_leading_the
));
252 if (Tags
->reallyEmpty())
255 Mpd
.GetSongs(Dirs
->current().value().second
, [this](MPD::Song s
) {
258 std::sort(Tags
->beginV(), Tags
->endV(),
259 LocaleBasedSorting(std::locale(), Config
.ignore_leading_the
));
263 if (w
== TagTypes
&& TagTypes
->choice() < 13)
267 else if (TagTypes
->choice() >= 13)
269 Tags
->Window::clear();
270 Tags
->Window::refresh();
274 void TagEditor::enterPressed()
276 using Global::wFooter
;
280 bool has_subdirs
= false;
281 Mpd
.GetDirectories(Dirs
->current().value().second
, [&has_subdirs
](std::string
) {
286 itsHighlightedDir
= itsBrowsedDir
;
287 itsBrowsedDir
= Dirs
->current().value().second
;
292 Statusbar::print("No subdirectories found");
294 else if (w
== FParserDialog
)
296 size_t choice
= FParserDialog
->choice();
297 if (choice
== 2) // cancel
305 // prepare additional windows
307 FParserLegend
->clear();
308 *FParserLegend
<< "%a - artist\n";
309 *FParserLegend
<< "%A - album artist\n";
310 *FParserLegend
<< "%t - title\n";
311 *FParserLegend
<< "%b - album\n";
312 *FParserLegend
<< "%y - date\n";
313 *FParserLegend
<< "%n - track number\n";
314 *FParserLegend
<< "%g - genre\n";
315 *FParserLegend
<< "%c - composer\n";
316 *FParserLegend
<< "%p - performer\n";
317 *FParserLegend
<< "%d - disc\n";
318 *FParserLegend
<< "%C - comment\n\n";
319 *FParserLegend
<< NC::Format::Bold
<< "Files:\n" << NC::Format::NoBold
;
320 for (auto it
= EditedSongs
.begin(); it
!= EditedSongs
.end(); ++it
)
321 *FParserLegend
<< Config
.color2
<< " * " << NC::Color::End
<< (*it
)->getName() << '\n';
322 FParserLegend
->flush();
324 if (!Patterns
.empty())
325 Config
.pattern
= Patterns
.front();
328 FParser
->addItem("Pattern: " + Config
.pattern
);
329 FParser
->addItem("Preview");
330 FParser
->addItem("Legend");
331 FParser
->addSeparator();
332 FParser
->addItem("Proceed");
333 FParser
->addItem("Cancel");
334 if (!Patterns
.empty())
336 FParser
->addSeparator();
337 FParser
->addItem("Recent patterns", 1, 1);
338 FParser
->addSeparator();
339 for (std::list
<std::string
>::const_iterator it
= Patterns
.begin(); it
!= Patterns
.end(); ++it
)
340 FParser
->addItem(*it
);
343 FParser
->setTitle(choice
== 0 ? "Get tags from filename" : "Rename files");
345 FParserUsePreview
= 1;
346 FParserHelper
= FParserLegend
;
347 FParserHelper
->display();
349 else if (w
== FParser
)
352 size_t pos
= FParser
->choice();
354 if (pos
== 4) // save
355 FParserUsePreview
= 0;
357 if (pos
== 0) // change pattern
360 Statusbar::put() << "Pattern: ";
361 std::string new_pattern
= wFooter
->getString(Config
.pattern
);
363 if (!new_pattern
.empty())
365 Config
.pattern
= new_pattern
;
366 FParser
->at(0).value() = "Pattern: ";
367 FParser
->at(0).value() += Config
.pattern
;
370 else if (pos
== 1 || pos
== 4) // preview or proceed
373 Statusbar::print("Parsing...");
374 FParserPreview
->clear();
375 for (auto it
= EditedSongs
.begin(); it
!= EditedSongs
.end(); ++it
)
377 MPD::MutableSong
&s
= **it
;
378 if (FParserDialog
->choice() == 0) // get tags from filename
380 if (FParserUsePreview
)
382 *FParserPreview
<< NC::Format::Bold
<< s
.getName() << ":\n" << NC::Format::NoBold
;
383 *FParserPreview
<< ParseFilename(s
, Config
.pattern
, FParserUsePreview
) << '\n';
386 ParseFilename(s
, Config
.pattern
, FParserUsePreview
);
390 std::string file
= s
.getName();
391 size_t last_dot
= file
.rfind(".");
392 std::string extension
= file
.substr(last_dot
);
393 std::string new_file
= GenerateFilename(s
, "{" + Config
.pattern
+ "}");
394 if (new_file
.empty() && !FParserUsePreview
)
396 Statusbar::printf("File \"%1%\" would have an empty name", s
.getName());
397 FParserUsePreview
= 1;
400 if (!FParserUsePreview
)
401 s
.setNewName(new_file
+ extension
);
402 *FParserPreview
<< file
<< Config
.color2
<< " -> " << NC::Color::End
;
403 if (new_file
.empty())
404 *FParserPreview
<< Config
.empty_tags_color
<< Config
.empty_tag
<< NC::Color::End
;
406 *FParserPreview
<< new_file
<< extension
;
407 *FParserPreview
<< "\n\n";
412 if (FParserUsePreview
)
414 FParserHelper
= FParserPreview
;
415 FParserHelper
->flush();
416 FParserHelper
->display();
420 Patterns
.remove(Config
.pattern
);
421 Patterns
.insert(Patterns
.begin(), Config
.pattern
);
424 if (pos
!= 4 || success
)
425 Statusbar::print("Operation finished");
427 else if (pos
== 2) // show legend
429 FParserHelper
= FParserLegend
;
430 FParserHelper
->display();
432 else if (pos
== 5) // cancel
436 else // list of patterns
438 Config
.pattern
= FParser
->current().value();
439 FParser
->at(0).value() = "Pattern: " + Config
.pattern
;
451 if ((w
!= TagTypes
&& w
!= Tags
) || Tags
->empty()) // after this point we start dealing with tags
455 // if there are selected songs, perform operations only on them
456 if (hasSelected(Tags
->begin(), Tags
->end()))
458 for (auto it
= Tags
->begin(); it
!= Tags
->end(); ++it
)
459 if (it
->isSelected())
460 EditedSongs
.push_back(&it
->value());
464 for (auto it
= Tags
->begin(); it
!= Tags
->end(); ++it
)
465 EditedSongs
.push_back(&it
->value());
468 size_t id
= TagTypes
->choice();
470 if (w
== TagTypes
&& id
== 5)
472 bool yes
= Actions::askYesNoQuestion("Number tracks?", Status::trace
);
475 auto it
= EditedSongs
.begin();
476 for (unsigned i
= 1; i
<= EditedSongs
.size(); ++i
, ++it
)
478 if (Config
.tag_editor_extended_numeration
)
479 (*it
)->setTrack(boost::lexical_cast
<std::string
>(i
) + "/" + boost::lexical_cast
<std::string
>(EditedSongs
.size()));
481 (*it
)->setTrack(boost::lexical_cast
<std::string
>(i
));
483 Statusbar::print("Tracks numbered");
486 Statusbar::print("Aborted");
492 MPD::Song::GetFunction get
= SongInfo::Tags
[id
].Get
;
493 MPD::MutableSong::SetFunction set
= SongInfo::Tags
[id
].Set
;
494 if (id
> 0 && w
== TagTypes
)
497 Statusbar::put() << NC::Format::Bold
<< TagTypes
->current().value() << NC::Format::NoBold
<< ": ";
498 std::string new_tag
= wFooter
->getString(Tags
->current().value().getTags(get
, Config
.tags_separator
));
500 for (auto it
= EditedSongs
.begin(); it
!= EditedSongs
.end(); ++it
)
501 (*it
)->setTags(set
, new_tag
, Config
.tags_separator
);
506 Statusbar::put() << NC::Format::Bold
<< TagTypes
->current().value() << NC::Format::NoBold
<< ": ";
507 std::string new_tag
= wFooter
->getString(Tags
->current().value().getTags(get
, Config
.tags_separator
));
509 if (new_tag
!= Tags
->current().value().getTags(get
, Config
.tags_separator
))
510 Tags
->current().value().setTags(set
, new_tag
, Config
.tags_separator
);
511 Tags
->scroll(NC::Scroll::Down
);
516 if (id
== 12) // filename related options
520 FParserDialog
->reset();
525 MPD::MutableSong
&s
= Tags
->current().value();
526 std::string old_name
= s
.getNewName().empty() ? s
.getName() : s
.getNewName();
527 size_t last_dot
= old_name
.rfind(".");
528 std::string extension
= old_name
.substr(last_dot
);
529 old_name
= old_name
.substr(0, last_dot
);
531 Statusbar::put() << NC::Format::Bold
<< "New filename: " << NC::Format::NoBold
;
532 std::string new_name
= wFooter
->getString(old_name
);
534 if (!new_name
.empty())
535 s
.setNewName(new_name
+ extension
);
536 Tags
->scroll(NC::Scroll::Down
);
539 else if (id
== TagTypes
->size()-5) // capitalize first letters
541 Statusbar::print("Processing...");
542 for (auto it
= EditedSongs
.begin(); it
!= EditedSongs
.end(); ++it
)
543 CapitalizeFirstLetters(**it
);
544 Statusbar::print("Done");
546 else if (id
== TagTypes
->size()-4) // lower all letters
548 Statusbar::print("Processing...");
549 for (auto it
= EditedSongs
.begin(); it
!= EditedSongs
.end(); ++it
)
550 LowerAllLetters(**it
);
551 Statusbar::print("Done");
553 else if (id
== TagTypes
->size()-2) // reset
555 for (auto it
= Tags
->beginV(); it
!= Tags
->endV(); ++it
)
556 it
->clearModifications();
557 Statusbar::print("Changes reset");
559 else if (id
== TagTypes
->size()-1) // save
562 Statusbar::print("Writing changes...");
563 for (auto it
= EditedSongs
.begin(); it
!= EditedSongs
.end(); ++it
)
565 Statusbar::printf("Writing tags in \"%1%\"...", (*it
)->getName());
566 if (!Tags::write(**it
))
568 const char msg
[] = "Error while writing tags in \"%1%\"";
569 Statusbar::printf(msg
, wideShorten((*it
)->getURI(), COLS
-const_strlen(msg
)).c_str());
576 Statusbar::print("Tags updated");
577 TagTypes
->setHighlightColor(Config
.main_highlight_color
);
581 Dirs
->setHighlightColor(Config
.active_column_color
);
582 Mpd
.UpdateDirectory(getSharedDirectory(Tags
->beginV(), Tags
->endV()));
590 void TagEditor::spacePressed()
592 if (w
== Tags
&& !Tags
->empty())
594 Tags
->current().setSelected(!Tags
->current().isSelected());
595 w
->scroll(NC::Scroll::Down
);
599 void TagEditor::mouseButtonPressed(MEVENT me
)
601 auto tryPreviousColumn
= [this]() -> bool {
605 if (previousColumnAvailable())
612 auto tryNextColumn
= [this]() -> bool {
616 if (nextColumnAvailable())
623 if (w
== FParserDialog
)
625 if (FParserDialog
->hasCoords(me
.x
, me
.y
))
627 if (me
.bstate
& (BUTTON1_PRESSED
| BUTTON3_PRESSED
))
629 FParserDialog
->Goto(me
.y
);
630 if (me
.bstate
& BUTTON3_PRESSED
)
634 Screen
<WindowType
>::mouseButtonPressed(me
);
637 else if (w
== FParser
|| w
== FParserHelper
)
639 if (FParser
->hasCoords(me
.x
, me
.y
))
643 if (previousColumnAvailable())
648 if (size_t(me
.y
) < FParser
->size() && (me
.bstate
& (BUTTON1_PRESSED
| BUTTON3_PRESSED
)))
651 if (me
.bstate
& BUTTON3_PRESSED
)
655 Screen
<WindowType
>::mouseButtonPressed(me
);
657 else if (FParserHelper
->hasCoords(me
.x
, me
.y
))
659 if (w
!= FParserHelper
)
661 if (nextColumnAvailable())
666 scrollpadMouseButtonPressed(*FParserHelper
, me
);
669 else if (!Dirs
->empty() && Dirs
->hasCoords(me
.x
, me
.y
))
671 if (!tryPreviousColumn() || !tryPreviousColumn())
673 if (size_t(me
.y
) < Dirs
->size() && (me
.bstate
& (BUTTON1_PRESSED
| BUTTON3_PRESSED
)))
676 if (me
.bstate
& BUTTON1_PRESSED
)
682 Screen
<WindowType
>::mouseButtonPressed(me
);
685 else if (!TagTypes
->empty() && TagTypes
->hasCoords(me
.x
, me
.y
))
691 success
= tryNextColumn();
693 success
= tryPreviousColumn();
697 if (size_t(me
.y
) < TagTypes
->size() && (me
.bstate
& (BUTTON1_PRESSED
| BUTTON3_PRESSED
)))
699 if (!TagTypes
->Goto(me
.y
))
703 if (me
.bstate
& BUTTON3_PRESSED
)
707 Screen
<WindowType
>::mouseButtonPressed(me
);
709 else if (!Tags
->empty() && Tags
->hasCoords(me
.x
, me
.y
))
711 if (!tryNextColumn() || !tryNextColumn())
713 if (size_t(me
.y
) < Tags
->size() && (me
.bstate
& (BUTTON1_PRESSED
| BUTTON3_PRESSED
)))
717 if (me
.bstate
& BUTTON3_PRESSED
)
721 Screen
<WindowType
>::mouseButtonPressed(me
);
725 /***********************************************************************/
727 bool TagEditor::allowsFiltering()
729 return w
== Dirs
|| w
== Tags
;
732 std::string
TagEditor::currentFilter()
736 filter
= RegexFilter
< std::pair
<std::string
, std::string
> >::currentFilter(*Dirs
);
738 filter
= RegexFilter
<MPD::MutableSong
>::currentFilter(*Tags
);
742 void TagEditor::applyFilter(const std::string
&filter
)
749 Dirs
->clearFilterResults();
754 Tags
->clearFilterResults();
763 auto fun
= boost::bind(DirEntryMatcher
, _1
, _2
, true);
764 auto rx
= RegexFilter
< std::pair
<std::string
, std::string
> >(
765 boost::regex(filter
, Config
.regex_type
), fun
);
766 Dirs
->filter(Dirs
->begin(), Dirs
->end(), rx
);
771 auto rx
= RegexFilter
<MPD::MutableSong
>(
772 boost::regex(filter
, Config
.regex_type
), SongEntryMatcher
);
773 Tags
->filter(Tags
->begin(), Tags
->end(), rx
);
776 catch (boost::bad_expression
&) { }
779 /***********************************************************************/
781 bool TagEditor::allowsSearching()
783 return w
== Dirs
|| w
== Tags
;
786 bool TagEditor::search(const std::string
&constraint
)
788 if (constraint
.empty())
791 Dirs
->clearSearchResults();
793 Tags
->clearSearchResults();
801 auto fun
= boost::bind(DirEntryMatcher
, _1
, _2
, false);
802 auto rx
= RegexFilter
< std::pair
<std::string
, std::string
> >(
803 boost::regex(constraint
, Config
.regex_type
), fun
);
804 result
= Dirs
->search(Dirs
->begin(), Dirs
->end(), rx
);
808 auto rx
= RegexFilter
<MPD::MutableSong
>(
809 boost::regex(constraint
, Config
.regex_type
), SongEntryMatcher
);
810 result
= Tags
->search(Tags
->begin(), Tags
->end(), rx
);
814 catch (boost::bad_expression
&)
820 void TagEditor::nextFound(bool wrap
)
823 Dirs
->nextFound(wrap
);
825 Tags
->nextFound(wrap
);
828 void TagEditor::prevFound(bool wrap
)
831 Dirs
->prevFound(wrap
);
833 Tags
->prevFound(wrap
);
836 /***********************************************************************/
838 ProxySongList
TagEditor::proxySongList()
840 auto ptr
= ProxySongList();
842 ptr
= ProxySongList(*Tags
, [](NC::Menu
<MPD::MutableSong
>::Item
&item
) {
843 return &item
.value();
848 bool TagEditor::allowsSelection()
853 void TagEditor::reverseSelection()
856 reverseSelectionHelper(Tags
->begin(), Tags
->end());
859 MPD::SongList
TagEditor::getSelectedSongs()
861 MPD::SongList result
;
864 for (auto it
= Tags
->begin(); it
!= Tags
->end(); ++it
)
865 if (it
->isSelected())
866 result
.push_back(it
->value());
867 // if no song was selected, add current one
868 if (result
.empty() && !Tags
->empty())
869 result
.push_back(Tags
->current().value());
874 /***********************************************************************/
876 bool TagEditor::previousColumnAvailable()
881 if (!TagTypes
->reallyEmpty() && !Dirs
->reallyEmpty())
884 else if (w
== TagTypes
)
886 if (!Dirs
->reallyEmpty())
887 result
= ifAnyModifiedAskForDiscarding();
889 else if (w
== FParserHelper
)
894 void TagEditor::previousColumn()
898 Tags
->setHighlightColor(Config
.main_highlight_color
);
901 TagTypes
->setHighlightColor(Config
.active_column_color
);
903 else if (w
== TagTypes
)
905 TagTypes
->setHighlightColor(Config
.main_highlight_color
);
908 Dirs
->setHighlightColor(Config
.active_column_color
);
910 else if (w
== FParserHelper
)
912 FParserHelper
->setBorder(Config
.window_border
);
913 FParserHelper
->display();
915 FParser
->setBorder(Config
.active_window_border
);
920 bool TagEditor::nextColumnAvailable()
925 if (!TagTypes
->reallyEmpty() && !Tags
->reallyEmpty())
928 else if (w
== TagTypes
)
930 if (!Tags
->reallyEmpty())
933 else if (w
== FParser
)
938 void TagEditor::nextColumn()
942 Dirs
->setHighlightColor(Config
.main_highlight_color
);
945 TagTypes
->setHighlightColor(Config
.active_column_color
);
947 else if (w
== TagTypes
&& TagTypes
->choice() < 13 && !Tags
->reallyEmpty())
949 TagTypes
->setHighlightColor(Config
.main_highlight_color
);
952 Tags
->setHighlightColor(Config
.active_column_color
);
954 else if (w
== FParser
)
956 FParser
->setBorder(Config
.window_border
);
959 FParserHelper
->setBorder(Config
.active_window_border
);
960 FParserHelper
->display();
964 /***********************************************************************/
966 bool TagEditor::ifAnyModifiedAskForDiscarding()
969 if (isAnyModified(*Tags
))
970 result
= Actions::askYesNoQuestion("There are pending changes, are you sure?", Status::trace
);
974 void TagEditor::LocateSong(const MPD::Song
&s
)
976 if (myScreen
== this)
979 if (s
.getDirectory().empty())
982 if (Global::myScreen
!= this)
985 // go to right directory
986 if (itsBrowsedDir
!= s
.getDirectory())
988 itsBrowsedDir
= s
.getDirectory();
989 size_t last_slash
= itsBrowsedDir
.rfind('/');
990 if (last_slash
!= std::string::npos
)
991 itsBrowsedDir
= itsBrowsedDir
.substr(0, last_slash
);
994 if (itsBrowsedDir
.empty())
999 if (itsBrowsedDir
== "/")
1000 Dirs
->reset(); // go to the first pos, which is "." (music dir root)
1002 // highlight directory we need and get files from it
1003 std::string dir
= getBasename(s
.getDirectory());
1004 for (size_t i
= 0; i
< Dirs
->size(); ++i
)
1006 if ((*Dirs
)[i
].value().first
== dir
)
1012 // refresh window so we can be highlighted item
1018 // reset TagTypes since it can be under Filename
1019 // and then songs in right column are not visible.
1021 // go to the right column
1025 // highlight our file
1026 for (size_t i
= 0; i
< Tags
->size(); ++i
)
1028 if ((*Tags
)[i
].value() == s
)
1038 bool isAnyModified(const NC::Menu
<MPD::MutableSong
> &m
)
1040 for (auto it
= m
.beginV(); it
!= m
.endV(); ++it
)
1041 if (it
->isModified())
1046 std::string
CapitalizeFirstLetters(const std::string
&s
)
1048 std::wstring ws
= ToWString(s
);
1050 for (auto it
= ws
.begin(); it
!= ws
.end(); ++it
)
1052 if (!iswalpha(prev
) && prev
!= L
'\'')
1053 *it
= towupper(*it
);
1056 return ToString(ws
);
1059 void CapitalizeFirstLetters(MPD::MutableSong
&s
)
1061 for (const SongInfo::Metadata
*m
= SongInfo::Tags
; m
->Name
; ++m
)
1064 for (std::string tag
; !(tag
= (s
.*m
->Get
)(i
)).empty(); ++i
)
1065 (s
.*m
->Set
)(CapitalizeFirstLetters(tag
), i
);
1069 void LowerAllLetters(MPD::MutableSong
&s
)
1071 for (const SongInfo::Metadata
*m
= SongInfo::Tags
; m
->Name
; ++m
)
1074 for (std::string tag
; !(tag
= (s
.*m
->Get
)(i
)).empty(); ++i
)
1075 (s
.*m
->Set
)(boost::locale::to_lower(tag
), i
);
1079 void GetPatternList()
1081 if (Patterns
.empty())
1083 std::ifstream
input(PatternsFile
.c_str());
1084 if (input
.is_open())
1087 while (std::getline(input
, line
))
1089 Patterns
.push_back(line
);
1095 void SavePatternList()
1097 std::ofstream
output(PatternsFile
.c_str());
1098 if (output
.is_open())
1100 std::list
<std::string
>::const_iterator it
= Patterns
.begin();
1101 for (unsigned i
= 30; it
!= Patterns
.end() && i
; ++it
, --i
)
1102 output
<< *it
<< std::endl
;
1106 MPD::MutableSong::SetFunction
IntoSetFunction(char c
)
1111 return &MPD::MutableSong::setArtist
;
1113 return &MPD::MutableSong::setAlbumArtist
;
1115 return &MPD::MutableSong::setTitle
;
1117 return &MPD::MutableSong::setAlbum
;
1119 return &MPD::MutableSong::setDate
;
1121 return &MPD::MutableSong::setTrack
;
1123 return &MPD::MutableSong::setGenre
;
1125 return &MPD::MutableSong::setComposer
;
1127 return &MPD::MutableSong::setPerformer
;
1129 return &MPD::MutableSong::setDisc
;
1131 return &MPD::MutableSong::setComment
;
1137 std::string
GenerateFilename(const MPD::MutableSong
&s
, const std::string
&pattern
)
1139 std::string result
= s
.toString(pattern
, Config
.tags_separator
);
1140 removeInvalidCharsFromFilename(result
, Config
.generate_win32_compatible_filenames
);
1144 std::string
ParseFilename(MPD::MutableSong
&s
, std::string mask
, bool preview
)
1146 std::ostringstream result
;
1147 std::vector
<std::string
> separators
;
1148 std::vector
< std::pair
<char, std::string
> > tags
;
1149 std::string file
= s
.getName().substr(0, s
.getName().rfind("."));
1151 for (size_t i
= mask
.find("%"); i
!= std::string::npos
; i
= mask
.find("%"))
1153 tags
.push_back(std::make_pair(mask
.at(i
+1), ""));
1154 mask
= mask
.substr(i
+2);
1157 separators
.push_back(mask
.substr(0, i
));
1160 for (auto it
= separators
.begin(); it
!= separators
.end(); ++it
, ++i
)
1162 size_t j
= file
.find(*it
);
1163 tags
.at(i
).second
= file
.substr(0, j
);
1164 if (j
+it
->length() > file
.length())
1166 file
= file
.substr(j
+it
->length());
1170 if (i
>= tags
.size())
1172 tags
.at(i
).second
= file
;
1178 return "Error while parsing filename!\n";
1181 for (auto it
= tags
.begin(); it
!= tags
.end(); ++it
)
1183 for (std::string::iterator j
= it
->second
.begin(); j
!= it
->second
.end(); ++j
)
1189 MPD::MutableSong::SetFunction set
= IntoSetFunction(it
->first
);
1191 s
.setTags(set
, it
->second
, Config
.tags_separator
);
1194 result
<< "%" << it
->first
<< ": " << it
->second
<< "\n";
1196 return result
.str();
1199 std::string
SongToString(const MPD::MutableSong
&s
)
1202 size_t i
= myTagEditor
->TagTypes
->choice();
1204 result
= (s
.*SongInfo::Tags
[i
].Get
)(0);
1206 result
= s
.getNewName().empty() ? s
.getName() : s
.getName() + " -> " + s
.getNewName();
1207 return result
.empty() ? Config
.empty_tag
: result
;
1210 bool DirEntryMatcher(const boost::regex
&rx
, const std::pair
<std::string
, std::string
> &dir
, bool filter
)
1212 if (dir
.first
== "." || dir
.first
== "..")
1214 return boost::regex_search(dir
.first
, rx
);
1217 bool SongEntryMatcher(const boost::regex
&rx
, const MPD::MutableSong
&s
)
1219 return boost::regex_search(SongToString(s
), rx
);