|
|
@@ -1,4 +1,5 @@
|
|
|
#include <gtkmm.h>
|
|
|
+#include <giomm.h>
|
|
|
#include <iostream>
|
|
|
#include <filesystem>
|
|
|
#include <vector>
|
|
|
@@ -11,6 +12,7 @@
|
|
|
#include <stack>
|
|
|
#include <iomanip>
|
|
|
#include <sstream>
|
|
|
+#include <set>
|
|
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
|
|
@@ -87,7 +89,6 @@ public:
|
|
|
RenamerWindow() : m_Dispatcher() {
|
|
|
set_title("Simple Image Renamer");
|
|
|
set_default_size(1280, 850);
|
|
|
-
|
|
|
set_wmclass("simpleimagerenamer", "simpleimagerenamer");
|
|
|
|
|
|
m_Dispatcher.connect(sigc::mem_fun(*this, &RenamerWindow::on_worker_notification));
|
|
|
@@ -121,7 +122,7 @@ public:
|
|
|
m_ComboSize.signal_changed().connect(sigc::mem_fun(*this, &RenamerWindow::on_size_changed));
|
|
|
m_ToolbarTop.pack_start(m_ComboSize, Gtk::PACK_SHRINK, 5);
|
|
|
|
|
|
- m_BtnUndo.set_label("Undo Last Rename");
|
|
|
+ m_BtnUndo.set_label("Undo");
|
|
|
m_BtnUndo.set_sensitive(false);
|
|
|
m_BtnUndo.signal_clicked().connect(sigc::mem_fun(*this, &RenamerWindow::on_undo_clicked));
|
|
|
m_ToolbarTop.pack_end(m_BtnUndo, Gtk::PACK_SHRINK, 5);
|
|
|
@@ -188,6 +189,7 @@ public:
|
|
|
|
|
|
m_IconView.set_selection_mode(Gtk::SELECTION_NONE);
|
|
|
m_IconView.set_reorderable(false);
|
|
|
+ m_IconView.signal_item_activated().connect(sigc::mem_fun(*this, &RenamerWindow::on_item_activated));
|
|
|
|
|
|
// --- Context Menu Setup (Right Click) ---
|
|
|
m_MenuItemSelectAll.set_label("Select All");
|
|
|
@@ -200,7 +202,7 @@ public:
|
|
|
|
|
|
m_MenuPopup.append(m_MenuSeparator);
|
|
|
|
|
|
- m_MenuItemDelete.set_label("Delete Selected Pictures");
|
|
|
+ m_MenuItemDelete.set_label("Move to Trash");
|
|
|
auto delete_img = Gtk::manage(new Gtk::Image());
|
|
|
delete_img->set_from_icon_name("user-trash", Gtk::ICON_SIZE_MENU);
|
|
|
m_MenuItemDelete.set_image(*delete_img);
|
|
|
@@ -218,10 +220,14 @@ public:
|
|
|
m_VBox.pack_start(m_ProgressBar, Gtk::PACK_SHRINK);
|
|
|
m_VBox.pack_end(m_Statusbar, Gtk::PACK_SHRINK);
|
|
|
|
|
|
+ // --- Drag and Drop ---
|
|
|
std::vector<Gtk::TargetEntry> listTargets = { Gtk::TargetEntry("text/uri-list") };
|
|
|
this->drag_dest_set(listTargets, Gtk::DEST_DEFAULT_ALL, Gdk::ACTION_COPY);
|
|
|
this->signal_drag_data_received().connect(sigc::mem_fun(*this, &RenamerWindow::on_drag_data_received));
|
|
|
|
|
|
+ // --- Keyboard Shortcuts (The "Hidden" Cockpit Controls) ---
|
|
|
+ this->signal_key_press_event().connect(sigc::mem_fun(*this, &RenamerWindow::on_window_key_press), false);
|
|
|
+
|
|
|
show_all_children();
|
|
|
m_ProgressBar.hide();
|
|
|
}
|
|
|
@@ -245,7 +251,6 @@ protected:
|
|
|
Gtk::CellRendererText m_cell_text;
|
|
|
Gtk::CellRendererToggle m_cell_toggle;
|
|
|
|
|
|
- // --- Menu Members ---
|
|
|
Gtk::Menu m_MenuPopup;
|
|
|
Gtk::MenuItem m_MenuItemSelectAll;
|
|
|
Gtk::MenuItem m_MenuItemSelectNone;
|
|
|
@@ -270,6 +275,30 @@ protected:
|
|
|
std::deque<LoadedItem> m_ResultQueue;
|
|
|
std::stack<UndoStep> m_UndoStack;
|
|
|
|
|
|
+ // --- Key Press Handler ---
|
|
|
+ bool on_window_key_press(GdkEventKey* event) {
|
|
|
+ if ((event->state & GDK_CONTROL_MASK)) {
|
|
|
+ if (event->keyval == GDK_KEY_o || event->keyval == GDK_KEY_O) { on_open_folder_clicked(); return true; }
|
|
|
+ if (event->keyval == GDK_KEY_z || event->keyval == GDK_KEY_Z) { on_undo_clicked(); return true; }
|
|
|
+ }
|
|
|
+ if (event->keyval == GDK_KEY_Delete) { on_delete_selected(); return true; }
|
|
|
+ if (event->keyval == GDK_KEY_Return || event->keyval == GDK_KEY_KP_Enter) { on_rename_execute(); return true; }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // --- Quick View (Double Click) ---
|
|
|
+ void on_item_activated(const Gtk::TreeModel::Path& path) {
|
|
|
+ auto it = m_RefListStore->get_iter(path);
|
|
|
+ if (it) {
|
|
|
+ std::string file_path = (*it)[m_Columns.m_col_path];
|
|
|
+ try {
|
|
|
+ Gio::AppInfo::launch_default_for_uri("file://" + file_path);
|
|
|
+ } catch (const Glib::Error& ex) {
|
|
|
+ std::cerr << "Failed to open image: " << ex.what() << std::endl;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
std::string format_size(uintmax_t bytes) {
|
|
|
double sz = (double)bytes;
|
|
|
const char* units[] = {"B", "KB", "MB", "GB"};
|
|
|
@@ -309,15 +338,17 @@ protected:
|
|
|
for (auto row : m_RefListStore->children()) if (row[m_Columns.m_col_checked]) count++;
|
|
|
if (count == 0) return;
|
|
|
|
|
|
- Gtk::MessageDialog dialog(*this, "Confirm Deletion", false, Gtk::MESSAGE_QUESTION, Gtk::BUTTONS_YES_NO);
|
|
|
- dialog.set_secondary_text("Are you sure you want to permanently delete " + std::to_string(count) + " selected pictures?");
|
|
|
+ Gtk::MessageDialog dialog(*this, "Move to Trash?", false, Gtk::MESSAGE_QUESTION, Gtk::BUTTONS_YES_NO);
|
|
|
+ dialog.set_secondary_text("Move " + std::to_string(count) + " selected pictures to the system trash?");
|
|
|
if (dialog.run() != Gtk::RESPONSE_YES) return;
|
|
|
|
|
|
for (auto it = m_RefListStore->children().begin(); it != m_RefListStore->children().end(); ) {
|
|
|
if ((*it)[m_Columns.m_col_checked]) {
|
|
|
try {
|
|
|
- fs::remove((std::string)(*it)[m_Columns.m_col_path]);
|
|
|
- it = m_RefListStore->erase(it);
|
|
|
+ auto file = Gio::File::create_for_path((std::string)(*it)[m_Columns.m_col_path]);
|
|
|
+ if (file->trash()) {
|
|
|
+ it = m_RefListStore->erase(it);
|
|
|
+ } else { ++it; }
|
|
|
} catch (...) { ++it; }
|
|
|
} else ++it;
|
|
|
}
|
|
|
@@ -337,8 +368,14 @@ protected:
|
|
|
size_t hash_pos = pattern.find('#');
|
|
|
int pad = (hash_pos != std::string::npos) ? (pattern.find_last_of('#') - hash_pos + 1) : 0;
|
|
|
int count = 0;
|
|
|
+
|
|
|
+ std::set<std::string> existing_names;
|
|
|
+ for (auto row : m_RefListStore->children()) existing_names.insert((std::string)row[m_Columns.m_col_filename]);
|
|
|
+
|
|
|
for (auto row : m_RefListStore->children()) {
|
|
|
std::string original = row[m_Columns.m_col_filename], meta = row[m_Columns.m_col_info_str], new_name = original;
|
|
|
+ bool conflict = false;
|
|
|
+
|
|
|
if (row[m_Columns.m_col_checked]) {
|
|
|
if (mode == 0 && !pattern.empty()) {
|
|
|
std::string num = std::to_string(start_num + count);
|
|
|
@@ -352,10 +389,15 @@ protected:
|
|
|
size_t pos = original.find(m_EntryFind.get_text());
|
|
|
if (pos != std::string::npos) { new_name = original; new_name.replace(pos, m_EntryFind.get_text().length(), m_EntryReplace.get_text()); }
|
|
|
}
|
|
|
+ if (new_name != original && existing_names.count(new_name)) conflict = true;
|
|
|
}
|
|
|
+
|
|
|
std::stringstream mu; mu << "<span font='9'>";
|
|
|
- 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>";
|
|
|
- else mu << "<span weight='bold'>" << Glib::Markup::escape_text(original) << "</span>";
|
|
|
+ if (new_name != original) {
|
|
|
+ mu << "<span size='small' alpha='60%'>" << Glib::Markup::escape_text(original) << "</span>\n";
|
|
|
+ if (conflict) mu << "<span foreground='red' weight='bold'>CONFLICT: " << Glib::Markup::escape_text(new_name) << "</span>";
|
|
|
+ else mu << "<span foreground='#3584e4' weight='bold'>" << Glib::Markup::escape_text(new_name) << "</span>";
|
|
|
+ } else mu << "<span weight='bold'>" << Glib::Markup::escape_text(original) << "</span>";
|
|
|
mu << "\n<span size='x-small' alpha='70%'>" << Glib::Markup::escape_text(meta) << "</span></span>";
|
|
|
row[m_Columns.m_col_markup] = mu.str();
|
|
|
}
|
|
|
@@ -381,29 +423,23 @@ protected:
|
|
|
size_t p = orig.find(m_EntryFind.get_text());
|
|
|
if (p != std::string::npos) { next = orig; next.replace(p, m_EntryFind.get_text().length(), m_EntryReplace.get_text()); }
|
|
|
}
|
|
|
- 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()});
|
|
|
+ if (next != orig) {
|
|
|
+ if (fs::exists(fs::path((std::string)row[m_Columns.m_col_path]).parent_path() / next)) {
|
|
|
+ std::cerr << "Rename skipped: Conflict with " << next << std::endl;
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ 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()});
|
|
|
+ }
|
|
|
}
|
|
|
if (step.moves.empty()) return;
|
|
|
- int ti = 0; std::vector<std::pair<std::string, std::string>> stage;
|
|
|
- for (auto& m : step.moves) {
|
|
|
- fs::path t = fs::path(m.original_path).parent_path() / ("__tmp_" + std::to_string(ti++) + fs::path(m.original_path).extension().string());
|
|
|
- try { fs::rename(m.original_path, t); stage.push_back({t.string(), m.new_path}); } catch (...) {}
|
|
|
- }
|
|
|
- for (auto& s : stage) try { fs::rename(s.first, s.second); } catch (...) {}
|
|
|
+ for (auto& m : step.moves) try { fs::rename(m.original_path, m.new_path); } catch (...) {}
|
|
|
m_UndoStack.push(step); m_BtnUndo.set_sensitive(true); start_loading_folder(m_current_path);
|
|
|
}
|
|
|
|
|
|
void on_undo_clicked() {
|
|
|
if (m_UndoStack.empty()) return;
|
|
|
UndoStep last = m_UndoStack.top(); m_UndoStack.pop(); m_BtnUndo.set_sensitive(!m_UndoStack.empty());
|
|
|
- int ti = 0; std::vector<std::pair<std::string, std::string>> stage;
|
|
|
- for (const auto& m : last.moves) {
|
|
|
- if (fs::exists(m.new_path)) {
|
|
|
- fs::path t = fs::path(m.new_path).parent_path() / ("__u_" + std::to_string(ti++) + fs::path(m.new_path).extension().string());
|
|
|
- try { fs::rename(m.new_path, t); stage.push_back({t.string(), m.original_path}); } catch (...) {}
|
|
|
- }
|
|
|
- }
|
|
|
- for (auto& s : stage) try { fs::rename(s.first, s.second); } catch(...) {}
|
|
|
+ for (const auto& m : last.moves) try { if (fs::exists(m.new_path)) fs::rename(m.new_path, m.original_path); } catch(...) {}
|
|
|
start_loading_folder(m_current_path);
|
|
|
}
|
|
|
|
|
|
@@ -455,7 +491,6 @@ protected:
|
|
|
}
|
|
|
{ std::lock_guard<std::mutex> l(m_QueueMutex); m_ResultQueue.push_back(item); }
|
|
|
m_processed_files++; m_Dispatcher.emit();
|
|
|
- std::this_thread::sleep_for(std::chrono::microseconds(50));
|
|
|
} catch (...) { m_processed_files++; }
|
|
|
}
|
|
|
m_is_loading = false; m_total_files = 0; m_Dispatcher.emit();
|
|
|
@@ -482,10 +517,9 @@ protected:
|
|
|
if (m_total_files > 0) {
|
|
|
m_ProgressBar.set_fraction(std::min(1.0, (double)m_processed_files / m_total_files));
|
|
|
m_ProgressBar.show();
|
|
|
- m_BtnStopReload.set_label("Stop Loading"); m_BtnStopReload.set_sensitive(true);
|
|
|
+ m_BtnStopReload.set_sensitive(true);
|
|
|
} else {
|
|
|
m_ProgressBar.hide();
|
|
|
- m_BtnStopReload.set_label("Reload Folder");
|
|
|
m_BtnStopReload.set_sensitive(!m_current_path.empty());
|
|
|
}
|
|
|
if (m_total_files == 0) update_preview();
|
|
|
@@ -516,7 +550,6 @@ protected:
|
|
|
int sz = (m_ComboSize.get_active_row_number() == 0) ? 100 : (m_ComboSize.get_active_row_number() == 2 ? 450 : 220);
|
|
|
m_IconView.set_item_width(sz); m_stop_flag = false;
|
|
|
m_WorkerThread = std::thread(&RenamerWindow::worker_thread_logic, this, f, sz, false);
|
|
|
- m_BtnStopReload.set_sensitive(true);
|
|
|
}
|
|
|
|
|
|
void on_open_folder_clicked() {
|
|
|
@@ -536,7 +569,7 @@ protected:
|
|
|
};
|
|
|
|
|
|
int main(int argc, char *argv[]) {
|
|
|
- auto app = Gtk::Application::create(argc, argv, "org.gtkmm.renamer_v03_1");
|
|
|
+ auto app = Gtk::Application::create(argc, argv, "org.gtkmm.renamer");
|
|
|
RenamerWindow window;
|
|
|
return app->run(window);
|
|
|
}
|