diff --git a/README.md b/README.md index 1e7a931..fb8c9a2 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ # mcopy -Rename music by specifying a format +Rename/copy audio files based on their metadata. diff --git a/include/core.hpp b/include/core.hpp new file mode 100644 index 0000000..2ec98d8 --- /dev/null +++ b/include/core.hpp @@ -0,0 +1,38 @@ +/* mcopy + * + * Rename music by specifying a format + * Licensed under the GNU General Public License version 3. + * See included LICENSE file for copyright and license details. + */ + +#pragma once + +#include + +namespace mcopy { + struct Settings { + bool ask{true}; + bool quiet{false}; + bool strict{false}; + bool skip_existing{false}; + }; + + struct FileData { + std::string title{}; + std::string album{}; + std::string artist{}; + std::string track{}; + std::string year{}; + std::string genre{}; + std::string comment{}; + std::string extension{}; + }; + + enum class FileStatus { + Success, + Failure, + }; + + std::pair get_metadata_from_file(const std::string& file); + void write_to_file(const Settings& settings, const std::string& format, const std::string& file); +}; diff --git a/meson.build b/meson.build index d3f6f82..0d2b1ae 100644 --- a/meson.build +++ b/meson.build @@ -4,14 +4,24 @@ project( version : '"0.0.1"', ) +include_directories = [ + include_directories('include'), +] + project_source_files = [ 'src/mcopy.cpp', + 'src/core.cpp', ] project_dependencies = [ dependency('taglib'), + dependency('wxwidgets'), ] +if get_option('gui') + project_dependencies += [ dependency('wxwidgets') ] +endif + build_args = [ '-DMCOPY_VERSION=' + meson.project_version(), ] @@ -21,6 +31,22 @@ project_target = executable( project_source_files, install : true, dependencies: project_dependencies, cpp_args : build_args, + include_directories : include_directories, ) -test('mcopy', project_target) +test(meson.project_name(), project_target) + +if get_option('gui') + gui_project_source_files = [ + 'src/core.cpp', + 'src/mcopy_gui.cpp', + ] + + project_gui_target = executable( + 'mcopy_gui', + gui_project_source_files, install : true, + dependencies : project_dependencies, + cpp_args : build_args, + include_directories : include_directories, + ) +endif diff --git a/meson_options.txt b/meson_options.txt new file mode 100644 index 0000000..df73e3e --- /dev/null +++ b/meson_options.txt @@ -0,0 +1 @@ +option('gui', type : 'boolean', value : true, description : 'Enable GUI') diff --git a/src/core.cpp b/src/core.cpp new file mode 100644 index 0000000..c7781be --- /dev/null +++ b/src/core.cpp @@ -0,0 +1,139 @@ +/* mcopy + * + * Rename music by specifying a format + * Licensed under the GNU General Public License version 3. + * See included LICENSE file for copyright and license details. + */ + +#include +#include +#include +#include +#include +#include + +std::pair mcopy::get_metadata_from_file(const std::string& file) { + TagLib::FileRef _file = TagLib::FileRef(TagLib::FileName(file.c_str())); + + if (_file.isNull() || !_file.audioProperties()) { + return {mcopy::FileStatus::Failure, {}}; + } + + TagLib::Tag* tag = _file.tag(); + + const auto get_title = [&tag]() -> std::string { return tag->title().to8Bit(true); }; + const auto get_album = [&tag]() -> std::string { return tag->album().to8Bit(true); }; + const auto get_artist = [&tag]() -> std::string { return tag->artist().to8Bit(true); }; + const auto get_track = [&tag]() -> std::string { return std::to_string(tag->track()); }; + const auto get_year = [&tag]() -> std::string { return std::to_string(tag->year()); }; + const auto get_genre = [&tag]() -> std::string { return tag->genre().to8Bit(true); }; + const auto get_comment = [&tag]() -> std::string { return tag->comment().to8Bit(true); }; + const auto get_extension = [&file]() -> std::string { return file.find_last_of(".") + 1 != file.npos ? file.substr(file.find_last_of(".") + 1) : ""; }; + + mcopy::FileData data{ + get_title(), + get_album(), + get_artist(), + get_track(), + get_year(), + get_genre(), + get_comment(), + get_extension(), + }; + + return {mcopy::FileStatus::Success, data}; +} + +void mcopy::write_to_file(const mcopy::Settings& settings, const std::string& format, const std::string& file) { + if (!std::filesystem::is_regular_file(file)) { + std::cerr << "mcopy: invalid file '" << file << "' is skipped.\n"; + if (settings.strict) { + std::exit(EXIT_FAILURE); + } + + return; + } + + std::pair pair = mcopy::get_metadata_from_file(file); + if (pair.first == mcopy::FileStatus::Failure) { + std::cerr << "mcopy: invalid file '" << file << "' is skipped.\n"; + if (settings.strict) { + std::exit(EXIT_FAILURE); + } + + return; + } + + mcopy::FileData& data = pair.second; +#ifdef _WIN32 + std::vector the_forbidden_characters{'<', '>', ':', '"', '/', '\\', '|', '?', '*'}; +#else + std::vector the_forbidden_characters{'/'}; +#endif + + const std::vector data_members = {&data.title, &data.album, &data.artist, &data.track, &data.year, &data.genre, &data.comment, &data.extension}; + for (const auto& it : data_members) { + for (const auto& c : the_forbidden_characters) { + it->erase(std::remove_if(it->begin(), it->end(), [&c](char _c) { return c == _c; }), it->end()); + } + } + + std::unordered_map delim_map{ + {"@t", data.title}, + {"@a", data.album}, + {"@A", data.artist}, + {"@n", data.track}, + {"@y", data.year}, + {"@g", data.genre}, + {"@c", data.comment}, + {"@e", data.extension}, + }; + + std::string output_filename = format; + for (auto& it : delim_map) { + while (output_filename.find(it.first) != std::string::npos) { + if (it.second.empty()) { + it.second = "Unknown"; + + if (settings.ask) { + std::cerr << "mcopy: failed to retrieve data to represent " << it.first << ", enter an appropriate replacement: "; + std::getline(std::cin, it.second); + } + } + + output_filename.replace(output_filename.find(it.first), it.first.length(), it.second); + } + } + + std::ifstream ef(output_filename); + if (ef.good() && settings.skip_existing) { + std::cerr << "mcopy: file << '" << file << "' already exists, skipping\n"; + return; + } + + const std::string dir{std::filesystem::path{output_filename}.remove_filename()}; + if (dir.empty()) { + std::cerr << "mcopy: failed to retrieve directory: " << output_filename << "\n"; + if (settings.strict) { + std::exit(EXIT_FAILURE); + } + + return; + } + + if (!std::filesystem::exists(dir) && !std::filesystem::create_directories(dir)) { + std::cerr << "mcopy: failed to create directory '" << dir << "'\n"; + if (settings.strict) { + std::exit(EXIT_FAILURE); + } + + return; + } + + if (!std::filesystem::copy_file(file, output_filename, std::filesystem::copy_options::overwrite_existing)) { + std::cerr << "mcopy: failed to copy input file '" << file << "' to '" << output_filename << "'\n"; + if (settings.strict) std::exit(EXIT_FAILURE); + } else if (!settings.quiet) { + std::cout << "mcopy: copied input file '" << file << "' to '" << output_filename << "'\n"; + } +} diff --git a/src/mcopy.cpp b/src/mcopy.cpp index 2db70b4..c51fba0 100644 --- a/src/mcopy.cpp +++ b/src/mcopy.cpp @@ -6,25 +6,16 @@ */ #include -#include #include +#include #include -#include #include -#include -#include +#include #ifndef MCOPY_VERSION #define MCOPY_VERSION "0.0.1" #endif -struct Settings { - bool ask{true}; - bool quiet{false}; - bool strict{false}; - bool skip_existing{false}; -}; - void help() { std::cout << "mcopy [options]\n" << @@ -50,144 +41,14 @@ void help() { "@n Track number\n" << "@y Year\n" << "@g Genre\n" << + "@c Comment\n" << "@e File extension\n"; std::exit(EXIT_SUCCESS); } -TagLib::FileRef get_file(const std::string& f) { - return TagLib::FileRef(TagLib::FileName(f.c_str())); -} - -void get_metadata_from_file(const Settings& settings, const std::string& format, const std::string& file) { - if (!std::filesystem::is_regular_file(file)) { - std::cerr << "mcopy: invalid file '" << file << "' is skipped.\n"; - if (settings.strict) { - std::exit(EXIT_FAILURE); - } - - return; - } - - TagLib::FileRef _file = get_file(file); - - if (_file.isNull() || !_file.audioProperties()) { - std::cerr << "mcopy: invalid file '" << file << "' is skipped.\n"; - if (settings.strict) { - std::exit(EXIT_FAILURE); - } - - return; - } - - TagLib::Tag* tag = _file.tag(); - - struct FileData { - std::string title{}; - std::string album{}; - std::string artist{}; - std::string track{}; - std::string year{}; - std::string genre{}; - std::string comment{}; - std::string extension{}; - }; - - const auto get_title = [&tag]() -> std::string { return tag->title().to8Bit(true); }; - const auto get_album = [&tag]() -> std::string { return tag->album().to8Bit(true); }; - const auto get_artist = [&tag]() -> std::string { return tag->artist().to8Bit(true); }; - const auto get_track = [&tag]() -> std::string { return std::to_string(tag->track()); }; - const auto get_year = [&tag]() -> std::string { return std::to_string(tag->year()); }; - const auto get_genre = [&tag]() -> std::string { return tag->genre().to8Bit(true); }; - const auto get_comment = [&tag]() -> std::string { return tag->comment().to8Bit(true); }; - const auto get_extension = [&file]() -> std::string { return file.find_last_of(".") + 1 != file.npos ? file.substr(file.find_last_of(".") + 1) : ""; }; - - FileData data{ - get_title(), - get_album(), - get_artist(), - get_track(), - get_year(), - get_genre(), - get_comment(), - get_extension(), - }; -#ifdef _WIN32 - std::vector the_forbidden_characters{'<', '>', ':', '"', '/', '\\', '|', '?', '*'}; -#else - std::vector the_forbidden_characters{'/'}; -#endif - - const std::vector data_members = {&data.title, &data.album, &data.artist, &data.track, &data.year, &data.genre, &data.comment, &data.extension}; - - for (const auto& it : data_members) { - for (const auto& c : the_forbidden_characters) { - it->erase(std::remove_if(it->begin(), it->end(), [&c](char _c) { return c == _c; }), it->end()); - } - } - - std::unordered_map delim_map{ - {"@t", data.title}, - {"@a", data.album}, - {"@A", data.artist}, - {"@n", data.track}, - {"@y", data.year}, - {"@g", data.genre}, - {"@c", data.comment}, - {"@e", data.extension}, - }; - - std::string output_filename = format; - for (auto& it : delim_map) { - while (output_filename.find(it.first) != std::string::npos) { - if (it.second.empty()) { - it.second = "Unknown"; - - if (settings.ask) { - std::cerr << "mcopy: failed to retrieve data to represent " << it.first << ", enter an appropriate replacement: "; - std::getline(std::cin, it.second); - } - } - - output_filename.replace(output_filename.find(it.first), it.first.length(), it.second); - } - } - - std::ifstream ef(output_filename); - if (ef.good() && settings.skip_existing) { - std::cerr << "mcopy: file << '" << file << "' already exists, skipping\n"; - return; - } - - const std::string dir{std::filesystem::path{output_filename}.remove_filename()}; - if (dir.empty()) { - std::cerr << "mcopy: failed to retrieve directory\n"; - if (settings.strict) { - std::exit(EXIT_FAILURE); - } - - return; - } - - if (!std::filesystem::exists(dir) && !std::filesystem::create_directories(dir)) { - std::cerr << "mcopy: failed to create directory '" << dir << "'\n"; - if (settings.strict) { - std::exit(EXIT_FAILURE); - } - - return; - } - - if (!std::filesystem::copy_file(file, output_filename, std::filesystem::copy_options::overwrite_existing)) { - std::cerr << "mcopy: failed to copy input file '" << file << "' to '" << output_filename << "'\n"; - if (settings.strict) std::exit(EXIT_FAILURE); - } else if (!settings.quiet) { - std::cout << "mcopy: copied input file '" << file << "' to '" << output_filename << "'\n"; - } -} - int main(int argc, char **argv) { - Settings settings{}; + mcopy::Settings settings{}; std::string format{}; std::vector unknown_args{}; @@ -237,7 +98,7 @@ int main(int argc, char **argv) { std::ifstream f(it); if (f.good()) { - get_metadata_from_file(settings, format, it); + mcopy::write_to_file(settings, format, it); } else { std::cerr << "mcopy: input file '" << it << "' not found\n"; diff --git a/src/mcopy_gui.cpp b/src/mcopy_gui.cpp new file mode 100644 index 0000000..48b0303 --- /dev/null +++ b/src/mcopy_gui.cpp @@ -0,0 +1,376 @@ +/* mcopy + * + * Copy music by specifying a format + * Licensed under the GNU General Public License version 3. + * See included LICENSE file for copyright and license details. + */ + +#include +#include +#include +#include +#include +#include +#include + +#ifndef MCOPY_VERSION +#define MCOPY_VERSION "0.0.1" +#endif + +class Application : public wxApp { + public: + bool OnInit() override; + +}; + +wxIMPLEMENT_APP(Application); + +enum { + ActionID_Import = wxID_OPEN, + ActionID_About = wxID_ABOUT, + ActionID_Exit = wxID_EXIT, + ActionID_Copy = 2, + ActionID_Remove = 3, +}; + +enum { + Property_File = 0, + Property_Artist = 1, + Property_Album = 2, + Property_Title = 3, + Property_Track = 4, + Property_Year = 5, + Property_Genre = 6, + Property_Comment = 7, +}; + +struct Global { + std::vector> imported_files{}; + std::string format_str{}; + mcopy::Settings settings{}; +}; + +Global global{}; + +class Window : public wxFrame { + public: + Window(); + virtual ~Window() = default; + + void import_handler(wxCommandEvent& event); + void about_handler(wxCommandEvent& event); + void exit_handler(wxCommandEvent& event); + void rename_handler(wxCommandEvent& event); + void append_handler(wxCommandEvent& event); + void context_handler(wxContextMenuEvent& event); + void remove_handler(wxCommandEvent& event); + void selection_handler(wxListEvent& event); + + void handle_file(const std::string& format, const std::string& file); + void rebuild_list(); + private: + wxListCtrl* list = nullptr; + wxTextCtrl* format = nullptr; + + wxDECLARE_EVENT_TABLE(); +}; + +void Window::import_handler(wxCommandEvent& event) { + wxFileDialog open_file_dialog(this, _("Open audio files"), "", "", "All files (*.*)|*.*", wxFD_OPEN | wxFD_MULTIPLE | wxFD_FILE_MUST_EXIST); + + if (open_file_dialog.ShowModal() == wxID_CANCEL) { + return; + } + + wxArrayString paths{}; + open_file_dialog.GetPaths(paths); + + for (const auto& it : paths) { + const std::string f{it.ToStdString()}; + if (std::filesystem::exists(f) == false) { + wxMessageBox("Imported file '" + f + "' doesn't exist.", "File does not exist.", wxICON_ERROR | wxOK); + global.imported_files.clear(); + return; + } + global.imported_files.push_back({true, f}); + } + + wxMenuItem* copy_item = GetMenuBar()->FindItem(ActionID_Copy); + if (copy_item) { + copy_item->Enable(true); + } + + append_handler(event); +} + +void Window::about_handler(wxCommandEvent& event) { + wxMessageBox("mcopy_gui\n\nLicensed under the GNU General Public License version 3.\nCopyright (c) 2024 speedie\nhttps://git.speedie.site/speedie/mcopy\nAlso available as a command-line application.", "About mcopy_gui", wxOK | wxICON_INFORMATION); +} + +void Window::exit_handler(wxCommandEvent& event) { + Close(true); +} + +void Window::rename_handler(wxCommandEvent& event) { + if (this->format == nullptr) { + return; + } + if (this->format->IsEmpty()) { + wxMessageBox("You must specify a format in order to rename.", "Error", wxICON_ERROR | wxOK); + global.imported_files.clear(); + return; + } + + long _index = -1; + _index = list->GetNextItem(_index, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED); + + if (_index == wxNOT_FOUND) { + for (auto& it : global.imported_files) { + it.first = true; + } + } + + global.format_str = this->format->GetValue().ToStdString(); + + int i{0}; + for (const auto& it : global.imported_files) { + if (!it.first) { + continue; + } + + SetStatusText("Copying '" + it.second + "'."); + handle_file(global.format_str, it.second); + SetStatusText("Copied '" + it.second + "'."); + ++i; + } + + wxMessageBox("Successfully copied " + std::to_string(i) + " file(s).", "Complete", wxOK | wxICON_INFORMATION); +} + +void Window::handle_file(const std::string& format, const std::string& file) { + std::pair pair = mcopy::get_metadata_from_file(file); + mcopy::FileData& data = pair.second; +#ifdef _WIN32 + std::vector the_forbidden_characters{'<', '>', ':', '"', '/', '\\', '|', '?', '*'}; +#else + std::vector the_forbidden_characters{'/'}; +#endif + + const std::vector data_members = {&data.title, &data.album, &data.artist, &data.track, &data.year, &data.genre, &data.comment, &data.extension}; + for (const auto& it : data_members) { + for (const auto& c : the_forbidden_characters) { + it->erase(std::remove_if(it->begin(), it->end(), [&c](char _c) { return c == _c; }), it->end()); + } + } + + std::unordered_map delim_map{ + {"@t", data.title}, + {"@a", data.album}, + {"@A", data.artist}, + {"@n", data.track}, + {"@y", data.year}, + {"@g", data.genre}, + {"@c", data.comment}, + {"@e", data.extension}, + }; + + std::string output_filename = format; + for (auto& it : delim_map) { + while (output_filename.find(it.first) != std::string::npos) { + if (it.second.empty()) { + it.second = "Unknown"; + + wxTextEntryDialog dialog(this, "Failed to retrieve data to represent " + it.first + ", enter an appropriate replacement: ", "Please input data", wxEmptyString); + if (dialog.ShowModal() == wxID_OK) { + it.second = dialog.GetValue().ToStdString(); + } + } + + output_filename.replace(output_filename.find(it.first), it.first.length(), it.second); + } + } + + const std::string dir{std::filesystem::path{output_filename}.remove_filename()}; + if (dir.empty()) { + wxMessageBox("Failed to retrieve directory.", "Error", wxOK | wxICON_ERROR); + global.imported_files.clear(); + list->ClearAll(); + + return; + } + + if (!std::filesystem::exists(dir) && !std::filesystem::create_directories(dir)) { + wxMessageBox("Failed to create directory.", "Error", wxOK | wxICON_ERROR); + global.imported_files.clear(); + list->ClearAll(); + + return; + } + + if (!std::filesystem::copy_file(file, output_filename, std::filesystem::copy_options::overwrite_existing)) { + std::cerr << "mcopy: failed to copy input file '" << file << "' to '" << output_filename << "'\n"; + + wxMessageBox("Failed to copy input file.", "Error", wxOK | wxICON_ERROR); + global.imported_files.clear(); + list->ClearAll(); + + return; + } +} + +void Window::remove_handler(wxCommandEvent& event) { + long index{list->GetNextItem(-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED)}; + if (index != wxNOT_FOUND) { + list->DeleteItem(index); + global.imported_files.erase(global.imported_files.begin() + index); + } + + rebuild_list(); +} + +void Window::rebuild_list() { + if (list == nullptr) { + return; + } + + list->ClearAll(); + + list->InsertColumn(Property_File, "File", wxLIST_FORMAT_LEFT, 300); + list->InsertColumn(Property_Artist, "Artist", wxLIST_FORMAT_RIGHT, 100); + list->InsertColumn(Property_Album, "Album", wxLIST_FORMAT_RIGHT, 100); + list->InsertColumn(Property_Title, "Title", wxLIST_FORMAT_RIGHT, 100); + list->InsertColumn(Property_Track, "Track", wxLIST_FORMAT_RIGHT, 100); + list->InsertColumn(Property_Year, "Year", wxLIST_FORMAT_RIGHT, 100); + list->InsertColumn(Property_Genre, "Genre", wxLIST_FORMAT_RIGHT, 100); + list->InsertColumn(Property_Comment, "Comment", wxLIST_FORMAT_RIGHT, 100); + + for (std::size_t i{0}; i < global.imported_files.size(); i++) { + const auto& data = mcopy::get_metadata_from_file(global.imported_files.at(i).second); + + if (data.first == mcopy::FileStatus::Failure) { + wxMessageBox("Imported file '" + global.imported_files.at(i).second + "' is invalid.", "Invalid file", wxICON_ERROR | wxOK); + list->ClearAll(); + global.imported_files.clear(); + return; + } + + list->InsertItem(i, global.imported_files.at(i).second); + + list->SetItem(i, Property_Year, data.second.year); + list->SetItem(i, Property_Album, data.second.album); + list->SetItem(i, Property_Artist, data.second.artist); + list->SetItem(i, Property_Title, data.second.title); + list->SetItem(i, Property_Track, data.second.track); + list->SetItem(i, Property_Comment, data.second.comment); + list->SetItem(i, Property_Genre, data.second.genre); + } +} + +void Window::append_handler(wxCommandEvent& event) { + rebuild_list(); +} + +void Window::context_handler(wxContextMenuEvent& event) { + wxMenu menu; + + long index{list->GetNextItem(-1, wxLIST_NEXT_ALL, wxLIST_STATE_SELECTED)}; + if (index != wxNOT_FOUND) { + menu.Append(ActionID_Remove, "Delete"); + } else { + menu.Append(ActionID_Import, "Import"); + } + + wxPoint pos = event.GetPosition(); + pos = ScreenToClient(pos); + + PopupMenu(&menu, pos); +} + +void Window::selection_handler(wxListEvent& event) { + long index = event.GetIndex(); + if (index == wxNOT_FOUND) { + return; + } + std::string file_text = list->GetItemText(index).ToStdString(); + + for (auto& it : global.imported_files) { + it.first = false; + if (it.second == file_text) { + it.first = true; + } + } +} + +Window::Window() : wxFrame(nullptr, wxID_ANY, "mcopy_gui") { + const auto create_menu = [this]() -> void { + wxMenu* menu_file = new wxMenu; + menu_file->Append(ActionID_Import, "&Import\tCtrl-I", "Import files to copy."); + menu_file->Append(ActionID_Copy, "&Copy\tCtrl-Shift-C", "Copy imported files"); + + wxMenuItem* copy_item = menu_file->FindItem(ActionID_Copy); + if (copy_item && global.imported_files.empty()) { + copy_item->Enable(false); + } + + wxMenu* menu_help = new wxMenu; + menu_help->Append(ActionID_About); + + wxMenuBar* bar = new wxMenuBar; + bar->Append(menu_file, "&File"); + bar->Append(menu_help, "&Help"); + + SetMenuBar(bar); + }; + + const auto create_file_table = [this]() -> void { + wxPanel* panel = new wxPanel(this, wxID_ANY); + + list = new wxListCtrl(panel, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxLC_REPORT); + + if (list == nullptr) { + throw std::runtime_error{"list == nullptr"}; + } + + const std::string& home_sweet_home{wxFileName(wxStandardPaths::Get().GetUserDataDir()).GetPath().ToStdString()}; + + wxStaticText* label = new wxStaticText(panel, wxID_ANY, "Format:", wxDefaultPosition, wxSize(70, 30)); +#ifdef _WIN32 + this->format = new wxTextCtrl(panel, wxID_ANY, home_sweet_home + "\\Music\\@A\\@a\\@n. @A - @t.@e", wxDefaultPosition, wxSize(500, 30)); +#else + this->format = new wxTextCtrl(panel, wxID_ANY, home_sweet_home + "/Music/@A/@a/@n. @A - @t.@e", wxDefaultPosition, wxSize(500, 30)); +#endif + + wxBoxSizer* top_sizer = new wxBoxSizer(wxVERTICAL); + wxBoxSizer* input_sizer = new wxBoxSizer(wxHORIZONTAL); + + input_sizer->Add(label, 0, wxALL | wxCENTER, 5); + input_sizer->Add(this->format, 1, wxALL | wxCENTER, 5); + + top_sizer->Add(list, 1, wxEXPAND | wxALL, 5); // Add wxListCtrl to top_sizer + top_sizer->Add(input_sizer, 0, wxEXPAND | wxALL, 5); // Add input_sizer (with label and textbox) to top_sizer + + panel->SetSizer(top_sizer); + }; + + create_menu(); + create_file_table(); + + CreateStatusBar(); + + Bind(wxEVT_MENU, &Window::import_handler, this, ActionID_Import); + Bind(wxEVT_MENU, &Window::rename_handler, this, ActionID_Copy); + Bind(wxEVT_MENU, &Window::about_handler, this, ActionID_About); + Bind(wxEVT_MENU, &Window::exit_handler, this, ActionID_Exit); +} + +bool Application::OnInit() { + Window* win = new Window(); + return win->Show(true); +} + +wxBEGIN_EVENT_TABLE(Window, wxFrame) + EVT_CONTEXT_MENU(Window::context_handler) + EVT_MENU(ActionID_Import, Window::import_handler) + EVT_MENU(ActionID_Remove, Window::remove_handler) + EVT_LIST_ITEM_SELECTED(wxID_ANY, Window::selection_handler) +wxEND_EVENT_TABLE()