main.cpp 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
  1. #include <gtkmm.h>
  2. #include <iostream>
  3. #include <filesystem>
  4. #include <vector>
  5. #include <algorithm>
  6. #include <string>
  7. #include <thread>
  8. #include <atomic>
  9. #include <mutex>
  10. #include <deque>
  11. #include <stack>
  12. #include <iomanip>
  13. #include <sstream>
  14. namespace fs = std::filesystem;
  15. // --- Natural Sort Helper ---
  16. struct NaturalSort {
  17. bool operator()(const std::string& a, const std::string& b) const {
  18. size_t i = 0, j = 0;
  19. while (i < a.length() && j < b.length()) {
  20. if (isdigit(a[i]) && isdigit(b[j])) {
  21. size_t start1 = i, start2 = j;
  22. while (i < a.length() && isdigit(a[i])) i++;
  23. while (j < b.length() && isdigit(b[j])) j++;
  24. try {
  25. long n1 = std::stol(a.substr(start1, i - start1));
  26. long n2 = std::stol(b.substr(start2, j - start2));
  27. if (n1 != n2) return n1 < n2;
  28. } catch (...) {
  29. if (a.substr(start1, i - start1) != b.substr(start2, j - start2))
  30. return a.substr(start1, i - start1) < b.substr(start2, j - start2);
  31. }
  32. } else {
  33. if (a[i] != b[j]) return a[i] < b[j];
  34. i++; j++;
  35. }
  36. }
  37. return a.length() < b.length();
  38. }
  39. };
  40. // --- Custom Renderer to draw a Frame ---
  41. class CellRendererFrame : public Gtk::CellRendererPixbuf {
  42. public:
  43. CellRendererFrame() {
  44. property_xpad() = 10;
  45. property_ypad() = 10;
  46. }
  47. protected:
  48. void render_vfunc(const Cairo::RefPtr<Cairo::Context>& cr,
  49. Gtk::Widget& widget,
  50. const Gdk::Rectangle& background_area,
  51. const Gdk::Rectangle& cell_area,
  52. Gtk::CellRendererState flags) override {
  53. cr->save();
  54. auto style = widget.get_style_context();
  55. Gdk::RGBA bg = style->get_background_color();
  56. double brightness = (bg.get_red() + bg.get_green() + bg.get_blue()) / 3.0;
  57. if (brightness > 0.5) cr->set_source_rgb(0.5, 0.5, 0.5);
  58. else cr->set_source_rgb(0.8, 0.8, 0.8);
  59. cr->set_line_width(2.0);
  60. cr->rectangle(cell_area.get_x() + 2, cell_area.get_y() + 2,
  61. cell_area.get_width() - 4, cell_area.get_height() - 4);
  62. cr->stroke();
  63. cr->restore();
  64. Gtk::CellRendererPixbuf::render_vfunc(cr, widget, background_area, cell_area, flags);
  65. }
  66. };
  67. struct RenameAction { std::string original_path; std::string new_path; };
  68. struct UndoStep { std::vector<RenameAction> moves; };
  69. struct LoadedItem {
  70. std::string path, filename, ext;
  71. fs::file_time_type last_write_time;
  72. Glib::RefPtr<Gdk::Pixbuf> pixbuf;
  73. int orig_w = 0, orig_h = 0;
  74. uintmax_t filesize = 0;
  75. bool is_update = false;
  76. Gtk::TreeModel::Path model_path;
  77. };
  78. class RenamerWindow : public Gtk::Window {
  79. public:
  80. RenamerWindow() : m_Dispatcher() {
  81. // --- VERSION 0.3.1 ---
  82. set_title("Simple Image Renamer 0.3.1");
  83. set_default_size(1280, 850);
  84. set_wmclass("simpleimagerenamer", "simpleimagerenamer");
  85. m_Dispatcher.connect(sigc::mem_fun(*this, &RenamerWindow::on_worker_notification));
  86. m_VBox.set_orientation(Gtk::ORIENTATION_VERTICAL);
  87. add(m_VBox);
  88. // --- Toolbar Top ---
  89. m_ToolbarTop.set_margin_top(5); m_ToolbarTop.set_margin_bottom(5);
  90. m_ToolbarTop.set_margin_left(10); m_ToolbarTop.set_margin_right(10);
  91. m_VBox.pack_start(m_ToolbarTop, Gtk::PACK_SHRINK);
  92. m_BtnOpen.set_label("Open Folder");
  93. m_BtnOpen.signal_clicked().connect(sigc::mem_fun(*this, &RenamerWindow::on_open_folder_clicked));
  94. m_ToolbarTop.pack_start(m_BtnOpen, Gtk::PACK_SHRINK, 5);
  95. m_BtnStopReload.set_label("Reload Folder");
  96. m_BtnStopReload.set_sensitive(false);
  97. m_BtnStopReload.signal_clicked().connect(sigc::mem_fun(*this, &RenamerWindow::on_stop_reload_clicked));
  98. m_ToolbarTop.pack_start(m_BtnStopReload, Gtk::PACK_SHRINK, 5);
  99. m_ToolbarTop.pack_start(*Gtk::manage(new Gtk::Label(" Sort By: ")), Gtk::PACK_SHRINK);
  100. m_ComboSort.append("Name (Natural)"); m_ComboSort.append("Oldest First"); m_ComboSort.append("Newest First"); m_ComboSort.append("Manual (Drag & Drop)");
  101. m_ComboSort.set_active(0);
  102. m_ComboSort.signal_changed().connect(sigc::mem_fun(*this, &RenamerWindow::on_sort_changed));
  103. m_ToolbarTop.pack_start(m_ComboSort, Gtk::PACK_SHRINK, 5);
  104. m_ToolbarTop.pack_start(*Gtk::manage(new Gtk::Label(" Zoom: ")), Gtk::PACK_SHRINK);
  105. m_ComboSize.append("Small"); m_ComboSize.append("Medium"); m_ComboSize.append("Large");
  106. m_ComboSize.set_active(1);
  107. m_ComboSize.signal_changed().connect(sigc::mem_fun(*this, &RenamerWindow::on_size_changed));
  108. m_ToolbarTop.pack_start(m_ComboSize, Gtk::PACK_SHRINK, 5);
  109. m_BtnUndo.set_label("Undo Last Rename");
  110. m_BtnUndo.set_sensitive(false);
  111. m_BtnUndo.signal_clicked().connect(sigc::mem_fun(*this, &RenamerWindow::on_undo_clicked));
  112. m_ToolbarTop.pack_end(m_BtnUndo, Gtk::PACK_SHRINK, 5);
  113. // --- Controls Frame ---
  114. m_FrameControls.set_shadow_type(Gtk::SHADOW_ETCHED_IN);
  115. m_VBox.pack_start(m_FrameControls, Gtk::PACK_SHRINK);
  116. m_ToolbarControls.set_margin_top(10); m_ToolbarControls.set_margin_bottom(10);
  117. m_FrameControls.add(m_ToolbarControls);
  118. m_ToolbarControls.pack_start(*Gtk::manage(new Gtk::Label(" Mode: ")), Gtk::PACK_SHRINK);
  119. m_ComboMode.append("Sequential Numbering"); m_ComboMode.append("Find & Replace");
  120. m_ComboMode.set_active(0);
  121. m_ComboMode.signal_changed().connect(sigc::mem_fun(*this, &RenamerWindow::on_mode_changed));
  122. m_ToolbarControls.pack_start(m_ComboMode, Gtk::PACK_SHRINK, 5);
  123. // Sequential Numbering Widgets
  124. m_BoxPattern.pack_start(*Gtk::manage(new Gtk::Label(" Pattern: ")), Gtk::PACK_SHRINK);
  125. m_EntryPattern.set_text("image_###");
  126. m_EntryPattern.signal_changed().connect(sigc::mem_fun(*this, &RenamerWindow::update_preview));
  127. m_BoxPattern.pack_start(m_EntryPattern, Gtk::PACK_EXPAND_WIDGET);
  128. m_BoxPattern.pack_start(*Gtk::manage(new Gtk::Label(" Start at: ")), Gtk::PACK_SHRINK);
  129. m_AdjStartNum = Gtk::Adjustment::create(1, 0, 100000, 1, 10, 0);
  130. m_SpinStartNum.set_adjustment(m_AdjStartNum);
  131. m_SpinStartNum.signal_value_changed().connect(sigc::mem_fun(*this, &RenamerWindow::update_preview));
  132. m_BoxPattern.pack_start(m_SpinStartNum, Gtk::PACK_SHRINK);
  133. m_StackModes.add(m_BoxPattern, "Pattern");
  134. // Find & Replace Widgets
  135. m_BoxReplace.pack_start(*Gtk::manage(new Gtk::Label(" Find Text: ")), Gtk::PACK_SHRINK);
  136. m_EntryFind.signal_changed().connect(sigc::mem_fun(*this, &RenamerWindow::update_preview));
  137. m_BoxReplace.pack_start(m_EntryFind, Gtk::PACK_EXPAND_WIDGET);
  138. m_BoxReplace.pack_start(*Gtk::manage(new Gtk::Label(" Replace With: ")), Gtk::PACK_SHRINK);
  139. m_EntryReplace.signal_changed().connect(sigc::mem_fun(*this, &RenamerWindow::update_preview));
  140. m_BoxReplace.pack_start(m_EntryReplace, Gtk::PACK_EXPAND_WIDGET);
  141. m_StackModes.add(m_BoxReplace, "Replace");
  142. m_ToolbarControls.pack_start(m_StackModes, Gtk::PACK_EXPAND_WIDGET, 5);
  143. m_BtnRename.set_label("Apply New Names");
  144. m_BtnRename.get_style_context()->add_class("suggested-action");
  145. m_BtnRename.signal_clicked().connect(sigc::mem_fun(*this, &RenamerWindow::on_rename_execute));
  146. m_ToolbarControls.pack_end(m_BtnRename, Gtk::PACK_SHRINK, 5);
  147. // --- IconView setup ---
  148. m_ScrolledWindow.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_ALWAYS);
  149. m_VBox.pack_start(m_ScrolledWindow);
  150. m_RefListStore = Gtk::ListStore::create(m_Columns);
  151. m_RefListStore->signal_row_inserted().connect([this](const Gtk::TreeModel::Path&, const Gtk::TreeModel::iterator&){ update_preview(); });
  152. m_IconView.set_model(m_RefListStore);
  153. m_IconView.pack_start(m_cell_frame, false);
  154. m_IconView.add_attribute(m_cell_frame, "pixbuf", m_Columns.m_col_pixbuf);
  155. m_IconView.pack_start(m_cell_toggle, false);
  156. m_IconView.add_attribute(m_cell_toggle, "active", m_Columns.m_col_checked);
  157. m_cell_toggle.property_activatable() = true;
  158. m_cell_toggle.signal_toggled().connect(sigc::mem_fun(*this, &RenamerWindow::on_cell_toggled));
  159. m_IconView.pack_start(m_cell_text, false);
  160. m_IconView.add_attribute(m_cell_text, "markup", m_Columns.m_col_markup);
  161. m_IconView.set_item_width(220);
  162. m_IconView.set_columns(-1);
  163. m_IconView.set_spacing(20);
  164. m_IconView.set_selection_mode(Gtk::SELECTION_NONE);
  165. m_IconView.set_reorderable(false);
  166. // --- Context Menu Setup (Right Click) ---
  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(m_MenuSeparator);
  174. m_MenuItemDelete.set_label("Delete Selected Pictures");
  175. auto delete_img = Gtk::manage(new Gtk::Image());
  176. delete_img->set_from_icon_name("user-trash", Gtk::ICON_SIZE_MENU);
  177. m_MenuItemDelete.set_image(*delete_img);
  178. m_MenuItemDelete.set_always_show_image(true);
  179. m_MenuItemDelete.signal_activate().connect(sigc::mem_fun(*this, &RenamerWindow::on_delete_selected));
  180. m_MenuPopup.append(m_MenuItemDelete);
  181. m_MenuPopup.show_all();
  182. m_MenuPopup.attach_to_widget(m_IconView);
  183. m_IconView.add_events(Gdk::BUTTON_PRESS_MASK);
  184. m_IconView.signal_button_press_event().connect(sigc::mem_fun(*this, &RenamerWindow::on_iconview_button_press), false);
  185. m_ScrolledWindow.add(m_IconView);
  186. m_VBox.pack_start(m_ProgressBar, Gtk::PACK_SHRINK);
  187. m_VBox.pack_end(m_Statusbar, Gtk::PACK_SHRINK);
  188. std::vector<Gtk::TargetEntry> listTargets = { Gtk::TargetEntry("text/uri-list") };
  189. this->drag_dest_set(listTargets, Gtk::DEST_DEFAULT_ALL, Gdk::ACTION_COPY);
  190. this->signal_drag_data_received().connect(sigc::mem_fun(*this, &RenamerWindow::on_drag_data_received));
  191. show_all_children();
  192. m_ProgressBar.hide();
  193. }
  194. ~RenamerWindow() { m_stop_flag = true; if (m_WorkerThread.joinable()) m_WorkerThread.join(); }
  195. protected:
  196. Gtk::Box m_VBox, m_ToolbarTop, m_ToolbarControls, m_BoxPattern, m_BoxReplace;
  197. Gtk::Frame m_FrameControls;
  198. Gtk::Button m_BtnOpen, m_BtnRename, m_BtnUndo, m_BtnStopReload;
  199. Gtk::Entry m_EntryPattern, m_EntryFind, m_EntryReplace;
  200. Gtk::ComboBoxText m_ComboSize, m_ComboSort, m_ComboMode;
  201. Gtk::SpinButton m_SpinStartNum;
  202. Glib::RefPtr<Gtk::Adjustment> m_AdjStartNum;
  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. // --- Menu Members ---
  212. Gtk::Menu m_MenuPopup;
  213. Gtk::MenuItem m_MenuItemSelectAll;
  214. Gtk::MenuItem m_MenuItemSelectNone;
  215. Gtk::SeparatorMenuItem m_MenuSeparator;
  216. Gtk::ImageMenuItem m_MenuItemDelete;
  217. struct ModelColumns : public Gtk::TreeModel::ColumnRecord {
  218. 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); }
  219. Gtk::TreeModelColumn<std::string> m_col_path, m_col_filename, m_col_markup, m_col_info_str;
  220. Gtk::TreeModelColumn<Glib::RefPtr<Gdk::Pixbuf>> m_col_pixbuf;
  221. Gtk::TreeModelColumn<bool> m_col_checked;
  222. Gtk::TreeModelColumn<long long> m_col_time;
  223. } m_Columns;
  224. Glib::RefPtr<Gtk::ListStore> m_RefListStore;
  225. std::string m_current_path;
  226. std::thread m_WorkerThread;
  227. std::atomic<bool> m_stop_flag{false}, m_is_loading{false};
  228. std::atomic<int> m_total_files{0}, m_processed_files{0};
  229. Glib::Dispatcher m_Dispatcher;
  230. std::mutex m_QueueMutex;
  231. std::deque<LoadedItem> m_ResultQueue;
  232. std::stack<UndoStep> m_UndoStack;
  233. std::string format_size(uintmax_t bytes) {
  234. double sz = (double)bytes;
  235. const char* units[] = {"B", "KB", "MB", "GB"};
  236. int i = 0; while (sz >= 1024 && i < 3) { sz /= 1024; i++; }
  237. std::stringstream ss; ss << std::fixed << std::setprecision(1) << sz << " " << units[i];
  238. return ss.str();
  239. }
  240. void on_mode_changed() {
  241. m_StackModes.set_visible_child(m_ComboMode.get_active_row_number() == 0 ? "Pattern" : "Replace");
  242. update_preview();
  243. }
  244. void on_cell_toggled(const Glib::ustring& p) {
  245. auto it = m_RefListStore->get_iter(Gtk::TreePath(p));
  246. if(it) { (*it)[m_Columns.m_col_checked] = !(*it)[m_Columns.m_col_checked]; update_preview(); }
  247. }
  248. void on_selection_change(bool select_all) {
  249. if (!m_RefListStore) return;
  250. for (auto row : m_RefListStore->children()) {
  251. row[m_Columns.m_col_checked] = select_all;
  252. }
  253. update_preview();
  254. }
  255. bool on_iconview_button_press(GdkEventButton* event) {
  256. if (event->type == GDK_BUTTON_PRESS && event->button == 3) {
  257. m_MenuPopup.popup(event->button, event->time);
  258. return true;
  259. }
  260. return false;
  261. }
  262. void on_delete_selected() {
  263. int count = 0;
  264. for (auto row : m_RefListStore->children()) if (row[m_Columns.m_col_checked]) count++;
  265. if (count == 0) return;
  266. Gtk::MessageDialog dialog(*this, "Confirm Deletion", false, Gtk::MESSAGE_QUESTION, Gtk::BUTTONS_YES_NO);
  267. dialog.set_secondary_text("Are you sure you want to permanently delete " + std::to_string(count) + " selected pictures?");
  268. if (dialog.run() != Gtk::RESPONSE_YES) return;
  269. for (auto it = m_RefListStore->children().begin(); it != m_RefListStore->children().end(); ) {
  270. if ((*it)[m_Columns.m_col_checked]) {
  271. try {
  272. fs::remove((std::string)(*it)[m_Columns.m_col_path]);
  273. it = m_RefListStore->erase(it);
  274. } catch (...) { ++it; }
  275. } else ++it;
  276. }
  277. update_preview();
  278. }
  279. void on_stop_reload_clicked() {
  280. if (m_is_loading) m_stop_flag = true;
  281. else if (!m_current_path.empty()) start_loading_folder(m_current_path);
  282. }
  283. void update_preview() {
  284. if (!m_RefListStore) return;
  285. int mode = m_ComboMode.get_active_row_number();
  286. std::string pattern = m_EntryPattern.get_text();
  287. int start_num = m_SpinStartNum.get_value_as_int();
  288. size_t hash_pos = pattern.find('#');
  289. int pad = (hash_pos != std::string::npos) ? (pattern.find_last_of('#') - hash_pos + 1) : 0;
  290. int count = 0;
  291. for (auto row : m_RefListStore->children()) {
  292. std::string original = row[m_Columns.m_col_filename], meta = row[m_Columns.m_col_info_str], new_name = original;
  293. if (row[m_Columns.m_col_checked]) {
  294. if (mode == 0 && !pattern.empty()) {
  295. std::string num = std::to_string(start_num + count);
  296. while (num.length() < (size_t)pad) num = "0" + num;
  297. if (hash_pos != std::string::npos) {
  298. std::string t = pattern; t.replace(hash_pos, pad, num);
  299. new_name = t + fs::path(original).extension().string();
  300. } else new_name = pattern + "_" + num + fs::path(original).extension().string();
  301. count++;
  302. } else if (mode == 1 && !m_EntryFind.get_text().empty()) {
  303. size_t pos = original.find(m_EntryFind.get_text());
  304. if (pos != std::string::npos) { new_name = original; new_name.replace(pos, m_EntryFind.get_text().length(), m_EntryReplace.get_text()); }
  305. }
  306. }
  307. std::stringstream mu; mu << "<span font='9'>";
  308. if (new_name != original) mu << "<span size='small' alpha='60%'>" << Glib::Markup::escape_text(original) << "</span>\n<span foreground='#3584e4' weight='bold'>" << Glib::Markup::escape_text(new_name) << "</span>";
  309. else mu << "<span weight='bold'>" << Glib::Markup::escape_text(original) << "</span>";
  310. mu << "\n<span size='x-small' alpha='70%'>" << Glib::Markup::escape_text(meta) << "</span></span>";
  311. row[m_Columns.m_col_markup] = mu.str();
  312. }
  313. }
  314. void on_rename_execute() {
  315. UndoStep step;
  316. int mode = m_ComboMode.get_active_row_number(), start = m_SpinStartNum.get_value_as_int();
  317. std::string pat = m_EntryPattern.get_text();
  318. size_t hpos = pat.find('#');
  319. int pad = (hpos != std::string::npos && mode == 0) ? (pat.find_last_of('#') - hpos + 1) : 0;
  320. int count = 0;
  321. for (auto row : m_RefListStore->children()) {
  322. if (!row[m_Columns.m_col_checked]) continue;
  323. std::string orig = row[m_Columns.m_col_filename], next = orig;
  324. if (mode == 0) {
  325. std::string num = std::to_string(start + count);
  326. while (num.length() < (size_t)pad) num = "0" + num;
  327. if (hpos != std::string::npos) { std::string t = pat; t.replace(hpos, pad, num); next = t + fs::path(orig).extension().string(); }
  328. else next = pat + "_" + num + fs::path(orig).extension().string();
  329. count++;
  330. } else if (mode == 1 && !m_EntryFind.get_text().empty()) {
  331. size_t p = orig.find(m_EntryFind.get_text());
  332. if (p != std::string::npos) { next = orig; next.replace(p, m_EntryFind.get_text().length(), m_EntryReplace.get_text()); }
  333. }
  334. if (next != orig) step.moves.push_back({(std::string)row[m_Columns.m_col_path], (fs::path((std::string)row[m_Columns.m_col_path]).parent_path() / next).string()});
  335. }
  336. if (step.moves.empty()) return;
  337. int ti = 0; std::vector<std::pair<std::string, std::string>> stage;
  338. for (auto& m : step.moves) {
  339. fs::path t = fs::path(m.original_path).parent_path() / ("__tmp_" + std::to_string(ti++) + fs::path(m.original_path).extension().string());
  340. try { fs::rename(m.original_path, t); stage.push_back({t.string(), m.new_path}); } catch (...) {}
  341. }
  342. for (auto& s : stage) try { fs::rename(s.first, s.second); } catch (...) {}
  343. m_UndoStack.push(step); m_BtnUndo.set_sensitive(true); start_loading_folder(m_current_path);
  344. }
  345. void on_undo_clicked() {
  346. if (m_UndoStack.empty()) return;
  347. UndoStep last = m_UndoStack.top(); m_UndoStack.pop(); m_BtnUndo.set_sensitive(!m_UndoStack.empty());
  348. int ti = 0; std::vector<std::pair<std::string, std::string>> stage;
  349. for (const auto& m : last.moves) {
  350. if (fs::exists(m.new_path)) {
  351. fs::path t = fs::path(m.new_path).parent_path() / ("__u_" + std::to_string(ti++) + fs::path(m.new_path).extension().string());
  352. try { fs::rename(m.new_path, t); stage.push_back({t.string(), m.original_path}); } catch (...) {}
  353. }
  354. }
  355. for (auto& s : stage) try { fs::rename(s.first, s.second); } catch(...) {}
  356. start_loading_folder(m_current_path);
  357. }
  358. void on_sort_changed() {
  359. if (!m_RefListStore || m_RefListStore->children().empty()) return;
  360. int mode = m_ComboSort.get_active_row_number();
  361. if (mode == 3) {
  362. m_IconView.set_selection_mode(Gtk::SELECTION_MULTIPLE);
  363. m_IconView.set_reorderable(true);
  364. m_RefListStore->set_sort_column(GTK_TREE_SORTABLE_UNSORTED_SORT_COLUMN_ID, Gtk::SORT_ASCENDING);
  365. } else {
  366. m_IconView.set_selection_mode(Gtk::SELECTION_NONE);
  367. m_IconView.set_reorderable(false);
  368. if (mode == 0) {
  369. m_RefListStore->set_sort_func(m_Columns.m_col_filename, [this](const Gtk::TreeModel::iterator& a, const Gtk::TreeModel::iterator& b){
  370. return NaturalSort()((*a)[m_Columns.m_col_filename], (*b)[m_Columns.m_col_filename]) ? -1 : 1;
  371. });
  372. m_RefListStore->set_sort_column(m_Columns.m_col_filename, Gtk::SORT_ASCENDING);
  373. } else {
  374. m_RefListStore->set_sort_func(m_Columns.m_col_time, [mode, this](const Gtk::TreeModel::iterator& a, const Gtk::TreeModel::iterator& b){
  375. long long ta = (*a)[m_Columns.m_col_time], tb = (*b)[m_Columns.m_col_time];
  376. if (ta == tb) return 0;
  377. return (mode == 1) ? (ta < tb ? -1 : 1) : (ta > tb ? -1 : 1);
  378. });
  379. m_RefListStore->set_sort_column(m_Columns.m_col_time, Gtk::SORT_ASCENDING);
  380. }
  381. }
  382. Glib::signal_idle().connect_once([this](){ update_preview(); });
  383. }
  384. void worker_thread_logic(std::vector<fs::path> paths, int sz, bool is_zoom_only) {
  385. m_is_loading = true; m_total_files = paths.size(); m_processed_files = 0;
  386. for (size_t idx = 0; idx < paths.size(); ++idx) {
  387. if (m_stop_flag) break;
  388. const auto& p = paths[idx];
  389. LoadedItem item; item.path = p.string(); item.is_update = is_zoom_only;
  390. if (is_zoom_only) item.model_path = Gtk::TreePath(std::to_string(idx));
  391. try {
  392. auto pb = Gdk::Pixbuf::create_from_file(p.string());
  393. float r = (float)pb->get_width() / pb->get_height();
  394. int dw = (r > 1) ? sz : sz * r, dh = (r > 1) ? sz / r : sz;
  395. item.pixbuf = pb->scale_simple(dw, dh, Gdk::INTERP_BILINEAR);
  396. if (!is_zoom_only) {
  397. item.filename = p.filename().string();
  398. item.ext = p.extension().string(); if (!item.ext.empty() && item.ext[0] == '.') item.ext = item.ext.substr(1);
  399. std::transform(item.ext.begin(), item.ext.end(), item.ext.begin(), ::toupper);
  400. item.orig_w = pb->get_width(); item.orig_h = pb->get_height();
  401. item.last_write_time = fs::last_write_time(p); item.filesize = fs::file_size(p);
  402. }
  403. { std::lock_guard<std::mutex> l(m_QueueMutex); m_ResultQueue.push_back(item); }
  404. m_processed_files++; m_Dispatcher.emit();
  405. std::this_thread::sleep_for(std::chrono::microseconds(50));
  406. } catch (...) { m_processed_files++; }
  407. }
  408. m_is_loading = false; m_total_files = 0; m_Dispatcher.emit();
  409. }
  410. void on_worker_notification() {
  411. std::lock_guard<std::mutex> l(m_QueueMutex);
  412. while (!m_ResultQueue.empty()) {
  413. LoadedItem i = m_ResultQueue.front(); m_ResultQueue.pop_front();
  414. if (i.is_update) {
  415. auto it = m_RefListStore->get_iter(i.model_path);
  416. if (it) (*it)[m_Columns.m_col_pixbuf] = i.pixbuf;
  417. } else {
  418. auto r = *(m_RefListStore->append());
  419. r[m_Columns.m_col_path] = i.path; r[m_Columns.m_col_filename] = i.filename;
  420. r[m_Columns.m_col_pixbuf] = i.pixbuf; r[m_Columns.m_col_checked] = true;
  421. auto sctp = std::chrono::time_point_cast<std::chrono::system_clock::duration>(i.last_write_time - fs::file_time_type::clock::now() + std::chrono::system_clock::now());
  422. std::time_t tt = std::chrono::system_clock::to_time_t(sctp);
  423. std::stringstream ss; ss << std::put_time(std::localtime(&tt), "%H:%M %d-%b-%Y");
  424. r[m_Columns.m_col_info_str] = i.ext + " | " + std::to_string(i.orig_w) + "x" + std::to_string(i.orig_h) + " | " + format_size(i.filesize) + " | " + ss.str();
  425. r[m_Columns.m_col_time] = sctp.time_since_epoch().count();
  426. }
  427. }
  428. if (m_total_files > 0) {
  429. m_ProgressBar.set_fraction(std::min(1.0, (double)m_processed_files / m_total_files));
  430. m_ProgressBar.show();
  431. m_BtnStopReload.set_label("Stop Loading"); m_BtnStopReload.set_sensitive(true);
  432. } else {
  433. m_ProgressBar.hide();
  434. m_BtnStopReload.set_label("Reload Folder");
  435. m_BtnStopReload.set_sensitive(!m_current_path.empty());
  436. }
  437. if (m_total_files == 0) update_preview();
  438. }
  439. void on_size_changed() {
  440. if (m_RefListStore->children().empty()) return;
  441. m_stop_flag = true; if (m_WorkerThread.joinable()) m_WorkerThread.join();
  442. int sz = (m_ComboSize.get_active_row_number() == 0) ? 100 : (m_ComboSize.get_active_row_number() == 2 ? 450 : 220);
  443. m_IconView.set_item_width(sz);
  444. std::vector<fs::path> paths;
  445. for(auto r : m_RefListStore->children()) paths.push_back(fs::path((std::string)r[m_Columns.m_col_path]));
  446. m_stop_flag = false;
  447. m_WorkerThread = std::thread(&RenamerWindow::worker_thread_logic, this, paths, sz, true);
  448. }
  449. void start_loading_folder(std::string p) {
  450. m_current_path = p; m_stop_flag = true; if (m_WorkerThread.joinable()) m_WorkerThread.join();
  451. m_RefListStore->clear();
  452. std::vector<fs::path> f;
  453. try {
  454. for (const auto& e : fs::directory_iterator(p)) {
  455. std::string x = e.path().extension().string(); std::transform(x.begin(), x.end(), x.begin(), ::tolower);
  456. if (x == ".jpg" || x == ".png" || x == ".jpeg" || x == ".gif" || x == ".bmp") f.push_back(e.path());
  457. }
  458. } catch (...) {}
  459. std::sort(f.begin(), f.end(), [](const fs::path& a, const fs::path& b){ return NaturalSort()(a.filename().string(), b.filename().string()); });
  460. int sz = (m_ComboSize.get_active_row_number() == 0) ? 100 : (m_ComboSize.get_active_row_number() == 2 ? 450 : 220);
  461. m_IconView.set_item_width(sz); m_stop_flag = false;
  462. m_WorkerThread = std::thread(&RenamerWindow::worker_thread_logic, this, f, sz, false);
  463. m_BtnStopReload.set_sensitive(true);
  464. }
  465. void on_open_folder_clicked() {
  466. Gtk::FileChooserDialog d(*this, "Select Image Folder", Gtk::FILE_CHOOSER_ACTION_SELECT_FOLDER);
  467. d.add_button("Cancel", Gtk::RESPONSE_CANCEL); d.add_button("Select", Gtk::RESPONSE_OK);
  468. if (d.run() == Gtk::RESPONSE_OK) start_loading_folder(d.get_filename());
  469. }
  470. void on_drag_data_received(const Glib::RefPtr<Gdk::DragContext>& c, int, int, const Gtk::SelectionData& s, guint, guint t) {
  471. auto uris = s.get_uris();
  472. if (!uris.empty()) {
  473. std::string f = Glib::filename_from_uri(uris[0]);
  474. start_loading_folder(fs::is_directory(f) ? f : fs::path(f).parent_path().string());
  475. c->drag_finish(true, false, t);
  476. } else c->drag_finish(false, false, t);
  477. }
  478. };
  479. int main(int argc, char *argv[]) {
  480. auto app = Gtk::Application::create(argc, argv, "org.gtkmm.renamer_v03_1");
  481. RenamerWindow window;
  482. return app->run(window);
  483. }