main.cpp 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535
  1. #include <gtkmm.h>
  2. #include <giomm.h>
  3. #include <exiv2/exiv2.hpp>
  4. #include <iostream>
  5. #include <filesystem>
  6. #include <vector>
  7. #include <algorithm>
  8. #include <string>
  9. #include <thread>
  10. #include <atomic>
  11. #include <mutex>
  12. #include <deque>
  13. #include <stack>
  14. #include <iomanip>
  15. #include <sstream>
  16. #include <set>
  17. #include <utime.h>
  18. #include <time.h>
  19. namespace fs = std::filesystem;
  20. // --- Natural Sort Helper ---
  21. struct NaturalSort {
  22. bool operator()(const std::string& a, const std::string& b) const {
  23. size_t i = 0, j = 0;
  24. while (i < a.length() && j < b.length()) {
  25. if (isdigit(a[i]) && isdigit(b[j])) {
  26. size_t s1 = i, s2 = j;
  27. while (i < a.length() && isdigit(a[i])) i++;
  28. while (j < b.length() && isdigit(b[j])) j++;
  29. try {
  30. long n1 = std::stol(a.substr(s1, i - s1));
  31. long n2 = std::stol(b.substr(s2, j - s2));
  32. if (n1 != n2) return n1 < n2;
  33. } catch (...) {
  34. if (a.substr(s1, i - s1) != b.substr(s2, j - s2))
  35. return a.substr(s1, i - s1) < b.substr(s2, j - s2);
  36. }
  37. } else {
  38. if (a[i] != b[j]) return a[i] < b[j];
  39. i++; j++;
  40. }
  41. }
  42. return a.length() < b.length();
  43. }
  44. };
  45. class CellRendererFrame : public Gtk::CellRendererPixbuf {
  46. public:
  47. CellRendererFrame() { property_xpad() = 10; property_ypad() = 10; }
  48. protected:
  49. void render_vfunc(const Cairo::RefPtr<Cairo::Context>& cr, Gtk::Widget& widget, const Gdk::Rectangle&, const Gdk::Rectangle& cell_area, Gtk::CellRendererState flags) override {
  50. cr->save();
  51. auto style = widget.get_style_context();
  52. Gdk::RGBA bg = style->get_background_color();
  53. if ((bg.get_red() + bg.get_green() + bg.get_blue()) / 3.0 > 0.5) cr->set_source_rgb(0.5, 0.5, 0.5);
  54. else cr->set_source_rgb(0.8, 0.8, 0.8);
  55. cr->set_line_width(2.0);
  56. cr->rectangle(cell_area.get_x() + 2, cell_area.get_y() + 2, cell_area.get_width() - 4, cell_area.get_height() - 4);
  57. cr->stroke(); cr->restore();
  58. Gtk::CellRendererPixbuf::render_vfunc(cr, widget, cell_area, cell_area, flags);
  59. }
  60. };
  61. struct RenameAction { std::string original_path; std::string new_path; };
  62. struct UndoStep { std::vector<RenameAction> moves; };
  63. struct LoadedItem {
  64. std::string path, filename, ext;
  65. std::string sort_time_str;
  66. long long sort_time_raw;
  67. Glib::RefPtr<Gdk::Pixbuf> pixbuf;
  68. int orig_w = 0, orig_h = 0;
  69. uintmax_t filesize = 0;
  70. bool is_zoom_update = false;
  71. Gtk::TreeModel::Path model_path;
  72. };
  73. class RenamerWindow : public Gtk::Window {
  74. public:
  75. RenamerWindow() : m_Dispatcher() {
  76. set_title("Simple Image Renamer 1.1");
  77. set_default_size(1280, 850);
  78. set_wmclass("simpleimagerenamer", "simpleimagerenamer");
  79. m_Dispatcher.connect(sigc::mem_fun(*this, &RenamerWindow::on_worker_notification));
  80. m_VBox.set_orientation(Gtk::ORIENTATION_VERTICAL);
  81. add(m_VBox);
  82. // --- Toolbar ---
  83. m_ToolbarTop.set_margin_top(5); m_ToolbarTop.set_margin_bottom(5);
  84. m_ToolbarTop.set_margin_left(10); m_ToolbarTop.set_margin_right(10);
  85. m_VBox.pack_start(m_ToolbarTop, Gtk::PACK_SHRINK);
  86. m_BtnOpen.set_label("Open Folder");
  87. m_BtnOpen.signal_clicked().connect(sigc::mem_fun(*this, &RenamerWindow::on_open_folder_clicked));
  88. m_ToolbarTop.pack_start(m_BtnOpen, Gtk::PACK_SHRINK, 5);
  89. m_ToolbarTop.pack_start(*Gtk::manage(new Gtk::Label(" Sort: ")), Gtk::PACK_SHRINK);
  90. m_ComboSort.append("Manual (Drag & Drop)");
  91. m_ComboSort.append("Name");
  92. m_ComboSort.append("Oldest (EXIF/File)");
  93. m_ComboSort.append("Newest (EXIF/File)");
  94. m_ComboSort.set_active(0);
  95. m_ComboSort.signal_changed().connect(sigc::mem_fun(*this, &RenamerWindow::on_sort_changed));
  96. m_ToolbarTop.pack_start(m_ComboSort, Gtk::PACK_SHRINK, 5);
  97. // --- Zoom Slider ---
  98. m_ToolbarTop.pack_start(*Gtk::manage(new Gtk::Label(" Zoom: ")), Gtk::PACK_SHRINK);
  99. m_AdjZoom = Gtk::Adjustment::create(220, 60, 600, 10, 50, 0);
  100. m_ScaleZoom.set_adjustment(m_AdjZoom);
  101. m_ScaleZoom.set_draw_value(false);
  102. m_ScaleZoom.set_size_request(150, -1);
  103. m_ScaleZoom.signal_value_changed().connect(sigc::mem_fun(*this, &RenamerWindow::on_size_changed));
  104. m_ToolbarTop.pack_start(m_ScaleZoom, Gtk::PACK_SHRINK, 5);
  105. m_BtnUndo.set_label("Undo Rename");
  106. m_BtnUndo.set_sensitive(false);
  107. m_BtnUndo.signal_clicked().connect(sigc::mem_fun(*this, &RenamerWindow::on_undo_clicked));
  108. m_ToolbarTop.pack_end(m_BtnUndo, Gtk::PACK_SHRINK, 5);
  109. // --- Controls ---
  110. m_FrameControls.set_shadow_type(Gtk::SHADOW_ETCHED_IN);
  111. m_VBox.pack_start(m_FrameControls, Gtk::PACK_SHRINK);
  112. m_ToolbarControls.set_margin_top(10); m_ToolbarControls.set_margin_bottom(10);
  113. m_FrameControls.add(m_ToolbarControls);
  114. m_ComboMode.append("Sequential");
  115. m_ComboMode.append("Replace");
  116. m_ComboMode.append("Change Date");
  117. m_ComboMode.set_active(0);
  118. m_ComboMode.signal_changed().connect(sigc::mem_fun(*this, &RenamerWindow::on_mode_changed));
  119. m_ToolbarControls.pack_start(m_ComboMode, Gtk::PACK_SHRINK, 5);
  120. // --- Sequential Pattern Mode ---
  121. m_EntryPattern.set_text("image_###");
  122. m_EntryPattern.signal_changed().connect(sigc::mem_fun(*this, &RenamerWindow::update_preview));
  123. m_BoxPattern.pack_start(m_EntryPattern, Gtk::PACK_EXPAND_WIDGET);
  124. m_AdjStartNum = Gtk::Adjustment::create(1, 0, 100000, 1, 10, 0);
  125. m_SpinStartNum.set_adjustment(m_AdjStartNum);
  126. m_SpinStartNum.signal_value_changed().connect(sigc::mem_fun(*this, &RenamerWindow::update_preview));
  127. m_BoxPattern.pack_start(m_SpinStartNum, Gtk::PACK_SHRINK);
  128. m_StackModes.add(m_BoxPattern, "Pattern");
  129. // --- Find & Replace Mode ---
  130. m_EntryFind.set_placeholder_text("Find string");
  131. m_EntryFind.signal_changed().connect(sigc::mem_fun(*this, &RenamerWindow::update_preview));
  132. m_BoxReplace.pack_start(*Gtk::manage(new Gtk::Label("Find:")), Gtk::PACK_SHRINK, 5);
  133. m_BoxReplace.pack_start(m_EntryFind, Gtk::PACK_EXPAND_WIDGET, 5);
  134. m_EntryReplace.set_placeholder_text("Replacement string");
  135. m_EntryReplace.signal_changed().connect(sigc::mem_fun(*this, &RenamerWindow::update_preview));
  136. m_BoxReplace.pack_start(*Gtk::manage(new Gtk::Label("Replace:")), Gtk::PACK_SHRINK, 5);
  137. m_BoxReplace.pack_start(m_EntryReplace, Gtk::PACK_EXPAND_WIDGET, 5);
  138. m_StackModes.add(m_BoxReplace, "Replace");
  139. // --- Change Date Mode ---
  140. m_EntryDate.set_placeholder_text("YYYY-MM-DD HH:MM:SS (Empty = 1970)");
  141. m_EntryDate.signal_changed().connect(sigc::mem_fun(*this, &RenamerWindow::update_preview));
  142. m_BoxDate.pack_start(*Gtk::manage(new Gtk::Label("Target Date:")), Gtk::PACK_SHRINK, 5);
  143. m_BoxDate.pack_start(m_EntryDate, Gtk::PACK_EXPAND_WIDGET, 5);
  144. m_StackModes.add(m_BoxDate, "Date");
  145. m_ToolbarControls.pack_start(m_StackModes, Gtk::PACK_EXPAND_WIDGET, 5);
  146. m_BtnRename.set_label("Apply New Names");
  147. m_BtnRename.get_style_context()->add_class("suggested-action");
  148. m_BtnRename.signal_clicked().connect(sigc::mem_fun(*this, &RenamerWindow::on_rename_execute));
  149. m_ToolbarControls.pack_end(m_BtnRename, Gtk::PACK_SHRINK, 5);
  150. // --- Main View ---
  151. m_ScrolledWindow.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_ALWAYS);
  152. m_VBox.pack_start(m_ScrolledWindow);
  153. m_RefListStore = Gtk::ListStore::create(m_Columns);
  154. m_RefListStore->signal_row_deleted().connect([this](const Gtk::TreeModel::Path&){ update_preview(); });
  155. m_IconView.set_model(m_RefListStore);
  156. m_IconView.set_reorderable(true);
  157. m_IconView.pack_start(m_cell_frame, false);
  158. m_IconView.add_attribute(m_cell_frame, "pixbuf", m_Columns.m_col_pixbuf);
  159. m_IconView.pack_start(m_cell_toggle, false);
  160. m_IconView.add_attribute(m_cell_toggle, "active", m_Columns.m_col_checked);
  161. m_cell_toggle.property_activatable() = true;
  162. m_cell_toggle.signal_toggled().connect(sigc::mem_fun(*this, &RenamerWindow::on_cell_toggled));
  163. m_IconView.pack_start(m_cell_text, false);
  164. m_IconView.add_attribute(m_cell_text, "markup", m_Columns.m_col_markup);
  165. m_IconView.set_item_width(220); m_IconView.set_spacing(20);
  166. m_IconView.signal_item_activated().connect(sigc::mem_fun(*this, &RenamerWindow::on_item_activated));
  167. m_MenuItemSelectAll.set_label("Select All");
  168. m_MenuItemSelectAll.signal_activate().connect([this](){ on_selection_change(true); });
  169. m_MenuPopup.append(m_MenuItemSelectAll);
  170. m_MenuItemSelectNone.set_label("Select None");
  171. m_MenuItemSelectNone.signal_activate().connect([this](){ on_selection_change(false); });
  172. m_MenuPopup.append(m_MenuItemSelectNone);
  173. m_MenuPopup.append(*Gtk::manage(new Gtk::SeparatorMenuItem()));
  174. m_MenuItemOpenWith.set_label("Open With...");
  175. m_MenuItemOpenWith.signal_activate().connect(sigc::mem_fun(*this, &RenamerWindow::on_open_with_clicked));
  176. m_MenuPopup.append(m_MenuItemOpenWith);
  177. m_MenuItemTerminal.set_label("Open Terminal Here");
  178. m_MenuItemTerminal.signal_activate().connect([this](){ launch_terminal(); });
  179. m_MenuPopup.append(m_MenuItemTerminal);
  180. m_MenuPopup.append(*Gtk::manage(new Gtk::SeparatorMenuItem()));
  181. m_MenuItemDelete.set_label("Move to Trash");
  182. m_MenuItemDelete.signal_activate().connect(sigc::mem_fun(*this, &RenamerWindow::on_delete_selected));
  183. m_MenuPopup.append(m_MenuItemDelete);
  184. m_MenuPopup.show_all();
  185. m_MenuPopup.attach_to_widget(m_IconView);
  186. m_IconView.signal_button_press_event().connect(sigc::mem_fun(*this, &RenamerWindow::on_iconview_button_press), false);
  187. m_ScrolledWindow.add(m_IconView);
  188. m_VBox.pack_start(m_ProgressBar, Gtk::PACK_SHRINK);
  189. m_VBox.pack_end(m_Statusbar, Gtk::PACK_SHRINK);
  190. this->signal_key_press_event().connect(sigc::mem_fun(*this, &RenamerWindow::on_window_key_press), false);
  191. show_all_children(); m_ProgressBar.hide();
  192. }
  193. ~RenamerWindow() { m_stop_flag = true; if (m_WorkerThread.joinable()) m_WorkerThread.join(); }
  194. protected:
  195. Gtk::Box m_VBox, m_ToolbarTop, m_ToolbarControls, m_BoxPattern, m_BoxReplace, m_BoxDate;
  196. Gtk::Frame m_FrameControls;
  197. Gtk::Button m_BtnOpen, m_BtnRename, m_BtnUndo;
  198. Gtk::Entry m_EntryPattern, m_EntryFind, m_EntryReplace, m_EntryDate;
  199. Gtk::ComboBoxText m_ComboSort, m_ComboMode;
  200. Gtk::Scale m_ScaleZoom;
  201. Gtk::SpinButton m_SpinStartNum;
  202. Glib::RefPtr<Gtk::Adjustment> m_AdjStartNum, m_AdjZoom;
  203. Gtk::Stack m_StackModes;
  204. Gtk::ScrolledWindow m_ScrolledWindow;
  205. Gtk::IconView m_IconView;
  206. Gtk::ProgressBar m_ProgressBar;
  207. Gtk::Statusbar m_Statusbar;
  208. CellRendererFrame m_cell_frame;
  209. Gtk::CellRendererText m_cell_text;
  210. Gtk::CellRendererToggle m_cell_toggle;
  211. Gtk::Menu m_MenuPopup;
  212. Gtk::MenuItem m_MenuItemSelectAll, m_MenuItemSelectNone, m_MenuItemDelete, m_MenuItemOpenWith, m_MenuItemTerminal;
  213. struct ModelColumns : public Gtk::TreeModel::ColumnRecord {
  214. ModelColumns() { add(m_col_path); add(m_col_filename); add(m_col_pixbuf); add(m_col_checked); add(m_col_markup); add(m_col_time); add(m_col_info_str); }
  215. Gtk::TreeModelColumn<std::string> m_col_path, m_col_filename, m_col_markup, m_col_info_str;
  216. Gtk::TreeModelColumn<Glib::RefPtr<Gdk::Pixbuf>> m_col_pixbuf;
  217. Gtk::TreeModelColumn<bool> m_col_checked;
  218. Gtk::TreeModelColumn<long long> m_col_time;
  219. } m_Columns;
  220. Glib::RefPtr<Gtk::ListStore> m_RefListStore;
  221. std::string m_current_path;
  222. std::thread m_WorkerThread;
  223. std::atomic<bool> m_stop_flag{false};
  224. std::atomic<int> m_total_files{0}, m_processed_files{0};
  225. Glib::Dispatcher m_Dispatcher;
  226. std::mutex m_QueueMutex;
  227. std::deque<LoadedItem> m_ResultQueue;
  228. std::stack<UndoStep> m_UndoStack;
  229. void launch_terminal() {
  230. if (m_current_path.empty()) return;
  231. std::string cmd = "gnome-terminal --working-directory='" + m_current_path + "'";
  232. std::vector<Glib::RefPtr<Gio::File>> no_files;
  233. Gio::AppInfo::create_from_commandline(cmd, "Terminal", Gio::APP_INFO_CREATE_NONE)->launch(no_files);
  234. }
  235. bool on_window_key_press(GdkEventKey* event) {
  236. if ((event->state & GDK_CONTROL_MASK)) {
  237. if (event->keyval == GDK_KEY_o) { on_open_folder_clicked(); return true; }
  238. if (event->keyval == GDK_KEY_z) { on_undo_clicked(); return true; }
  239. if (event->keyval == GDK_KEY_e) { on_open_with_clicked(); return true; }
  240. if (event->keyval == GDK_KEY_t) { launch_terminal(); return true; }
  241. }
  242. if (event->keyval == GDK_KEY_Delete) { on_delete_selected(); return true; }
  243. if (event->keyval == GDK_KEY_Return) { on_rename_execute(); return true; }
  244. return false;
  245. }
  246. void on_item_activated(const Gtk::TreeModel::Path& path) {
  247. auto it = m_RefListStore->get_iter(path);
  248. if (it) Gio::AppInfo::launch_default_for_uri("file://" + (std::string)(*it)[m_Columns.m_col_path]);
  249. }
  250. void on_open_with_clicked() {
  251. for (auto row : m_RefListStore->children()) if (row[m_Columns.m_col_checked]) {
  252. Gtk::AppChooserDialog dialog(Gio::File::create_for_path((std::string)row[m_Columns.m_col_path]), *this);
  253. if (dialog.run() == Gtk::RESPONSE_OK) {
  254. std::vector<Glib::RefPtr<Gio::File>> files = { Gio::File::create_for_path((std::string)row[m_Columns.m_col_path]) };
  255. dialog.get_app_info()->launch(files);
  256. }
  257. break;
  258. }
  259. }
  260. void on_delete_selected() {
  261. Gtk::MessageDialog d(*this, "Move to Trash?", false, Gtk::MESSAGE_QUESTION, Gtk::BUTTONS_YES_NO);
  262. if (d.run() == Gtk::RESPONSE_YES) {
  263. for (auto it = m_RefListStore->children().begin(); it != m_RefListStore->children().end(); ) {
  264. if ((*it)[m_Columns.m_col_checked]) { Gio::File::create_for_path((std::string)(*it)[m_Columns.m_col_path])->trash(); it = m_RefListStore->erase(it); }
  265. else ++it;
  266. }
  267. }
  268. }
  269. void update_preview() {
  270. int mode = m_ComboMode.get_active_row_number(), start = m_SpinStartNum.get_value_as_int(), count = 0;
  271. std::string pat = m_EntryPattern.get_text();
  272. size_t hpos = pat.find('#');
  273. int pad = (hpos != std::string::npos) ? (pat.find_last_of('#') - hpos + 1) : 0;
  274. std::set<std::string> seen;
  275. for (auto row : m_RefListStore->children()) {
  276. std::string orig = row[m_Columns.m_col_filename], next = orig;
  277. if (mode == 2 && row[m_Columns.m_col_checked]) {
  278. std::string input = m_EntryDate.get_text();
  279. if (input.empty()) input = "1970-01-01 00:00:00";
  280. std::stringstream mu;
  281. mu << "<span font='9'><span weight='bold'>" << Glib::Markup::escape_text(orig) << "</span>\n";
  282. mu << "<span foreground='#e01b24' weight='bold'>DATE INJECTION: " << Glib::Markup::escape_text(input) << "</span></span>";
  283. row[m_Columns.m_col_markup] = mu.str();
  284. continue;
  285. }
  286. if (row[m_Columns.m_col_checked]) {
  287. if (mode == 0 && !pat.empty()) {
  288. std::string num = std::to_string(start + count++);
  289. while (num.length() < (size_t)pad) num = "0" + num;
  290. if (hpos != std::string::npos) { std::string t = pat; t.replace(hpos, pad, num); next = t + fs::path(orig).extension().string(); }
  291. else next = pat + "_" + num + fs::path(orig).extension().string();
  292. } else if (mode == 1 && !m_EntryFind.get_text().empty()) {
  293. size_t p = orig.find(m_EntryFind.get_text());
  294. if (p != std::string::npos) { next = orig; next.replace(p, m_EntryFind.get_text().length(), m_EntryReplace.get_text()); }
  295. }
  296. }
  297. std::stringstream mu; mu << "<span font='9'>";
  298. if (next != orig && seen.count(next)) mu << "<span foreground='red' weight='bold'>CONFLICT: " << Glib::Markup::escape_text(next) << "</span>";
  299. else if (next != orig) mu << "<span foreground='#3584e4' weight='bold'>" << Glib::Markup::escape_text(next) << "</span>";
  300. else mu << "<span weight='bold'>" << Glib::Markup::escape_text(orig) << "</span>";
  301. mu << "\n<span size='x-small' alpha='70%'>" << Glib::Markup::escape_text((std::string)row[m_Columns.m_col_info_str]) << "</span></span>";
  302. row[m_Columns.m_col_markup] = mu.str(); seen.insert(next);
  303. }
  304. }
  305. void on_rename_execute() {
  306. int mode = m_ComboMode.get_active_row_number();
  307. // Handle Date Application logic
  308. if (mode == 2) {
  309. std::string input = m_EntryDate.get_text();
  310. if (input.empty()) input = "1970-01-01 00:00:00";
  311. struct tm tm = {};
  312. if (!strptime(input.c_str(), "%Y-%m-%d %H:%M:%S", &tm)) {
  313. m_Statusbar.push("Error: Invalid date format. Use YYYY-MM-DD HH:MM:SS");
  314. return;
  315. }
  316. time_t target_epoch = mktime(&tm);
  317. char exif_buf[32];
  318. strftime(exif_buf, sizeof(exif_buf), "%Y:%m:%d %H:%M:%S", &tm);
  319. std::string exif_date = exif_buf;
  320. for (auto row : m_RefListStore->children()) {
  321. if (row[m_Columns.m_col_checked]) {
  322. std::string file_path = (std::string)row[m_Columns.m_col_path];
  323. // Rewrite EXIF via exiv2
  324. try {
  325. auto img = Exiv2::ImageFactory::open(file_path);
  326. if (img->good()) {
  327. img->readMetadata();
  328. Exiv2::ExifData &exifData = img->exifData();
  329. exifData["Exif.Image.DateTime"] = exif_date;
  330. exifData["Exif.Photo.DateTimeOriginal"] = exif_date;
  331. exifData["Exif.Photo.DateTimeDigitized"] = exif_date;
  332. img->setExifData(exifData);
  333. img->writeMetadata();
  334. }
  335. } catch (...) {
  336. // Fallthrough, skip EXIF if format doesn't support it or is violently corrupted
  337. }
  338. // Rewrite Filesystem mtime and atime
  339. struct utimbuf new_times;
  340. new_times.actime = target_epoch;
  341. new_times.modtime = target_epoch;
  342. utime(file_path.c_str(), &new_times);
  343. }
  344. }
  345. m_Statusbar.push("Dates Updated (EXIF + FS).");
  346. start_loading_folder(m_current_path); // Refresh UI to pull new metadata
  347. return;
  348. }
  349. // Standard Rename Logic
  350. UndoStep step; int start = m_SpinStartNum.get_value_as_int(), count = 0;
  351. std::string pat = m_EntryPattern.get_text();
  352. size_t hpos = pat.find('#');
  353. int pad = (hpos != std::string::npos) ? (pat.find_last_of('#') - hpos + 1) : 0;
  354. for (auto row : m_RefListStore->children()) if (row[m_Columns.m_col_checked]) {
  355. std::string orig = row[m_Columns.m_col_filename], next = orig;
  356. if (mode == 0) {
  357. std::string num = std::to_string(start + count++);
  358. while (num.length() < (size_t)pad) num = "0" + num;
  359. if (hpos != std::string::npos) { std::string t = pat; t.replace(hpos, pad, num); next = t + fs::path(orig).extension().string(); }
  360. else next = pat + "_" + num + fs::path(orig).extension().string();
  361. } else if (mode == 1) {
  362. size_t p = orig.find(m_EntryFind.get_text());
  363. if (p != std::string::npos) { next = orig; next.replace(p, m_EntryFind.get_text().length(), m_EntryReplace.get_text()); }
  364. }
  365. std::string final_name = next; int res = 1;
  366. while (fs::exists(fs::path((std::string)row[m_Columns.m_col_path]).parent_path() / final_name))
  367. final_name = fs::path(next).stem().string() + "_" + std::to_string(res++) + fs::path(next).extension().string();
  368. if (final_name != orig) {
  369. fs::rename((std::string)row[m_Columns.m_col_path], fs::path((std::string)row[m_Columns.m_col_path]).parent_path() / final_name);
  370. step.moves.push_back({(std::string)row[m_Columns.m_col_path], (fs::path((std::string)row[m_Columns.m_col_path]).parent_path() / final_name).string()});
  371. }
  372. }
  373. m_UndoStack.push(step); m_BtnUndo.set_sensitive(true);
  374. m_Statusbar.push("Rename Finished."); start_loading_folder(m_current_path);
  375. }
  376. void on_undo_clicked() {
  377. if (m_UndoStack.empty()) return;
  378. UndoStep last = m_UndoStack.top(); m_UndoStack.pop(); m_BtnUndo.set_sensitive(!m_UndoStack.empty());
  379. for (const auto& m : last.moves) if (fs::exists(m.new_path)) fs::rename(m.new_path, m.original_path);
  380. start_loading_folder(m_current_path);
  381. }
  382. void on_sort_changed() {
  383. int s = m_ComboSort.get_active_row_number();
  384. if (s == 0) m_RefListStore->set_sort_column(GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID, Gtk::SORT_ASCENDING);
  385. else if (s == 1) m_RefListStore->set_sort_column(m_Columns.m_col_filename, Gtk::SORT_ASCENDING);
  386. else m_RefListStore->set_sort_column(m_Columns.m_col_time, (s == 2 ? Gtk::SORT_ASCENDING : Gtk::SORT_DESCENDING));
  387. update_preview();
  388. }
  389. void on_worker_notification() {
  390. std::lock_guard<std::mutex> l(m_QueueMutex);
  391. while (!m_ResultQueue.empty()) {
  392. LoadedItem i = m_ResultQueue.front(); m_ResultQueue.pop_front();
  393. if (i.is_zoom_update) {
  394. auto it = m_RefListStore->get_iter(i.model_path);
  395. if (it) (*it)[m_Columns.m_col_pixbuf] = i.pixbuf;
  396. } else {
  397. auto r = *(m_RefListStore->append());
  398. r[m_Columns.m_col_path] = i.path; r[m_Columns.m_col_filename] = i.filename; r[m_Columns.m_col_pixbuf] = i.pixbuf; r[m_Columns.m_col_checked] = true;
  399. r[m_Columns.m_col_info_str] = i.sort_time_str + " | " + std::to_string(i.orig_w) + "x" + std::to_string(i.orig_h) + " | " + std::to_string(i.filesize/1024) + "KB";
  400. r[m_Columns.m_col_time] = i.sort_time_raw;
  401. }
  402. m_processed_files++;
  403. }
  404. if (m_total_files > 0) {
  405. m_ProgressBar.show(); m_ProgressBar.set_fraction(std::min(1.0, (double)m_processed_files / m_total_files));
  406. if (m_processed_files >= m_total_files) { m_ProgressBar.hide(); m_total_files = 0; m_processed_files = 0; }
  407. }
  408. update_preview();
  409. }
  410. void worker_thread_logic(std::vector<fs::path> paths, int sz, bool is_zoom) {
  411. for (size_t idx = 0; idx < paths.size(); ++idx) {
  412. if (m_stop_flag) break;
  413. LoadedItem item; item.path = paths[idx].string(); item.is_zoom_update = is_zoom;
  414. if (is_zoom) item.model_path = Gtk::TreePath(std::to_string(idx));
  415. try {
  416. auto pb = Gdk::Pixbuf::create_from_file(paths[idx].string());
  417. item.pixbuf = pb->scale_simple(sz, sz * pb->get_height() / pb->get_width(), Gdk::INTERP_BILINEAR);
  418. if (!is_zoom) {
  419. item.filename = paths[idx].filename().string(); item.orig_w = pb->get_width(); item.orig_h = pb->get_height(); item.filesize = fs::file_size(paths[idx]);
  420. try {
  421. auto img = Exiv2::ImageFactory::open(paths[idx].string()); img->readMetadata();
  422. auto tag = img->exifData().findKey(Exiv2::ExifKey("Exif.Photo.DateTimeDigitized"));
  423. if (tag != img->exifData().end()) {
  424. item.sort_time_str = tag->toString(); std::string c = item.sort_time_str;
  425. c.erase(std::remove_if(c.begin(), c.end(), [](char x){return !isdigit(x);}), c.end());
  426. item.sort_time_raw = std::stoll(c);
  427. } else throw std::runtime_error("");
  428. } catch (...) { item.sort_time_raw = fs::last_write_time(paths[idx]).time_since_epoch().count(); item.sort_time_str = "File Date"; }
  429. }
  430. { std::lock_guard<std::mutex> l(m_QueueMutex); m_ResultQueue.push_back(item); }
  431. m_Dispatcher.emit();
  432. } catch (...) {}
  433. }
  434. }
  435. void start_loading_folder(std::string p) {
  436. m_current_path = p; m_RefListStore->clear(); std::vector<fs::path> f;
  437. for (const auto& e : fs::directory_iterator(p)) {
  438. std::string ext = e.path().extension().string(); std::transform(ext.begin(), ext.end(), ext.begin(), ::tolower);
  439. if (ext == ".jpg" || ext == ".png" || ext == ".jpeg" || ext == ".webp" || ext == ".tif" || ext == ".tiff") f.push_back(e.path());
  440. }
  441. m_total_files = f.size(); m_processed_files = 0;
  442. if (m_WorkerThread.joinable()) { m_stop_flag = true; m_WorkerThread.join(); }
  443. m_stop_flag = false; m_WorkerThread = std::thread(&RenamerWindow::worker_thread_logic, this, f, (int)m_ScaleZoom.get_value(), false);
  444. }
  445. void on_size_changed() {
  446. if (m_current_path.empty()) return;
  447. int sz = (int)m_ScaleZoom.get_value();
  448. m_IconView.set_item_width(sz);
  449. std::vector<fs::path> f;
  450. for (auto r : m_RefListStore->children()) f.push_back(fs::path((std::string)r[m_Columns.m_col_path]));
  451. m_total_files = f.size(); m_processed_files = 0;
  452. if (m_WorkerThread.joinable()) { m_stop_flag = true; m_WorkerThread.join(); }
  453. m_stop_flag = false; m_WorkerThread = std::thread(&RenamerWindow::worker_thread_logic, this, f, sz, true);
  454. }
  455. void on_open_folder_clicked() {
  456. Gtk::FileChooserDialog d(*this, "Open", Gtk::FILE_CHOOSER_ACTION_SELECT_FOLDER);
  457. d.set_show_hidden(true);
  458. d.add_button("Cancel", Gtk::RESPONSE_CANCEL); d.add_button("Open", Gtk::RESPONSE_OK);
  459. if (d.run() == Gtk::RESPONSE_OK) start_loading_folder(d.get_filename());
  460. }
  461. void on_selection_change(bool s) { for (auto row : m_RefListStore->children()) row[m_Columns.m_col_checked] = s; update_preview(); }
  462. void on_mode_changed() {
  463. int mode = m_ComboMode.get_active_row_number();
  464. if (mode == 0) m_StackModes.set_visible_child("Pattern");
  465. else if (mode == 1) m_StackModes.set_visible_child("Replace");
  466. else if (mode == 2) m_StackModes.set_visible_child("Date");
  467. m_BtnRename.set_label(mode == 2 ? "Apply Target Date (EXIF+FS)" : "Apply New Names");
  468. update_preview();
  469. }
  470. void on_cell_toggled(const Glib::ustring& p) { auto it = m_RefListStore->get_iter(Gtk::TreePath(p)); if(it) { (*it)[m_Columns.m_col_checked] = !(*it)[m_Columns.m_col_checked]; update_preview(); } }
  471. bool on_iconview_button_press(GdkEventButton* e) { if (e->button == 3) { m_MenuPopup.popup(e->button, e->time); return true; } return false; }
  472. };
  473. int main(int argc, char *argv[]) {
  474. auto app = Gtk::Application::create(argc, argv, "org.gtkmm.renamer");
  475. RenamerWindow window; return app->run(window);
  476. }