configuration: load bindings from the main directory if not specified otherwise
[ncmpcpp.git] / src / tag_editor.cpp
blob2655eb66d82fd7f7c5b130bf99c5dd70c31b5193
1 /***************************************************************************
2 * Copyright (C) 2008-2014 by Andrzej Rybczak *
3 * electricityispower@gmail.com *
4 * *
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. *
9 * *
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. *
14 * *
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"
23 #ifdef HAVE_TAGLIB_H
25 #include <boost/bind.hpp>
26 #include <boost/locale/conversion.hpp>
27 #include <algorithm>
28 #include <fstream>
30 #include "actions.h"
31 #include "browser.h"
32 #include "charset.h"
33 #include "display.h"
34 #include "global.h"
35 #include "helpers.h"
36 #include "playlist.h"
37 #include "song_info.h"
38 #include "statusbar.h"
39 #include "utility/comparators.h"
40 #include "title.h"
41 #include "tags.h"
42 #include "screen_switcher.h"
44 using Global::myScreen;
45 using Global::MainHeight;
46 using Global::MainStartY;
48 TagEditor *myTagEditor;
50 namespace {//
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;
61 size_t FParserWidth;
62 size_t FParserWidthOne;
63 size_t FParserWidthTwo;
64 size_t FParserHeight;
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);
99 });
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);
154 w = Dirs;
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);
197 hasToBeResized = 0;
200 std::wstring TagEditor::title()
202 return L"Tag editor";
205 void TagEditor::switchTo()
207 SwitchTo::execute(this);
208 drawHeader();
209 refresh();
212 void TagEditor::refresh()
214 Dirs->display();
215 mvvline(MainStartY, MiddleColumnStartX-1, 0, MainHeight);
216 TagTypes->display();
217 mvvline(MainStartY, RightColumnStartX-1, 0, MainHeight);
218 Tags->display();
220 if (w == FParserDialog)
222 FParserDialog->display();
224 else if (w == FParser || w == FParserHelper)
226 FParser->display();
227 FParserHelper->display();
231 void TagEditor::update()
233 if (Dirs->reallyEmpty())
235 Dirs->Window::clear();
236 Tags->clear();
238 if (itsBrowsedDir != "/")
239 Dirs->addItem(std::make_pair("..", getParentDirectory(itsBrowsedDir)));
240 else
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));
249 Dirs->display();
252 if (Tags->reallyEmpty())
254 Tags->reset();
255 Mpd.GetSongs(Dirs->current().value().second, [this](MPD::Song s) {
256 Tags->addItem(s);
258 std::sort(Tags->beginV(), Tags->endV(),
259 LocaleBasedSorting(std::locale(), Config.ignore_leading_the));
260 Tags->refresh();
263 if (w == TagTypes && TagTypes->choice() < 13)
265 Tags->refresh();
267 else if (TagTypes->choice() >= 13)
269 Tags->Window::clear();
270 Tags->Window::refresh();
274 void TagEditor::enterPressed()
276 using Global::wFooter;
278 if (w == Dirs)
280 bool has_subdirs = false;
281 Mpd.GetDirectories(Dirs->current().value().second, [&has_subdirs](std::string) {
282 has_subdirs = true;
284 if (has_subdirs)
286 itsHighlightedDir = itsBrowsedDir;
287 itsBrowsedDir = Dirs->current().value().second;
288 Dirs->clear();
289 Dirs->reset();
291 else
292 Statusbar::print("No subdirectories found");
294 else if (w == FParserDialog)
296 size_t choice = FParserDialog->choice();
297 if (choice == 2) // cancel
299 w = TagTypes;
300 refresh();
301 return;
303 GetPatternList();
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();
326 FParser->clear();
327 FParser->reset();
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");
344 w = FParser;
345 FParserUsePreview = 1;
346 FParserHelper = FParserLegend;
347 FParserHelper->display();
349 else if (w == FParser)
351 bool quit = 0;
352 size_t pos = FParser->choice();
354 if (pos == 4) // save
355 FParserUsePreview = 0;
357 if (pos == 0) // change pattern
359 Statusbar::lock();
360 Statusbar::put() << "Pattern: ";
361 std::string new_pattern = wFooter->getString(Config.pattern);
362 Statusbar::unlock();
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
372 bool success = 1;
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';
385 else
386 ParseFilename(s, Config.pattern, FParserUsePreview);
388 else // rename files
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;
398 success = 0;
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;
405 else
406 *FParserPreview << new_file << extension;
407 *FParserPreview << "\n\n";
408 if (!success)
409 break;
412 if (FParserUsePreview)
414 FParserHelper = FParserPreview;
415 FParserHelper->flush();
416 FParserHelper->display();
418 else if (success)
420 Patterns.remove(Config.pattern);
421 Patterns.insert(Patterns.begin(), Config.pattern);
422 quit = 1;
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
434 quit = 1;
436 else // list of patterns
438 Config.pattern = FParser->current().value();
439 FParser->at(0).value() = "Pattern: " + Config.pattern;
442 if (quit)
444 SavePatternList();
445 w = TagTypes;
446 refresh();
447 return;
451 if ((w != TagTypes && w != Tags) || Tags->empty()) // after this point we start dealing with tags
452 return;
454 EditedSongs.clear();
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());
462 else
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);
473 if (yes)
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()));
480 else
481 (*it)->setTrack(boost::lexical_cast<std::string>(i));
483 Statusbar::print("Tracks numbered");
485 else
486 Statusbar::print("Aborted");
487 return;
490 if (id < 11)
492 MPD::Song::GetFunction get = SongInfo::Tags[id].Get;
493 MPD::MutableSong::SetFunction set = SongInfo::Tags[id].Set;
494 if (id > 0 && w == TagTypes)
496 Statusbar::lock();
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));
499 Statusbar::unlock();
500 for (auto it = EditedSongs.begin(); it != EditedSongs.end(); ++it)
501 (*it)->setTags(set, new_tag, Config.tags_separator);
503 else if (w == Tags)
505 Statusbar::lock();
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));
508 Statusbar::unlock();
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);
514 else
516 if (id == 12) // filename related options
518 if (w == TagTypes)
520 FParserDialog->reset();
521 w = FParserDialog;
523 else if (w == Tags)
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);
530 Statusbar::lock();
531 Statusbar::put() << NC::Format::Bold << "New filename: " << NC::Format::NoBold;
532 std::string new_name = wFooter->getString(old_name);
533 Statusbar::unlock();
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
561 bool success = 1;
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());
570 success = 0;
571 break;
574 if (success)
576 Statusbar::print("Tags updated");
577 TagTypes->setHighlightColor(Config.main_highlight_color);
578 TagTypes->reset();
579 w->refresh();
580 w = Dirs;
581 Dirs->setHighlightColor(Config.active_column_color);
582 Mpd.UpdateDirectory(getSharedDirectory(Tags->beginV(), Tags->endV()));
584 else
585 Tags->clear();
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 {
602 bool result = true;
603 if (w != Dirs)
605 if (previousColumnAvailable())
606 previousColumn();
607 else
608 result = false;
610 return result;
612 auto tryNextColumn = [this]() -> bool {
613 bool result = true;
614 if (w != Tags)
616 if (nextColumnAvailable())
617 nextColumn();
618 else
619 result = false;
621 return result;
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)
631 enterPressed();
633 else
634 Screen<WindowType>::mouseButtonPressed(me);
637 else if (w == FParser || w == FParserHelper)
639 if (FParser->hasCoords(me.x, me.y))
641 if (w != FParser)
643 if (previousColumnAvailable())
644 previousColumn();
645 else
646 return;
648 if (size_t(me.y) < FParser->size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)))
650 FParser->Goto(me.y);
651 if (me.bstate & BUTTON3_PRESSED)
652 enterPressed();
654 else
655 Screen<WindowType>::mouseButtonPressed(me);
657 else if (FParserHelper->hasCoords(me.x, me.y))
659 if (w != FParserHelper)
661 if (nextColumnAvailable())
662 nextColumn();
663 else
664 return;
666 scrollpadMouseButtonPressed(*FParserHelper, me);
669 else if (!Dirs->empty() && Dirs->hasCoords(me.x, me.y))
671 if (!tryPreviousColumn() || !tryPreviousColumn())
672 return;
673 if (size_t(me.y) < Dirs->size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)))
675 Dirs->Goto(me.y);
676 if (me.bstate & BUTTON1_PRESSED)
677 enterPressed();
678 else
679 spacePressed();
681 else
682 Screen<WindowType>::mouseButtonPressed(me);
683 Tags->clear();
685 else if (!TagTypes->empty() && TagTypes->hasCoords(me.x, me.y))
687 if (w != TagTypes)
689 bool success;
690 if (w == Dirs)
691 success = tryNextColumn();
692 else
693 success = tryPreviousColumn();
694 if (!success)
695 return;
697 if (size_t(me.y) < TagTypes->size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)))
699 if (!TagTypes->Goto(me.y))
700 return;
701 TagTypes->refresh();
702 Tags->refresh();
703 if (me.bstate & BUTTON3_PRESSED)
704 enterPressed();
706 else
707 Screen<WindowType>::mouseButtonPressed(me);
709 else if (!Tags->empty() && Tags->hasCoords(me.x, me.y))
711 if (!tryNextColumn() || !tryNextColumn())
712 return;
713 if (size_t(me.y) < Tags->size() && (me.bstate & (BUTTON1_PRESSED | BUTTON3_PRESSED)))
715 Tags->Goto(me.y);
716 Tags->refresh();
717 if (me.bstate & BUTTON3_PRESSED)
718 enterPressed();
720 else
721 Screen<WindowType>::mouseButtonPressed(me);
725 /***********************************************************************/
727 bool TagEditor::allowsFiltering()
729 return w == Dirs || w == Tags;
732 std::string TagEditor::currentFilter()
734 std::string filter;
735 if (w == Dirs)
736 filter = RegexFilter< std::pair<std::string, std::string> >::currentFilter(*Dirs);
737 else if (w == Tags)
738 filter = RegexFilter<MPD::MutableSong>::currentFilter(*Tags);
739 return filter;
742 void TagEditor::applyFilter(const std::string &filter)
744 if (filter.empty())
746 if (w == Dirs)
748 Dirs->clearFilter();
749 Dirs->clearFilterResults();
751 else if (w == Tags)
753 Tags->clearFilter();
754 Tags->clearFilterResults();
756 return;
760 if (w == Dirs)
762 Dirs->showAll();
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);
768 else if (w == Tags)
770 Tags->showAll();
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())
790 if (w == Dirs)
791 Dirs->clearSearchResults();
792 else if (w == Tags)
793 Tags->clearSearchResults();
794 return false;
798 bool result = false;
799 if (w == Dirs)
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);
806 else if (w == Tags)
808 auto rx = RegexFilter<MPD::MutableSong>(
809 boost::regex(constraint, Config.regex_type), SongEntryMatcher);
810 result = Tags->search(Tags->begin(), Tags->end(), rx);
812 return result;
814 catch (boost::bad_expression &)
816 return false;
820 void TagEditor::nextFound(bool wrap)
822 if (w == Dirs)
823 Dirs->nextFound(wrap);
824 else if (w == Tags)
825 Tags->nextFound(wrap);
828 void TagEditor::prevFound(bool wrap)
830 if (w == Dirs)
831 Dirs->prevFound(wrap);
832 else if (w == Tags)
833 Tags->prevFound(wrap);
836 /***********************************************************************/
838 ProxySongList TagEditor::proxySongList()
840 auto ptr = ProxySongList();
841 if (w == Tags)
842 ptr = ProxySongList(*Tags, [](NC::Menu<MPD::MutableSong>::Item &item) {
843 return &item.value();
845 return ptr;
848 bool TagEditor::allowsSelection()
850 return w == Tags;
853 void TagEditor::reverseSelection()
855 if (w == Tags)
856 reverseSelectionHelper(Tags->begin(), Tags->end());
859 MPD::SongList TagEditor::getSelectedSongs()
861 MPD::SongList result;
862 if (w == Tags)
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());
871 return result;
874 /***********************************************************************/
876 bool TagEditor::previousColumnAvailable()
878 bool result = false;
879 if (w == Tags)
881 if (!TagTypes->reallyEmpty() && !Dirs->reallyEmpty())
882 result = true;
884 else if (w == TagTypes)
886 if (!Dirs->reallyEmpty())
887 result = ifAnyModifiedAskForDiscarding();
889 else if (w == FParserHelper)
890 result = true;
891 return result;
894 void TagEditor::previousColumn()
896 if (w == Tags)
898 Tags->setHighlightColor(Config.main_highlight_color);
899 w->refresh();
900 w = TagTypes;
901 TagTypes->setHighlightColor(Config.active_column_color);
903 else if (w == TagTypes)
905 TagTypes->setHighlightColor(Config.main_highlight_color);
906 w->refresh();
907 w = Dirs;
908 Dirs->setHighlightColor(Config.active_column_color);
910 else if (w == FParserHelper)
912 FParserHelper->setBorder(Config.window_border);
913 FParserHelper->display();
914 w = FParser;
915 FParser->setBorder(Config.active_window_border);
916 FParser->display();
920 bool TagEditor::nextColumnAvailable()
922 bool result = false;
923 if (w == Dirs)
925 if (!TagTypes->reallyEmpty() && !Tags->reallyEmpty())
926 result = true;
928 else if (w == TagTypes)
930 if (!Tags->reallyEmpty())
931 result = true;
933 else if (w == FParser)
934 result = true;
935 return result;
938 void TagEditor::nextColumn()
940 if (w == Dirs)
942 Dirs->setHighlightColor(Config.main_highlight_color);
943 w->refresh();
944 w = TagTypes;
945 TagTypes->setHighlightColor(Config.active_column_color);
947 else if (w == TagTypes && TagTypes->choice() < 13 && !Tags->reallyEmpty())
949 TagTypes->setHighlightColor(Config.main_highlight_color);
950 w->refresh();
951 w = Tags;
952 Tags->setHighlightColor(Config.active_column_color);
954 else if (w == FParser)
956 FParser->setBorder(Config.window_border);
957 FParser->display();
958 w = FParserHelper;
959 FParserHelper->setBorder(Config.active_window_border);
960 FParserHelper->display();
964 /***********************************************************************/
966 bool TagEditor::ifAnyModifiedAskForDiscarding()
968 bool result = true;
969 if (isAnyModified(*Tags))
970 result = Actions::askYesNoQuestion("There are pending changes, are you sure?", Status::trace);
971 return result;
974 void TagEditor::LocateSong(const MPD::Song &s)
976 if (myScreen == this)
977 return;
979 if (s.getDirectory().empty())
980 return;
982 if (Global::myScreen != this)
983 switchTo();
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);
992 else
993 itsBrowsedDir = "/";
994 if (itsBrowsedDir.empty())
995 itsBrowsedDir = "/";
996 Dirs->clear();
997 update();
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)
1008 Dirs->highlight(i);
1009 break;
1012 // refresh window so we can be highlighted item
1013 Dirs->refresh();
1015 Tags->clear();
1016 update();
1018 // reset TagTypes since it can be under Filename
1019 // and then songs in right column are not visible.
1020 TagTypes->reset();
1021 // go to the right column
1022 nextColumn();
1023 nextColumn();
1025 // highlight our file
1026 for (size_t i = 0; i < Tags->size(); ++i)
1028 if ((*Tags)[i].value() == s)
1030 Tags->highlight(i);
1031 break;
1036 namespace {//
1038 bool isAnyModified(const NC::Menu<MPD::MutableSong> &m)
1040 for (auto it = m.beginV(); it != m.endV(); ++it)
1041 if (it->isModified())
1042 return true;
1043 return false;
1046 std::string CapitalizeFirstLetters(const std::string &s)
1048 std::wstring ws = ToWString(s);
1049 wchar_t prev = 0;
1050 for (auto it = ws.begin(); it != ws.end(); ++it)
1052 if (!iswalpha(prev) && prev != L'\'')
1053 *it = towupper(*it);
1054 prev = *it;
1056 return ToString(ws);
1059 void CapitalizeFirstLetters(MPD::MutableSong &s)
1061 for (const SongInfo::Metadata *m = SongInfo::Tags; m->Name; ++m)
1063 unsigned i = 0;
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)
1073 unsigned i = 0;
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())
1086 std::string line;
1087 while (std::getline(input, line))
1088 if (!line.empty())
1089 Patterns.push_back(line);
1090 input.close();
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;
1103 output.close();
1106 MPD::MutableSong::SetFunction IntoSetFunction(char c)
1108 switch (c)
1110 case 'a':
1111 return &MPD::MutableSong::setArtist;
1112 case 'A':
1113 return &MPD::MutableSong::setAlbumArtist;
1114 case 't':
1115 return &MPD::MutableSong::setTitle;
1116 case 'b':
1117 return &MPD::MutableSong::setAlbum;
1118 case 'y':
1119 return &MPD::MutableSong::setDate;
1120 case 'n':
1121 return &MPD::MutableSong::setTrack;
1122 case 'g':
1123 return &MPD::MutableSong::setGenre;
1124 case 'c':
1125 return &MPD::MutableSong::setComposer;
1126 case 'p':
1127 return &MPD::MutableSong::setPerformer;
1128 case 'd':
1129 return &MPD::MutableSong::setDisc;
1130 case 'C':
1131 return &MPD::MutableSong::setComment;
1132 default:
1133 return 0;
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);
1141 return result;
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);
1155 i = mask.find("%");
1156 if (!mask.empty())
1157 separators.push_back(mask.substr(0, i));
1159 size_t i = 0;
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())
1165 goto PARSE_FAILED;
1166 file = file.substr(j+it->length());
1168 if (!file.empty())
1170 if (i >= tags.size())
1171 goto PARSE_FAILED;
1172 tags.at(i).second = file;
1175 if (0) // tss...
1177 PARSE_FAILED:
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)
1184 if (*j == '_')
1185 *j = ' ';
1187 if (!preview)
1189 MPD::MutableSong::SetFunction set = IntoSetFunction(it->first);
1190 if (set)
1191 s.setTags(set, it->second, Config.tags_separator);
1193 else
1194 result << "%" << it->first << ": " << it->second << "\n";
1196 return result.str();
1199 std::string SongToString(const MPD::MutableSong &s)
1201 std::string result;
1202 size_t i = myTagEditor->TagTypes->choice();
1203 if (i < 11)
1204 result = (s.*SongInfo::Tags[i].Get)(0);
1205 else if (i == 12)
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 == "..")
1213 return filter;
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);
1224 #endif