From a6fef97cac65936e2042a62200dd1bd859009465 Mon Sep 17 00:00:00 2001 From: Lukas Prediger Date: Wed, 18 Aug 2021 19:41:46 +0300 Subject: [PATCH] Separating out filename formatting options into separate widget. First step towards unifying filename formatting over different dialogs. --- src/CMakeLists.txt | 3 + src/core/organiseformat.cpp | 22 ++++ src/core/organiseformat.h | 2 + src/ui/organisedialog.cpp | 123 ++++---------------- src/ui/organisedialog.h | 4 - src/ui/organisedialog.ui | 113 +++++------------- src/widgets/filenameformatwidget.cpp | 166 +++++++++++++++++++++++++++ src/widgets/filenameformatwidget.h | 59 ++++++++++ src/widgets/filenameformatwidget.ui | 107 +++++++++++++++++ 9 files changed, 412 insertions(+), 187 deletions(-) create mode 100644 src/widgets/filenameformatwidget.cpp create mode 100644 src/widgets/filenameformatwidget.h create mode 100644 src/widgets/filenameformatwidget.ui diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 88d4165ed..5e1518e1d 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -385,6 +385,7 @@ set(SOURCES widgets/errordialog.cpp widgets/fancytabwidget.cpp widgets/favoritewidget.cpp + widgets/filenameformatwidget.cpp widgets/fileview.cpp widgets/fileviewlist.cpp widgets/forcescrollperpixel.cpp @@ -683,6 +684,7 @@ set(HEADERS widgets/errordialog.h widgets/fancytabwidget.h widgets/favoritewidget.h + widgets/filenameformatwidget.h widgets/fileview.h widgets/fileviewlist.h widgets/freespacebar.h @@ -807,6 +809,7 @@ set(UI widgets/equalizerslider.ui widgets/errordialog.ui + widgets/filenameformatwidget.ui widgets/fileview.ui widgets/loginstatewidget.ui widgets/osdpretty.ui diff --git a/src/core/organiseformat.cpp b/src/core/organiseformat.cpp index 844e64400..7fa83ec0a 100644 --- a/src/core/organiseformat.cpp +++ b/src/core/organiseformat.cpp @@ -24,6 +24,7 @@ #include #include +#include #include #include @@ -143,6 +144,27 @@ QString OrganiseFormat::GetFilenameForSong(const Song& song) const { return parts.join("/"); } +QStringList OrganiseFormat::GetFilenamesForSongs(const SongList& songs) const { + // Check if we will have multiple files with the same name. + // If so, they will erase each other if the overwrite flag is set. + // Better to rename them: e.g. foo.bar -> foo(2).bar + QHash filenames; + QStringList new_filenames; + + for (const Song& song : songs) { + QString new_filename = GetFilenameForSong(song); + if (filenames.contains(new_filename)) { + QString song_number = QString::number(++filenames[new_filename]); + new_filename = Utilities::PathWithoutFilenameExtension(new_filename) + + "(" + song_number + ")." + + QFileInfo(new_filename).suffix(); + } + filenames.insert(new_filename, 1); + new_filenames << new_filename; + } + return new_filenames; +} + QString OrganiseFormat::ParseBlock(QString block, const Song& song, bool* any_empty) const { QRegExp tag_regexp(kTagPattern); diff --git a/src/core/organiseformat.h b/src/core/organiseformat.h index d65ce79d0..3cad35f0b 100644 --- a/src/core/organiseformat.h +++ b/src/core/organiseformat.h @@ -20,6 +20,7 @@ #ifndef CORE_ORGANISEFORMAT_H_ #define CORE_ORGANISEFORMAT_H_ +#include #include #include #include @@ -55,6 +56,7 @@ class OrganiseFormat { bool IsValid() const; QString GetFilenameForSong(const Song& song) const; + QStringList GetFilenamesForSongs(const SongList& songs) const; class Validator : public QValidator { public: diff --git a/src/ui/organisedialog.cpp b/src/ui/organisedialog.cpp index 1d8a9040c..12b9fe2f2 100644 --- a/src/ui/organisedialog.cpp +++ b/src/ui/organisedialog.cpp @@ -57,52 +57,11 @@ OrganiseDialog::OrganiseDialog(TaskManager* task_manager, ui_->aftercopying->setItemIcon( 1, IconLoader::Load("edit-delete", IconLoader::Base)); - // Valid tags - QMap tags; - tags[tr("Title")] = "title"; - tags[tr("Album")] = "album"; - tags[tr("Artist")] = "artist"; - tags[tr("Artist's initial")] = "artistinitial"; - tags[tr("Album artist")] = "albumartist"; - tags[tr("Composer")] = "composer"; - tags[tr("Performer")] = "performer"; - tags[tr("Grouping")] = "grouping"; - tags[tr("Lyrics")] = "lyrics"; - tags[tr("Track")] = "track"; - tags[tr("Disc")] = "disc"; - tags[tr("BPM")] = "bpm"; - tags[tr("Year")] = "year"; - tags[tr("Original year")] = "originalyear"; - tags[tr("Genre")] = "genre"; - tags[tr("Comment")] = "comment"; - tags[tr("Length")] = "length"; - tags[tr("Bitrate", "Refers to bitrate in file organise dialog.")] = "bitrate"; - tags[tr("Samplerate")] = "samplerate"; - tags[tr("File extension")] = "extension"; - - // Naming scheme input field - new OrganiseFormat::SyntaxHighlighter(ui_->naming); - connect(ui_->destination, SIGNAL(currentIndexChanged(int)), SLOT(UpdatePreviews())); - connect(ui_->naming, SIGNAL(textChanged()), SLOT(UpdatePreviews())); - connect(ui_->replace_ascii, SIGNAL(toggled(bool)), SLOT(UpdatePreviews())); - connect(ui_->replace_the, SIGNAL(toggled(bool)), SLOT(UpdatePreviews())); - connect(ui_->replace_spaces, SIGNAL(toggled(bool)), SLOT(UpdatePreviews())); - - // Get the titles of the tags to put in the insert menu - QStringList tag_titles = tags.keys(); - std::stable_sort(tag_titles.begin(), tag_titles.end()); - - // Build the insert menu - QMenu* tag_menu = new QMenu(this); - for (const QString& title : tag_titles) { - QAction* action = tag_menu->addAction(title); - QString tag = tags[title]; - connect(action, &QAction::triggered, [this, tag]() { InsertTag(tag); }); - } - - ui_->insert->setMenu(tag_menu); + connect(ui_->naming_group, SIGNAL(OptionChanged()), SLOT(UpdatePreviews())); + connect(ui_->naming_group, SIGNAL(FormatStringChanged()), + SLOT(UpdatePreviews())); } OrganiseDialog::~OrganiseDialog() { delete ui_; } @@ -209,28 +168,14 @@ void OrganiseDialog::SetCopy(bool copy) { ui_->aftercopying->setCurrentIndex(copy ? 0 : 1); } -void OrganiseDialog::InsertTag(const QString& tag) { - ui_->naming->insertPlainText("%" + tag); -} - Organise::NewSongInfoList OrganiseDialog::ComputeNewSongsFilenames( const SongList& songs, const OrganiseFormat& format) { - // Check if we will have multiple files with the same name. - // If so, they will erase each other if the overwrite flag is set. - // Better to rename them: e.g. foo.bar -> foo(2).bar - QHash filenames; - Organise::NewSongInfoList new_songs_info; + QStringList new_filenames = format.GetFilenamesForSongs(songs); + Q_ASSERT(new_filenames.length() == songs.length()); - for (const Song& song : songs) { - QString new_filename = format.GetFilenameForSong(song); - if (filenames.contains(new_filename)) { - QString song_number = QString::number(++filenames[new_filename]); - new_filename = Utilities::PathWithoutFilenameExtension(new_filename) + - "(" + song_number + ")." + - QFileInfo(new_filename).suffix(); - } - filenames.insert(new_filename, 1); - new_songs_info << Organise::NewSongInfo(song, new_filename); + Organise::NewSongInfoList new_songs_info; + for (int i = 0; i < new_filenames.length(); ++i) { + new_songs_info << Organise::NewSongInfo(songs[i], new_filenames[i]); } return new_songs_info; } @@ -265,11 +210,8 @@ void OrganiseDialog::UpdatePreviews() { ui_->free_space->set_total_bytes(capacity); } - // Update the format object - format_.set_format(ui_->naming->toPlainText()); - format_.set_replace_non_ascii(ui_->replace_ascii->isChecked()); - format_.set_replace_spaces(ui_->replace_spaces->isChecked()); - format_.set_replace_the(ui_->replace_the->isChecked()); + // Get updated format object + OrganiseFormat format = ui_->naming_group->format(); // If this is set to Transcode_Always, then the user has selected transcode, // so we can be fairly certain that the device supports the selected format. @@ -278,14 +220,14 @@ void OrganiseDialog::UpdatePreviews() { // the preview will be incorrect. if (storage && storage->GetTranscodeMode() == MusicStorage::Transcode_Always) { - const Song::FileType format = storage->GetTranscodeFormat(); - TranscoderPreset preset = Transcoder::PresetForFileType(format); - format_.add_tag_override("extension", preset.extension_); + const Song::FileType file_format = storage->GetTranscodeFormat(); + TranscoderPreset preset = Transcoder::PresetForFileType(file_format); + format.add_tag_override("extension", preset.extension_); } else { - format_.reset_tag_overrides(); + format.reset_tag_overrides(); } - const bool format_valid = !has_local_destination || format_.IsValid(); + const bool format_valid = !has_local_destination || format.IsValid(); // Are we gonna enable the ok button? bool ok = format_valid && !songs_.isEmpty(); @@ -294,7 +236,7 @@ void OrganiseDialog::UpdatePreviews() { ui_->button_box->button(QDialogButtonBox::Ok)->setEnabled(ok); if (!format_valid) return; - new_songs_info_ = ComputeNewSongsFilenames(songs_, format_); + new_songs_info_ = ComputeNewSongsFilenames(songs_, format); // Update the previews ui_->preview->clear(); @@ -325,12 +267,7 @@ void OrganiseDialog::DestDataChanged(const QModelIndex& begin, QSize OrganiseDialog::sizeHint() const { return QSize(650, 0); } void OrganiseDialog::Reset() { - ui_->naming->setPlainText(kDefaultFormat); - ui_->replace_ascii->setChecked(false); - ui_->replace_spaces->setChecked(false); - ui_->replace_the->setChecked(false); - ui_->overwrite->setChecked(false); - ui_->mark_as_listened->setChecked(false); + ui_->naming_group->Reset(); ui_->eject_after->setChecked(false); } @@ -339,13 +276,6 @@ void OrganiseDialog::showEvent(QShowEvent*) { QSettings s; s.beginGroup(kSettingsGroup); - ui_->naming->setPlainText(s.value("format", kDefaultFormat).toString()); - ui_->replace_ascii->setChecked(s.value("replace_ascii", false).toBool()); - ui_->replace_spaces->setChecked(s.value("replace_spaces", false).toBool()); - ui_->replace_the->setChecked(s.value("replace_the", false).toBool()); - ui_->overwrite->setChecked(s.value("overwrite", false).toBool()); - ui_->mark_as_listened->setChecked( - s.value("mark_as_listened", false).toBool()); ui_->eject_after->setChecked(s.value("eject_after", false).toBool()); QString destination = s.value("destination").toString(); @@ -358,14 +288,9 @@ void OrganiseDialog::showEvent(QShowEvent*) { void OrganiseDialog::accept() { QSettings s; s.beginGroup(kSettingsGroup); - s.setValue("format", ui_->naming->toPlainText()); - s.setValue("replace_ascii", ui_->replace_ascii->isChecked()); - s.setValue("replace_spaces", ui_->replace_spaces->isChecked()); - s.setValue("replace_the", ui_->replace_the->isChecked()); - s.setValue("overwrite", ui_->overwrite->isChecked()); - s.setValue("mark_as_listened", ui_->overwrite->isChecked()); s.setValue("destination", ui_->destination->currentText()); s.setValue("eject_after", ui_->eject_after->isChecked()); + ui_->naming_group->StoreSettings(); const QModelIndex destination = ui_->destination->model()->index(ui_->destination->currentIndex(), 0); @@ -377,14 +302,16 @@ void OrganiseDialog::accept() { // Reset the extension override if we set it. Organise should correctly set // the Song object. - format_.reset_tag_overrides(); + OrganiseFormat format = ui_->naming_group->format(); + format.reset_tag_overrides(); // It deletes itself when it's finished. const bool copy = ui_->aftercopying->currentIndex() == 0; - Organise* organise = new Organise( - task_manager_, storage, format_, copy, ui_->overwrite->isChecked(), - ui_->mark_as_listened->isChecked(), new_songs_info_, - ui_->eject_after->isChecked()); + Organise* organise = + new Organise(task_manager_, storage, format, copy, + ui_->naming_group->overwrite_existing(), + ui_->naming_group->mark_as_listened(), new_songs_info_, + ui_->eject_after->isChecked()); connect(organise, SIGNAL(Finished(QStringList)), SLOT(OrganiseFinished(QStringList))); connect(organise, SIGNAL(FileCopied(int)), this, SIGNAL(FileCopied(int))); diff --git a/src/ui/organisedialog.h b/src/ui/organisedialog.h index aae147c1a..0d115d3f1 100644 --- a/src/ui/organisedialog.h +++ b/src/ui/organisedialog.h @@ -25,7 +25,6 @@ #include #include "core/organise.h" -#include "core/organiseformat.h" #include "core/song.h" #include "gtest/gtest_prod.h" #include "library/librarybackend.h" @@ -75,7 +74,6 @@ class OrganiseDialog : public QDialog { private slots: void Reset(); - void InsertTag(const QString& tag); void UpdatePreviews(); void DestDataChanged(const QModelIndex& begin, const QModelIndex& end); @@ -95,8 +93,6 @@ class OrganiseDialog : public QDialog { QMetaObject::Connection model_connection_; - OrganiseFormat format_; - QFuture songs_future_; SongList songs_; Organise::NewSongInfoList new_songs_info_; diff --git a/src/ui/organisedialog.ui b/src/ui/organisedialog.ui index 510f1af23..af1dd325a 100644 --- a/src/ui/organisedialog.ui +++ b/src/ui/organisedialog.ui @@ -7,7 +7,7 @@ 0 0 588 - 525 + 596 @@ -64,77 +64,7 @@ - - - Naming options - - - - - - - - <p>Tokens start with %, for example: %artist %album %title </p> - -<p>If you surround sections of text that contain a token with curly-braces, that section will be hidden if the token is empty.</p> - - - QTextEdit::NoWrap - - - false - - - - - - - Insert... - - - QToolButton::InstantPopup - - - - - - - - - Ignore "The" in artist names - - - - - - - Replaces spaces with underscores - - - - - - - Restrict to ASCII characters - - - - - - - Overwrite existing files - - - - - - - Mark as listened - - - - - + @@ -152,7 +82,16 @@ 0 - + + 0 + + + 0 + + + 0 + + 0 @@ -165,7 +104,16 @@ 0 - + + 0 + + + 0 + + + 0 + + 0 @@ -227,28 +175,23 @@
widgets/freespacebar.h
1 - - LineTextEdit - QTextEdit -
widgets/linetextedit.h
-
BusyIndicator QWidget
widgets/busyindicator.h
1
+ + FileNameFormatWidget + QWidget +
widgets/filenameformatwidget.h
+ 1 +
destination aftercopying eject_after - naming - insert - replace_the - replace_spaces - replace_ascii - overwrite button_box diff --git a/src/widgets/filenameformatwidget.cpp b/src/widgets/filenameformatwidget.cpp new file mode 100644 index 000000000..0c1daf29f --- /dev/null +++ b/src/widgets/filenameformatwidget.cpp @@ -0,0 +1,166 @@ +/* This file is part of Clementine. + Copyright 2010, David Sansome + Copyright 2021, Lukas Prediger + + Clementine is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Clementine is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Clementine. If not, see . +*/ + +#include "filenameformatwidget.h" + +#include +#include +#include +#include +#include +#include + +#include "core/organise.h" +#include "ui/organisedialog.h" +#include "ui_filenameformatwidget.h" + +const char* FileNameFormatWidget::kDefaultFormat = + "%artist/%album{ (Disc %disc)}/{%track - }%title.%extension"; +const char* FileNameFormatWidget::kSettingsGroup = "FileNameFormatWidget"; + +FileNameFormatWidget::FileNameFormatWidget(QWidget* parent) + : QWidget(parent), ui_(new Ui_FileNameFormatWidget) { + setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); + ui_->setupUi(this); + + // Syntax highlighting for naming scheme input field. + // attaches as child to ui_->naming, which transfers ownership + new OrganiseFormat::SyntaxHighlighter(ui_->naming); + + // Valid tags + QMap tags; + tags[tr("Title")] = "title"; + tags[tr("Album")] = "album"; + tags[tr("Artist")] = "artist"; + tags[tr("Artist's initial")] = "artistinitial"; + tags[tr("Album artist")] = "albumartist"; + tags[tr("Composer")] = "composer"; + tags[tr("Performer")] = "performer"; + tags[tr("Grouping")] = "grouping"; + tags[tr("Lyrics")] = "lyrics"; + tags[tr("Track")] = "track"; + tags[tr("Disc")] = "disc"; + tags[tr("BPM")] = "bpm"; + tags[tr("Year")] = "year"; + tags[tr("Original year")] = "originalyear"; + tags[tr("Genre")] = "genre"; + tags[tr("Comment")] = "comment"; + tags[tr("Length")] = "length"; + tags[tr("Bitrate", "Refers to bitrate in file organise dialog.")] = "bitrate"; + tags[tr("Samplerate")] = "samplerate"; + tags[tr("File extension")] = "extension"; + + // Get the titles of the tags to put in the insert menu + QStringList tag_titles = tags.keys(); + std::stable_sort(tag_titles.begin(), tag_titles.end()); + + // Build the insert menu + QMenu* tag_menu = new QMenu(this); + for (const QString& title : tag_titles) { + QAction* action = tag_menu->addAction(title); + QString tag = tags[title]; + connect(action, &QAction::triggered, [this, tag]() { InsertTag(tag); }); + } + + ui_->insert->setMenu(tag_menu); + + connect(ui_->naming, SIGNAL(textChanged()), SIGNAL(FormatStringChanged())); + connect(ui_->replace_ascii, SIGNAL(toggled(bool)), SIGNAL(OptionChanged())); + connect(ui_->replace_spaces, SIGNAL(toggled(bool)), SIGNAL(OptionChanged())); + connect(ui_->replace_the, SIGNAL(toggled(bool)), SIGNAL(OptionChanged())); + connect(ui_->overwrite, SIGNAL(toggled(bool)), SIGNAL(OptionChanged())); + connect(ui_->mark_as_listened, SIGNAL(toggled(bool)), + SIGNAL(OptionChanged())); + + LoadSettings(); +} + +void FileNameFormatWidget::Reset() { + ui_->naming->setPlainText(kDefaultFormat); + ui_->replace_ascii->setChecked(false); + ui_->replace_spaces->setChecked(false); + ui_->replace_the->setChecked(false); + ui_->overwrite->setChecked(false); + ui_->mark_as_listened->setChecked(false); +} + +bool FileNameFormatWidget::ignore_the() const { + return ui_->replace_the->isChecked(); +} + +bool FileNameFormatWidget::replace_spaces() const { + return ui_->replace_spaces->isChecked(); +} + +bool FileNameFormatWidget::restrict_to_ascii() const { + return ui_->replace_ascii->isChecked(); +} + +bool FileNameFormatWidget::overwrite_existing() const { + return ui_->overwrite->isChecked(); +} + +bool FileNameFormatWidget::mark_as_listened() const { + return ui_->mark_as_listened->isChecked(); +} + +OrganiseFormat FileNameFormatWidget::format() const { + OrganiseFormat format; + format.set_format(ui_->naming->toPlainText()); + format.set_replace_non_ascii(ui_->replace_ascii->isChecked()); + format.set_replace_spaces(ui_->replace_spaces->isChecked()); + format.set_replace_the(ui_->replace_the->isChecked()); + return format; +} + +void FileNameFormatWidget::InsertTag(const QString& tag) { + ui_->naming->insertPlainText("%" + tag); +} + +void FileNameFormatWidget::LoadSettings() { + QSettings s; + + // transitional fallback: if the new kSettingsGroup for FileNameFormatWidget + // is not present, try loading from OrganiseDialog::kSettingsGroup, where + // these settings where previously held. + if (!s.childGroups().contains(kSettingsGroup) && + s.childGroups().contains(OrganiseDialog::kSettingsGroup)) { + s.beginGroup(OrganiseDialog::kSettingsGroup); + } else { + s.beginGroup(kSettingsGroup); + } + + ui_->naming->setPlainText(s.value("format", kDefaultFormat).toString()); + ui_->replace_ascii->setChecked(s.value("replace_ascii", false).toBool()); + ui_->replace_spaces->setChecked(s.value("replace_spaces", false).toBool()); + ui_->replace_the->setChecked(s.value("replace_the", false).toBool()); + ui_->overwrite->setChecked(s.value("overwrite", false).toBool()); + ui_->mark_as_listened->setChecked( + s.value("mark_as_listened", false).toBool()); +} + +void FileNameFormatWidget::StoreSettings() { + QSettings s; + s.beginGroup(kSettingsGroup); + s.setValue("format", ui_->naming->toPlainText()); + s.setValue("replace_ascii", ui_->replace_ascii->isChecked()); + s.setValue("replace_spaces", ui_->replace_spaces->isChecked()); + s.setValue("replace_the", ui_->replace_the->isChecked()); + s.setValue("overwrite", ui_->overwrite->isChecked()); + s.setValue("mark_as_listened", ui_->overwrite->isChecked()); +} diff --git a/src/widgets/filenameformatwidget.h b/src/widgets/filenameformatwidget.h new file mode 100644 index 000000000..9d265353e --- /dev/null +++ b/src/widgets/filenameformatwidget.h @@ -0,0 +1,59 @@ +/* This file is part of Clementine. + Copyright 2010, David Sansome + Copyright 2021, Lukas Prediger + + Clementine is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Clementine is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Clementine. If not, see . +*/ + +#ifndef FILENAMEFORMATWIDGET_H +#define FILENAMEFORMATWIDGET_H + +#include +#include + +#include "core/organiseformat.h" +#include "ui_filenameformatwidget.h" + +class FileNameFormatWidget : public QWidget { + Q_OBJECT + public: + static const char* kDefaultFormat; + static const char* kSettingsGroup; + + signals: + void OptionChanged(); + void FormatStringChanged(); + + private slots: + void InsertTag(const QString& tag); + + public: + FileNameFormatWidget(QWidget* parent); + void Reset(); + void StoreSettings(); + + bool ignore_the() const; + bool replace_spaces() const; + bool restrict_to_ascii() const; + bool overwrite_existing() const; + bool mark_as_listened() const; + OrganiseFormat format() const; + + private: + void LoadSettings(); + + std::unique_ptr ui_; +}; + +#endif // FILENAMEFORMATWIDGET_H diff --git a/src/widgets/filenameformatwidget.ui b/src/widgets/filenameformatwidget.ui new file mode 100644 index 000000000..5f24d0c06 --- /dev/null +++ b/src/widgets/filenameformatwidget.ui @@ -0,0 +1,107 @@ + + + FileNameFormatWidget + + + + 0 + 0 + 361 + 283 + + + + + + + Naming options + + + + + + + + <p>Tokens start with %, for example: %artist %album %title </p> + +<p>If you surround sections of text that contain a token with curly-braces, that section will be hidden if the token is empty.</p> + + + QTextEdit::NoWrap + + + false + + + + + + + Insert... + + + QToolButton::InstantPopup + + + + + + + + + Ignore "The" in artist names + + + + + + + Replaces spaces with underscores + + + + + + + Restrict to ASCII characters + + + + + + + Overwrite existing files + + + + + + + Mark as listened + + + + + + + + + + + LineTextEdit + QTextEdit +
widgets/linetextedit.h
+
+
+ + naming + insert + replace_the + replace_spaces + replace_ascii + overwrite + mark_as_listened + + + +