|
|
@@ -0,0 +1,423 @@
|
|
|
+#include <gtkmm.h>
|
|
|
+#include <iostream>
|
|
|
+#include <filesystem>
|
|
|
+#include <vector>
|
|
|
+#include <algorithm>
|
|
|
+#include <string>
|
|
|
+#include <thread>
|
|
|
+#include <atomic>
|
|
|
+#include <mutex>
|
|
|
+#include <deque>
|
|
|
+
|
|
|
+namespace fs = std::filesystem;
|
|
|
+
|
|
|
+// --- Natural Sort Helper ---
|
|
|
+// Sorts strings like humans do: "img1.jpg", "img2.jpg", "img10.jpg"
|
|
|
+struct NaturalSort {
|
|
|
+ bool operator()(const fs::path& a, const fs::path& b) const {
|
|
|
+ std::string s1 = a.filename().string();
|
|
|
+ std::string s2 = b.filename().string();
|
|
|
+
|
|
|
+ size_t i = 0, j = 0;
|
|
|
+ while (i < s1.length() && j < s2.length()) {
|
|
|
+ if (isdigit(s1[i]) && isdigit(s2[j])) {
|
|
|
+ size_t start1 = i, start2 = j;
|
|
|
+ while (i < s1.length() && isdigit(s1[i])) i++;
|
|
|
+ while (j < s2.length() && isdigit(s2[j])) j++;
|
|
|
+
|
|
|
+ long n1 = std::stol(s1.substr(start1, i - start1));
|
|
|
+ long n2 = std::stol(s2.substr(start2, j - start2));
|
|
|
+
|
|
|
+ if (n1 != n2) return n1 < n2;
|
|
|
+ } else {
|
|
|
+ if (s1[i] != s2[j]) return s1[i] < s2[j];
|
|
|
+ i++; j++;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return s1.length() < s2.length();
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// --- Data Structure for Thread Communication ---
|
|
|
+struct LoadedItem {
|
|
|
+ std::string path;
|
|
|
+ std::string filename;
|
|
|
+ Glib::RefPtr<Gdk::Pixbuf> pixbuf;
|
|
|
+};
|
|
|
+
|
|
|
+class RenamerWindow : public Gtk::Window {
|
|
|
+public:
|
|
|
+ RenamerWindow() : m_Dispatcher() {
|
|
|
+ set_title("Visual Renamer Pro");
|
|
|
+ set_default_size(1200, 800);
|
|
|
+
|
|
|
+ // Connect the background thread signal to the UI thread
|
|
|
+ m_Dispatcher.connect(sigc::mem_fun(*this, &RenamerWindow::on_worker_notification));
|
|
|
+
|
|
|
+ // --- Layout ---
|
|
|
+ m_VBox.set_orientation(Gtk::ORIENTATION_VERTICAL);
|
|
|
+ add(m_VBox);
|
|
|
+
|
|
|
+ // --- Toolbar ---
|
|
|
+ m_Toolbar.set_margin_top(10); m_Toolbar.set_margin_bottom(10);
|
|
|
+ m_Toolbar.set_margin_left(10); m_Toolbar.set_margin_right(10);
|
|
|
+ m_VBox.pack_start(m_Toolbar, Gtk::PACK_SHRINK);
|
|
|
+
|
|
|
+ // Open Button
|
|
|
+ Gtk::Image* icon_open = Gtk::manage(new Gtk::Image);
|
|
|
+ icon_open->set_from_icon_name("folder-open", Gtk::ICON_SIZE_BUTTON);
|
|
|
+ m_BtnOpen.set_image(*icon_open);
|
|
|
+ m_BtnOpen.set_label("Open Folder");
|
|
|
+ m_BtnOpen.set_always_show_image(true);
|
|
|
+ m_BtnOpen.signal_clicked().connect(sigc::mem_fun(*this, &RenamerWindow::on_open_folder_clicked));
|
|
|
+ m_Toolbar.pack_start(m_BtnOpen, Gtk::PACK_SHRINK, 5);
|
|
|
+
|
|
|
+ // Pattern Inputs
|
|
|
+ m_LblPattern.set_text(" Pattern:");
|
|
|
+ m_Toolbar.pack_start(m_LblPattern, Gtk::PACK_SHRINK, 5);
|
|
|
+ m_EntryPattern.set_text("image_###");
|
|
|
+ m_Toolbar.pack_start(m_EntryPattern, Gtk::PACK_SHRINK, 5);
|
|
|
+
|
|
|
+ // --- SIZE DROPDOWN ---
|
|
|
+ m_LblSize.set_text(" Size:");
|
|
|
+ m_Toolbar.pack_start(m_LblSize, Gtk::PACK_SHRINK, 5);
|
|
|
+
|
|
|
+ m_ComboSize.append("Small (100px)");
|
|
|
+ m_ComboSize.append("Medium (250px)");
|
|
|
+ m_ComboSize.append("Large (500px)");
|
|
|
+ m_ComboSize.set_active(1); // Default to Medium (Index 1)
|
|
|
+ m_ComboSize.signal_changed().connect(sigc::mem_fun(*this, &RenamerWindow::on_size_changed));
|
|
|
+ m_Toolbar.pack_start(m_ComboSize, Gtk::PACK_SHRINK, 5);
|
|
|
+ // ---------------------
|
|
|
+
|
|
|
+ // Rename Button
|
|
|
+ m_BtnRename.set_label("Rename Files");
|
|
|
+ m_BtnRename.set_sensitive(false);
|
|
|
+ m_BtnRename.get_style_context()->add_class("suggested-action");
|
|
|
+ m_BtnRename.signal_clicked().connect(sigc::mem_fun(*this, &RenamerWindow::on_rename_files));
|
|
|
+ m_Toolbar.pack_end(m_BtnRename, Gtk::PACK_SHRINK, 5);
|
|
|
+
|
|
|
+ // --- Icon View Area ---
|
|
|
+ m_ScrolledWindow.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
|
|
|
+ m_ScrolledWindow.set_shadow_type(Gtk::SHADOW_IN);
|
|
|
+ m_VBox.pack_start(m_ScrolledWindow);
|
|
|
+
|
|
|
+ m_RefListStore = Gtk::ListStore::create(m_Columns);
|
|
|
+ m_IconView.set_model(m_RefListStore);
|
|
|
+ m_IconView.set_text_column(m_Columns.m_col_name);
|
|
|
+ m_IconView.set_pixbuf_column(m_Columns.m_col_pixbuf);
|
|
|
+
|
|
|
+ m_IconView.set_item_width(250);
|
|
|
+ m_IconView.set_selection_mode(Gtk::SELECTION_MULTIPLE);
|
|
|
+ m_IconView.set_reorderable(true);
|
|
|
+ m_IconView.set_margin(10);
|
|
|
+ m_IconView.set_column_spacing(10);
|
|
|
+ m_IconView.set_row_spacing(10);
|
|
|
+
|
|
|
+ // FIX: Use .index() to prevent compile error
|
|
|
+ m_IconView.set_tooltip_column(m_Columns.m_col_orig_name.index());
|
|
|
+
|
|
|
+ m_IconView.signal_button_press_event().connect(sigc::mem_fun(*this, &RenamerWindow::on_iconview_button_press), false);
|
|
|
+ m_IconView.signal_key_press_event().connect(sigc::mem_fun(*this, &RenamerWindow::on_iconview_key_press), false);
|
|
|
+
|
|
|
+ m_ScrolledWindow.add(m_IconView);
|
|
|
+
|
|
|
+ // --- Status Bar ---
|
|
|
+ m_Statusbar.push("Ready.");
|
|
|
+ m_VBox.pack_end(m_Statusbar, Gtk::PACK_SHRINK);
|
|
|
+
|
|
|
+ // --- Drag and Drop (System -> Window) ---
|
|
|
+ std::vector<Gtk::TargetEntry> listTargets;
|
|
|
+ listTargets.push_back(Gtk::TargetEntry("text/uri-list"));
|
|
|
+ drag_dest_set(listTargets, Gtk::DEST_DEFAULT_ALL, Gdk::ACTION_COPY);
|
|
|
+ signal_drag_data_received().connect(sigc::mem_fun(*this, &RenamerWindow::on_drag_data_received));
|
|
|
+
|
|
|
+ // --- Context Menu ---
|
|
|
+ auto item = Gtk::manage(new Gtk::MenuItem("Remove Selected"));
|
|
|
+ item->signal_activate().connect(sigc::mem_fun(*this, &RenamerWindow::on_menu_remove));
|
|
|
+ m_ContextMenu.append(*item);
|
|
|
+ m_ContextMenu.show_all();
|
|
|
+
|
|
|
+ show_all_children();
|
|
|
+ }
|
|
|
+
|
|
|
+ ~RenamerWindow() override {
|
|
|
+ m_stop_flag = true;
|
|
|
+ if (m_WorkerThread.joinable()) m_WorkerThread.join();
|
|
|
+ }
|
|
|
+
|
|
|
+protected:
|
|
|
+ // Widgets
|
|
|
+ Gtk::Box m_VBox, m_Toolbar;
|
|
|
+ Gtk::Button m_BtnOpen, m_BtnRename;
|
|
|
+ Gtk::Label m_LblPattern, m_LblSize;
|
|
|
+ Gtk::Entry m_EntryPattern;
|
|
|
+ Gtk::ComboBoxText m_ComboSize;
|
|
|
+ Gtk::ScrolledWindow m_ScrolledWindow;
|
|
|
+ Gtk::IconView m_IconView;
|
|
|
+ Gtk::Statusbar m_Statusbar;
|
|
|
+ Gtk::Menu m_ContextMenu;
|
|
|
+
|
|
|
+ // Model
|
|
|
+ class ModelColumns : public Gtk::TreeModel::ColumnRecord {
|
|
|
+ public:
|
|
|
+ ModelColumns() { add(m_col_path); add(m_col_name); add(m_col_pixbuf); add(m_col_orig_name); }
|
|
|
+ Gtk::TreeModelColumn<std::string> m_col_path;
|
|
|
+ Gtk::TreeModelColumn<std::string> m_col_name;
|
|
|
+ Gtk::TreeModelColumn<Glib::RefPtr<Gdk::Pixbuf>> m_col_pixbuf;
|
|
|
+ Gtk::TreeModelColumn<std::string> m_col_orig_name;
|
|
|
+ };
|
|
|
+ ModelColumns m_Columns;
|
|
|
+ Glib::RefPtr<Gtk::ListStore> m_RefListStore;
|
|
|
+
|
|
|
+ // Threading
|
|
|
+ std::thread m_WorkerThread;
|
|
|
+ std::atomic<bool> m_stop_flag{false};
|
|
|
+ Glib::Dispatcher m_Dispatcher;
|
|
|
+ std::mutex m_QueueMutex;
|
|
|
+ std::deque<LoadedItem> m_ResultQueue;
|
|
|
+
|
|
|
+ // --- Helper Logic ---
|
|
|
+
|
|
|
+ int get_current_size() {
|
|
|
+ std::string txt = m_ComboSize.get_active_text();
|
|
|
+ if (txt.find("100px") != std::string::npos) return 100;
|
|
|
+ if (txt.find("500px") != std::string::npos) return 500;
|
|
|
+ return 250;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Called when Dropdown Changes
|
|
|
+ void on_size_changed() {
|
|
|
+ int new_size = get_current_size();
|
|
|
+
|
|
|
+ // 1. Collect current files (preserve order)
|
|
|
+ std::vector<fs::path> current_files;
|
|
|
+ auto children = m_RefListStore->children();
|
|
|
+ for(auto iter = children.begin(); iter != children.end(); ++iter) {
|
|
|
+ current_files.push_back(fs::path((std::string)(*iter)[m_Columns.m_col_path]));
|
|
|
+ }
|
|
|
+
|
|
|
+ if (current_files.empty()) return;
|
|
|
+
|
|
|
+ // 2. Stop Thread
|
|
|
+ m_stop_flag = true;
|
|
|
+ if (m_WorkerThread.joinable()) m_WorkerThread.join();
|
|
|
+
|
|
|
+ // 3. Clear UI & Restart Thread
|
|
|
+ m_RefListStore->clear();
|
|
|
+ m_IconView.set_item_width(new_size);
|
|
|
+ m_Statusbar.push("Resizing...");
|
|
|
+
|
|
|
+ m_stop_flag = false;
|
|
|
+ m_WorkerThread = std::thread(&RenamerWindow::worker_thread_files, this, current_files, new_size);
|
|
|
+ }
|
|
|
+
|
|
|
+ void start_loading_folder(std::string path) {
|
|
|
+ m_stop_flag = true;
|
|
|
+ if (m_WorkerThread.joinable()) m_WorkerThread.join();
|
|
|
+
|
|
|
+ m_RefListStore->clear();
|
|
|
+ m_BtnRename.set_sensitive(false);
|
|
|
+ m_Statusbar.push("Loading images from: " + path);
|
|
|
+
|
|
|
+ int size = get_current_size();
|
|
|
+ m_IconView.set_item_width(size);
|
|
|
+
|
|
|
+ m_stop_flag = false;
|
|
|
+ m_WorkerThread = std::thread(&RenamerWindow::worker_thread_folder, this, path, size);
|
|
|
+ }
|
|
|
+
|
|
|
+ void on_open_folder_clicked() {
|
|
|
+ Gtk::FileChooserDialog dialog(*this, "Select Folder", Gtk::FILE_CHOOSER_ACTION_SELECT_FOLDER);
|
|
|
+ dialog.add_button("Cancel", Gtk::RESPONSE_CANCEL);
|
|
|
+ dialog.add_button("Select", Gtk::RESPONSE_OK);
|
|
|
+ if (dialog.run() == Gtk::RESPONSE_OK) {
|
|
|
+ start_loading_folder(dialog.get_filename());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ void on_drag_data_received(const Glib::RefPtr<Gdk::DragContext>& context, int, int, const Gtk::SelectionData& selection_data, guint, guint time) {
|
|
|
+ std::vector<Glib::ustring> uris = selection_data.get_uris();
|
|
|
+ if (uris.empty()) return;
|
|
|
+
|
|
|
+ std::string filename = Glib::filename_from_uri(uris[0]);
|
|
|
+ if (fs::is_directory(filename)) {
|
|
|
+ start_loading_folder(filename);
|
|
|
+ context->drag_finish(true, false, time);
|
|
|
+ } else {
|
|
|
+ // Load parent folder if a single file is dropped
|
|
|
+ start_loading_folder(fs::path(filename).parent_path().string());
|
|
|
+ context->drag_finish(true, false, time);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // FIX: Correct logic for getting path at position
|
|
|
+ bool on_iconview_button_press(GdkEventButton* event) {
|
|
|
+ if (event->type == GDK_BUTTON_PRESS && event->button == 3) {
|
|
|
+ // FIX: Cast to int to match GTKmm signature
|
|
|
+ Gtk::TreeModel::Path path = m_IconView.get_path_at_pos((int)event->x, (int)event->y);
|
|
|
+
|
|
|
+ if (path) {
|
|
|
+ if (!m_IconView.path_is_selected(path)) {
|
|
|
+ m_IconView.unselect_all();
|
|
|
+ m_IconView.select_path(path);
|
|
|
+ }
|
|
|
+ m_ContextMenu.popup_at_pointer((GdkEvent*)event);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ bool on_iconview_key_press(GdkEventKey* event) {
|
|
|
+ if (event->keyval == GDK_KEY_Delete) {
|
|
|
+ on_menu_remove();
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ void on_menu_remove() {
|
|
|
+ auto selection = m_IconView.get_selected_items();
|
|
|
+ std::vector<Gtk::TreeRowReference> refs;
|
|
|
+ for (auto path : selection) refs.push_back(Gtk::TreeRowReference(m_RefListStore, path));
|
|
|
+ for (auto ref : refs) if (ref.is_valid()) m_RefListStore->erase(m_RefListStore->get_iter(ref.get_path()));
|
|
|
+ update_labels();
|
|
|
+ }
|
|
|
+
|
|
|
+ void update_labels() {
|
|
|
+ int index = 1;
|
|
|
+ for (auto row : m_RefListStore->children()) {
|
|
|
+ std::string filename = row[m_Columns.m_col_orig_name];
|
|
|
+ std::string label = "#" + std::to_string(index++) + "\n" + filename;
|
|
|
+ row[m_Columns.m_col_name] = label;
|
|
|
+ }
|
|
|
+ m_Statusbar.push("Items remaining: " + std::to_string(index - 1));
|
|
|
+ }
|
|
|
+
|
|
|
+ // --- WORKER: Load from Directory ---
|
|
|
+ void worker_thread_folder(std::string dir_path, int size) {
|
|
|
+ std::vector<fs::path> files;
|
|
|
+ try {
|
|
|
+ for (const auto& entry : fs::directory_iterator(dir_path)) {
|
|
|
+ if (m_stop_flag) return;
|
|
|
+ std::string ext = entry.path().extension().string();
|
|
|
+ std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
|
|
|
+ if (ext == ".jpg" || ext == ".png" || ext == ".jpeg" || ext == ".gif") {
|
|
|
+ files.push_back(entry.path());
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (...) { return; }
|
|
|
+
|
|
|
+ std::sort(files.begin(), files.end(), NaturalSort());
|
|
|
+ process_files(files, size);
|
|
|
+ }
|
|
|
+
|
|
|
+ // --- WORKER: Load from List ---
|
|
|
+ void worker_thread_files(std::vector<fs::path> files, int size) {
|
|
|
+ process_files(files, size);
|
|
|
+ }
|
|
|
+
|
|
|
+ // --- Shared Processing Logic ---
|
|
|
+ void process_files(const std::vector<fs::path>& files, int size) {
|
|
|
+ for (const auto& path : files) {
|
|
|
+ if (m_stop_flag) return;
|
|
|
+ LoadedItem item;
|
|
|
+ item.path = path.string();
|
|
|
+ item.filename = path.filename().string();
|
|
|
+
|
|
|
+ try {
|
|
|
+ auto pixbuf = Gdk::Pixbuf::create_from_file(path.string());
|
|
|
+ int w = pixbuf->get_width();
|
|
|
+ int h = pixbuf->get_height();
|
|
|
+ float ratio = (float)w / h;
|
|
|
+
|
|
|
+ int dest_w = (ratio > 1) ? size : size * ratio;
|
|
|
+ int dest_h = (ratio > 1) ? size / ratio : size;
|
|
|
+
|
|
|
+ // Scale in background thread
|
|
|
+ item.pixbuf = pixbuf->scale_simple(dest_w, dest_h, Gdk::INTERP_BILINEAR);
|
|
|
+
|
|
|
+ {
|
|
|
+ std::lock_guard<std::mutex> lock(m_QueueMutex);
|
|
|
+ m_ResultQueue.push_back(item);
|
|
|
+ }
|
|
|
+ m_Dispatcher.emit();
|
|
|
+ std::this_thread::sleep_for(std::chrono::microseconds(500));
|
|
|
+ } catch (...) {}
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ void on_worker_notification() {
|
|
|
+ std::lock_guard<std::mutex> lock(m_QueueMutex);
|
|
|
+ while (!m_ResultQueue.empty()) {
|
|
|
+ LoadedItem item = m_ResultQueue.front();
|
|
|
+ m_ResultQueue.pop_front();
|
|
|
+
|
|
|
+ auto row = *(m_RefListStore->append());
|
|
|
+ row[m_Columns.m_col_path] = item.path;
|
|
|
+ row[m_Columns.m_col_orig_name] = item.filename;
|
|
|
+ row[m_Columns.m_col_pixbuf] = item.pixbuf;
|
|
|
+ }
|
|
|
+ update_labels();
|
|
|
+ if (!m_RefListStore->children().empty()) m_BtnRename.set_sensitive(true);
|
|
|
+ }
|
|
|
+
|
|
|
+ void on_rename_files() {
|
|
|
+ std::string pattern = m_EntryPattern.get_text();
|
|
|
+ if (pattern.empty()) return;
|
|
|
+
|
|
|
+ Gtk::MessageDialog dialog(*this, "Confirm Rename", false, Gtk::MESSAGE_QUESTION, Gtk::BUTTONS_YES_NO);
|
|
|
+ dialog.set_secondary_text("Renaming " + std::to_string(m_RefListStore->children().size()) + " files.");
|
|
|
+ if (dialog.run() != Gtk::RESPONSE_YES) return;
|
|
|
+
|
|
|
+ auto children = m_RefListStore->children();
|
|
|
+ int index = 1;
|
|
|
+ std::vector<std::pair<std::string, std::string>> rename_map;
|
|
|
+
|
|
|
+ // 1. Temp Rename
|
|
|
+ for (auto row : children) {
|
|
|
+ fs::path p((std::string)row[m_Columns.m_col_path]);
|
|
|
+ std::string temp = "__temp_" + std::to_string(index++) + p.extension().string();
|
|
|
+ try {
|
|
|
+ fs::rename(p, p.parent_path() / temp);
|
|
|
+ rename_map.push_back({(p.parent_path() / temp).string(), p.extension().string()});
|
|
|
+ } catch(...) {}
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. Final Rename
|
|
|
+ index = 1;
|
|
|
+ size_t hash_pos = pattern.find('#');
|
|
|
+ int pad_width = 0;
|
|
|
+ if (hash_pos != std::string::npos) {
|
|
|
+ size_t end = pattern.find_last_of('#');
|
|
|
+ pad_width = end - hash_pos + 1;
|
|
|
+ }
|
|
|
+
|
|
|
+ for (const auto& item : rename_map) {
|
|
|
+ fs::path src(item.first);
|
|
|
+ std::string new_name;
|
|
|
+ if (pad_width > 0) {
|
|
|
+ std::string num = std::to_string(index);
|
|
|
+ while (num.length() < pad_width) num = "0" + num;
|
|
|
+ std::string pat = pattern;
|
|
|
+ pat.replace(hash_pos, pad_width, num);
|
|
|
+ new_name = pat + item.second;
|
|
|
+ } else {
|
|
|
+ new_name = pattern + "_" + std::to_string(index) + item.second;
|
|
|
+ }
|
|
|
+ try { fs::rename(src, src.parent_path() / new_name); } catch(...) {}
|
|
|
+ index++;
|
|
|
+ }
|
|
|
+
|
|
|
+ m_RefListStore->clear();
|
|
|
+ m_Statusbar.push("Rename Complete.");
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+int main(int argc, char *argv[]) {
|
|
|
+ Glib::thread_init();
|
|
|
+ auto app = Gtk::Application::create(argc, argv, "org.gtkmm.renamer_dropdown");
|
|
|
+ RenamerWindow window;
|
|
|
+ return app->run(window);
|
|
|
+}
|