├── main.cpp ├── songlist.h ├── .gitignore ├── CMakeLists.txt ├── songlist.ui └── songlist.cpp /main.cpp: -------------------------------------------------------------------------------- 1 | #include "songlist.h" 2 | 3 | #include 4 | 5 | int main(int argc, char *argv[]) 6 | { 7 | QApplication a(argc, argv); 8 | 9 | SongList w; 10 | w.show(); 11 | 12 | return a.exec(); 13 | } 14 | -------------------------------------------------------------------------------- /songlist.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | class SongList : public QMainWindow 7 | { 8 | Q_OBJECT 9 | 10 | public: 11 | SongList(QWidget *parent = nullptr); 12 | ~SongList(); 13 | 14 | public slots: 15 | void OnChangeButtonClicked(); 16 | void OnSearchBoxTextChanged(const QString& text); 17 | void OnSongListTableCellDoubleClicked(const int row, const int column); 18 | 19 | private: 20 | struct Impl; 21 | std::unique_ptr m_impl; 22 | }; 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # This file is used to ignore files which are generated 2 | # ---------------------------------------------------------------------------- 3 | 4 | *~ 5 | *.autosave 6 | *.a 7 | *.core 8 | *.moc 9 | *.o 10 | *.obj 11 | *.orig 12 | *.rej 13 | *.so 14 | *.so.* 15 | *_pch.h.cpp 16 | *_resource.rc 17 | *.qm 18 | .#* 19 | *.*# 20 | core 21 | !core/ 22 | tags 23 | .DS_Store 24 | .directory 25 | *.debug 26 | Makefile* 27 | *.prl 28 | *.app 29 | moc_*.cpp 30 | ui_*.h 31 | qrc_*.cpp 32 | Thumbs.db 33 | *.res 34 | *.rc 35 | /.qmake.cache 36 | /.qmake.stash 37 | 38 | # qtcreator generated files 39 | *.pro.user* 40 | *.qbs.user* 41 | CMakeLists.txt.user* 42 | 43 | # xemacs temporary files 44 | *.flc 45 | 46 | # Vim temporary files 47 | .*.swp 48 | 49 | # Visual Studio generated files 50 | *.ib_pdb_index 51 | *.idb 52 | *.ilk 53 | *.pdb 54 | *.sln 55 | *.suo 56 | *.vcproj 57 | *vcproj.*.*.user 58 | *.ncb 59 | *.sdf 60 | *.opensdf 61 | *.vcxproj 62 | *vcxproj.* 63 | 64 | # MinGW generated files 65 | *.Debug 66 | *.Release 67 | 68 | # Python byte code 69 | *.pyc 70 | 71 | # Binaries 72 | # -------- 73 | *.dll 74 | *.exe 75 | 76 | # Directories with generated files 77 | .moc/ 78 | .obj/ 79 | .pch/ 80 | .rcc/ 81 | .uic/ 82 | /build*/ 83 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.16) 2 | 3 | project(SongList VERSION 0.1 LANGUAGES CXX) 4 | 5 | set(CMAKE_AUTOUIC ON) 6 | set(CMAKE_AUTOMOC ON) 7 | set(CMAKE_AUTORCC ON) 8 | 9 | set(CMAKE_CXX_STANDARD 17) 10 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 11 | 12 | find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets) 13 | find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets) 14 | 15 | set(PROJECT_SOURCES 16 | main.cpp 17 | songlist.cpp 18 | songlist.h 19 | songlist.ui 20 | ) 21 | 22 | if(${QT_VERSION_MAJOR} GREATER_EQUAL 6) 23 | qt_add_executable(SongList 24 | MANUAL_FINALIZATION 25 | ${PROJECT_SOURCES} 26 | ) 27 | # Define target properties for Android with Qt 6 as: 28 | # set_property(TARGET SongList APPEND PROPERTY QT_ANDROID_PACKAGE_SOURCE_DIR 29 | # ${CMAKE_CURRENT_SOURCE_DIR}/android) 30 | # For more information, see https://doc.qt.io/qt-6/qt-add-executable.html#target-creation 31 | else() 32 | if(ANDROID) 33 | add_library(SongList SHARED 34 | ${PROJECT_SOURCES} 35 | ) 36 | # Define properties for Android with Qt 5 after find_package() calls as: 37 | # set(ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/android") 38 | else() 39 | add_executable(SongList 40 | ${PROJECT_SOURCES} 41 | ) 42 | endif() 43 | endif() 44 | 45 | target_link_libraries(SongList PRIVATE Qt${QT_VERSION_MAJOR}::Widgets) 46 | 47 | # Qt for iOS sets MACOSX_BUNDLE_GUI_IDENTIFIER automatically since Qt 6.1. 48 | # If you are developing for iOS or macOS you should consider setting an 49 | # explicit, fixed bundle identifier manually though. 50 | if(${QT_VERSION} VERSION_LESS 6.1.0) 51 | set(BUNDLE_ID_OPTION MACOSX_BUNDLE_GUI_IDENTIFIER com.example.SongList) 52 | endif() 53 | set_target_properties(SongList PROPERTIES 54 | ${BUNDLE_ID_OPTION} 55 | MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION} 56 | MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR} 57 | MACOSX_BUNDLE TRUE 58 | WIN32_EXECUTABLE TRUE 59 | ) 60 | 61 | include(GNUInstallDirs) 62 | install(TARGETS SongList 63 | BUNDLE DESTINATION . 64 | LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} 65 | RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} 66 | ) 67 | 68 | if(QT_VERSION_MAJOR EQUAL 6) 69 | qt_finalize_executable(SongList) 70 | endif() 71 | -------------------------------------------------------------------------------- /songlist.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 500 10 | 475 11 | 12 | 13 | 14 | Song List 15 | 16 | 17 | 18 | true 19 | 20 | 21 | 22 | 23 | 24 | QFrame::Shape::NoFrame 25 | 26 | 27 | QFrame::Shadow::Plain 28 | 29 | 30 | QAbstractScrollArea::SizeAdjustPolicy::AdjustToContents 31 | 32 | 33 | true 34 | 35 | 36 | QAbstractItemView::EditTrigger::NoEditTriggers 37 | 38 | 39 | false 40 | 41 | 42 | false 43 | 44 | 45 | false 46 | 47 | 48 | false 49 | 50 | 51 | QAbstractItemView::SelectionMode::SingleSelection 52 | 53 | 54 | QAbstractItemView::SelectionBehavior::SelectRows 55 | 56 | 57 | QAbstractItemView::ScrollMode::ScrollPerItem 58 | 59 | 60 | QAbstractItemView::ScrollMode::ScrollPerItem 61 | 62 | 63 | false 64 | 65 | 66 | Qt::PenStyle::NoPen 67 | 68 | 69 | true 70 | 71 | 72 | false 73 | 74 | 75 | 0 76 | 77 | 78 | false 79 | 80 | 81 | false 82 | 83 | 84 | false 85 | 86 | 87 | 88 | 89 | 90 | 91 | Search... ("Artist", "Song", or "Artist - Song") 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 0 100 | 0 101 | 102 | 103 | 104 | Select location... 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 0 113 | 0 114 | 115 | 116 | 117 | Location: 118 | 119 | 120 | 121 | 122 | 123 | 124 | Change 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /songlist.cpp: -------------------------------------------------------------------------------- 1 | #include "songlist.h" 2 | #include "./ui_songlist.h" 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | const static auto APP_DATA = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); 18 | 19 | struct SongList::Impl 20 | { 21 | Impl(SongList* self) 22 | : m_self(self) 23 | , m_ui(std::make_unique()) 24 | , m_location("C:/") 25 | { 26 | m_ui->setupUi(m_self); 27 | 28 | // Setup song list table headers 29 | m_ui->songListTable->setColumnCount(3); 30 | m_ui->songListTable->setHorizontalHeaderLabels({"Project", "Artist", "Song"}); 31 | 32 | auto header = m_ui->songListTable->horizontalHeader(); 33 | header->setSectionResizeMode(1, QHeaderView::ResizeToContents); 34 | header->setSectionResizeMode(2, QHeaderView::Stretch); 35 | 36 | // Hide the "Project" column 37 | m_ui->songListTable->setColumnHidden(0, true); 38 | 39 | // Initialize data 40 | this->InitializeLocation(); 41 | 42 | // Setup slot connections 43 | QObject::connect(m_ui->changeButton, &QPushButton::clicked, m_self, &SongList::OnChangeButtonClicked); 44 | QObject::connect(m_ui->searchBox, &QLineEdit::textChanged, m_self, &SongList::OnSearchBoxTextChanged); 45 | QObject::connect(m_ui->songListTable, &QTableWidget::cellDoubleClicked, m_self, &SongList::OnSongListTableCellDoubleClicked); 46 | } 47 | 48 | void OnSearchBoxTextChanged(const QString& text) 49 | { 50 | for (int row = 0; row < m_ui->songListTable->rowCount(); ++row) 51 | { 52 | bool hidden = true; 53 | 54 | const auto item = m_ui->songListTable->item(row, 0); 55 | 56 | if (item && item->text().contains(text, Qt::CaseInsensitive)) 57 | { 58 | hidden = false; 59 | } 60 | 61 | m_ui->songListTable->setRowHidden(row, hidden); 62 | } 63 | } 64 | 65 | void OnChangeButtonClicked() 66 | { 67 | QFileDialog fileDialog(m_self); 68 | fileDialog.setFileMode(QFileDialog::Directory); 69 | 70 | fileDialog.setDirectory(m_location.absolutePath()); 71 | 72 | if (fileDialog.exec()) 73 | { 74 | const auto newLocation = fileDialog.selectedFiles()[0]; 75 | this->UpdateLocation(newLocation); 76 | } 77 | } 78 | 79 | void OnSongListTableCellDoubleClicked(const int row, const int column) 80 | { 81 | // Open the REAPER project and if there is an associated guitar pro file open that too 82 | const auto project = m_ui->songListTable->item(row, 0)->text(); 83 | QDir projectDir(m_location.absolutePath() + "/" + project); 84 | 85 | const auto files = projectDir.entryList(QDir::Files); 86 | for (const auto file : files) 87 | { 88 | if (file.endsWith(".rpp") || file.endsWith(".gp")) 89 | { 90 | const auto absoluteFilePath = projectDir.absolutePath() + "/" + file; 91 | QDesktopServices::openUrl(QUrl::fromLocalFile(absoluteFilePath)); 92 | } 93 | } 94 | } 95 | 96 | private: 97 | void InitializeLocation() 98 | { 99 | QFile locationFile(APP_DATA + "/location.dat"); 100 | 101 | if (locationFile.open(QIODevice::ReadOnly)) 102 | { 103 | QTextStream inputStream(&locationFile); 104 | const auto location = inputStream.readLine(); 105 | locationFile.close(); 106 | 107 | this->UpdateLocation(location, true); 108 | } 109 | 110 | else 111 | { 112 | m_ui->locationValueLabel->setText("Select Location..."); 113 | } 114 | } 115 | 116 | void UpdateLocation(const QDir& location, bool init=false) 117 | { 118 | if (location.exists()) 119 | { 120 | // Save the location to a file to be recalled on next startup 121 | if (!init) 122 | { 123 | QFile locationFile(APP_DATA + "/location.dat"); 124 | if (locationFile.open(QIODevice::WriteOnly)) 125 | { 126 | QTextStream outputStream(&locationFile); 127 | outputStream << location.absolutePath(); 128 | locationFile.close(); 129 | } 130 | 131 | else 132 | { 133 | QMessageBox::warning(m_self, "Warning", "Failed to save location to AppData"); 134 | } 135 | } 136 | 137 | m_location = location; 138 | m_ui->locationValueLabel->setText(m_location.absolutePath()); 139 | this->PopulateSongList(); 140 | } 141 | else 142 | { 143 | m_ui->locationValueLabel->setText("Select location..."); 144 | } 145 | } 146 | 147 | void PopulateSongList() 148 | { 149 | // Clear the song list 150 | m_ui->songListTable->clearContents(); 151 | 152 | // Populate the list of available projects 153 | auto projects = m_location.entryList(QDir::Dirs | QDir::NoDotAndDotDot); 154 | 155 | // Remove projects starting with "__" 156 | projects.removeIf([](const QString& project){ 157 | return project.startsWith("__"); 158 | }); 159 | 160 | // Remove projects with no REAPER (.rpp) file 161 | projects.removeIf([&](const QString& project){ 162 | QDir projectDir(m_location.absolutePath() + "/" + project); 163 | 164 | const auto files = projectDir.entryList(QDir::Files); 165 | for (const auto file : files) 166 | { 167 | if (file.endsWith(".rpp")) 168 | { 169 | return false; 170 | } 171 | } 172 | 173 | return true; 174 | }); 175 | 176 | // If projects exist update the table 177 | if (projects.length() > 0) 178 | { 179 | int row = 0; 180 | for (const auto project : projects) 181 | { 182 | // Split artist and song name on "-" 183 | auto parts = project.split(" - ", Qt::SkipEmptyParts); 184 | 185 | // Trim blank spaces 186 | for (auto& part : parts) 187 | { 188 | part = part.trimmed(); 189 | } 190 | 191 | // Validate result 192 | if (parts.size() != 2) 193 | { 194 | qDebug() << "Artist/song name could not be extracted for project: " << project; 195 | continue; 196 | } 197 | 198 | // Extract the artist and song name 199 | const auto artist = parts[0]; 200 | const auto song = parts[1]; 201 | 202 | // Insert a new row 203 | m_ui->songListTable->insertRow(row); 204 | 205 | // Build the row 206 | m_ui->songListTable->setItem(row, 0, new QTableWidgetItem(project)); 207 | m_ui->songListTable->setItem(row, 1, new QTableWidgetItem(artist)); 208 | m_ui->songListTable->setItem(row, 2, new QTableWidgetItem(song)); 209 | 210 | row++; 211 | } 212 | 213 | m_ui->songListTable->setSortingEnabled(true); 214 | m_ui->songListTable->sortItems(1, Qt::AscendingOrder); 215 | } 216 | } 217 | 218 | private: 219 | SongList* m_self; 220 | std::unique_ptr m_ui; 221 | 222 | QDir m_location; 223 | }; 224 | 225 | SongList::SongList(QWidget *parent) 226 | : QMainWindow(parent) 227 | , m_impl(std::make_unique(this)) 228 | {} 229 | 230 | SongList::~SongList() = default; 231 | 232 | void SongList::OnChangeButtonClicked() { 233 | m_impl->OnChangeButtonClicked(); 234 | } 235 | 236 | void SongList::OnSearchBoxTextChanged(const QString& text) { 237 | m_impl->OnSearchBoxTextChanged(text); 238 | } 239 | 240 | void SongList::OnSongListTableCellDoubleClicked(const int row, const int column) { 241 | m_impl->OnSongListTableCellDoubleClicked(row, column); 242 | } 243 | --------------------------------------------------------------------------------