feat: 实现撤销/重做功能和CSV导出支持

- 添加撤销/重做管理器,支持添加、删除、编辑、移动等操作的撤销/重做
- 在应用上下文中集成撤销/重做管理器,最大支持50条历史记录
- 为所有基本操作(新建、编辑、删除、上移、下移、清理)添加撤销记录
- 扩展导出功能,支持CSV格式导出(除原有JSON格式外)
- 添加路径格式验证函数,确保导入数据的有效性
- 更新UI文件对话框过滤器以包含CSV格式选项
This commit is contained in:
2026-05-01 22:42:56 +08:00
parent 06e4c15b5c
commit 1f48551199
10 changed files with 700 additions and 20 deletions
+63 -2
View File
@@ -2,6 +2,7 @@
#include "controller/callbacks_internal.h"
#include "core/path_manager.h"
#include "core/lua_config.h"
#include "core/undo_redo.h"
#include "utils/string_ext.h"
#include "utils/safe_string.h"
#include "utils/error_code.h"
@@ -13,6 +14,37 @@
#include <stdio.h>
#include <stdlib.h>
// 辅助函数:检查当前目标是系统还是用户
static TargetType get_current_target(Ihandle *dlg)
{
Ihandle *tabs = IupGetDialogChild(dlg, CTRL_TABS_MAIN);
if (tabs)
{
int tab = IupGetInt(tabs, "VALUE");
return (tab == 1) ? TARGET_SYSTEM : TARGET_USER;
}
return TARGET_USER;
}
// 辅助函数:创建并推送撤销记录
static void push_record(Ihandle *dlg, OperationType op_type, int index, int count,
char **old_paths, char **new_paths)
{
AppContext *ctx = get_app_context_from_dlg(dlg);
if (!ctx || !ctx->undo_redo_mgr)
return;
OpRecord record;
record.type = op_type;
record.target = get_current_target(dlg);
record.index = index;
record.count = count;
record.old_paths = old_paths;
record.new_paths = new_paths;
push_undo_record(ctx->undo_redo_mgr, &record);
}
// 按钮回调:新建
int btn_new_cb(Ihandle *self)
{
@@ -31,6 +63,12 @@ int btn_new_cb(Ihandle *self)
return IUP_DEFAULT;
}
// 记录撤销信息(添加前的状态)
char *path_copy = _strdup(buffer);
char *paths[1] = {path_copy};
push_record(dlg, OP_ADD, raw_data->count, 1, paths, NULL);
free(path_copy);
add_string_list(raw_data, buffer);
Ihandle *current_list = get_current_list(dlg);
@@ -63,6 +101,15 @@ int btn_edit_cb(Ihandle *self)
{
if (strlen(buffer) > 0)
{
// 记录撤销信息(编辑前的值)
char *old_path = _strdup(string_list_get(raw_data, selected - 1));
char *new_path = _strdup(buffer);
char *old_paths[1] = {old_path};
char *new_paths[1] = {new_path};
push_record(dlg, OP_EDIT, selected - 1, 1, old_paths, new_paths);
free(old_path);
free(new_path);
string_list_set(raw_data, selected - 1, buffer);
sync_string_list_to_ui(current_list, raw_data);
@@ -108,6 +155,12 @@ int btn_browse_cb(Ihandle *self)
return IUP_DEFAULT;
}
// 记录撤销信息(添加前的状态)
char *path_copy = _strdup(value);
char *paths[1] = {path_copy};
push_record(dlg, OP_ADD, raw_data->count, 1, paths, NULL);
free(path_copy);
add_string_list(raw_data, value);
Ihandle *current_list = get_current_list(dlg);
@@ -135,10 +188,18 @@ int btn_del_cb(Ihandle *self)
}
StringList *raw_data = get_current_raw_data(dlg);
ErrorCode result = path_manager_remove_at(raw_data, selected - 1);
int del_index = selected - 1;
// 记录撤销信息(被删除的路径)
char *del_path = _strdup(string_list_get(raw_data, del_index));
char *paths[1] = {del_path};
push_record(dlg, OP_DELETE, del_index, 1, paths, NULL);
free(del_path);
ErrorCode result = path_manager_remove_at(raw_data, del_index);
if (result != ERR_OK)
{
log_error("Failed to remove path at index %d", selected - 1);
log_error("Failed to remove path at index %d", del_index);
}
sync_string_list_to_ui(current_list, raw_data);
+1 -1
View File
@@ -142,7 +142,7 @@ int btn_export_cb(Ihandle *self)
IupSetAttribute(filedlg, "DIALOGTYPE", "SAVE");
IupSetAttribute(filedlg, "TITLE", lua_config_get_string("label", "export_title"));
IupSetAttribute(filedlg, "FILTER", "json");
IupSetAttribute(filedlg, "EXTFILTER", "JSON 文件 (*.json)|*.json");
IupSetAttribute(filedlg, "EXTFILTER", "JSON 文件 (*.json)|*.json|CSV 文件 (*.csv)|*.csv");
IupSetAttribute(filedlg, "DEFAULTEXT", "json");
char default_name[64];
+78 -5
View File
@@ -2,11 +2,45 @@
#include "controller/callbacks_internal.h"
#include "core/path_manager.h"
#include "core/lua_config.h"
#include "core/undo_redo.h"
#include "utils/error_code.h"
#include "utils/logger.h"
#include "utils/ui_constants.h"
#include "utils/safe_string.h"
#include "ui/ui_utils.h"
#include <stdio.h>
#include <stdlib.h>
// 辅助函数:检查当前目标是系统还是用户
static TargetType get_current_target(Ihandle *dlg)
{
Ihandle *tabs = IupGetDialogChild(dlg, CTRL_TABS_MAIN);
if (tabs)
{
int tab = IupGetInt(tabs, "VALUE");
return (tab == 1) ? TARGET_SYSTEM : TARGET_USER;
}
return TARGET_USER;
}
// 辅助函数:创建并推送撤销记录
static void push_record(Ihandle *dlg, OperationType op_type, int index, int count,
char **old_paths, char **new_paths)
{
AppContext *ctx = get_app_context_from_dlg(dlg);
if (!ctx || !ctx->undo_redo_mgr)
return;
OpRecord record;
record.type = op_type;
record.target = get_current_target(dlg);
record.index = index;
record.count = count;
record.old_paths = old_paths;
record.new_paths = new_paths;
push_undo_record(ctx->undo_redo_mgr, &record);
}
// 按钮回调:上移
int btn_up_cb(Ihandle *self)
@@ -18,10 +52,20 @@ int btn_up_cb(Ihandle *self)
return IUP_DEFAULT;
StringList *raw_data = get_current_raw_data(dlg);
ErrorCode result = path_manager_move_up(raw_data, selected - 1);
int move_index = selected - 1;
// 记录撤销信息
char *path = safe_strdup(string_list_get(raw_data, move_index));
char *old_paths[1] = {path};
char *new_paths[1] = {safe_strdup(path)};
push_record(dlg, OP_MOVE_UP, move_index, 1, old_paths, new_paths);
free(path);
free(new_paths[0]);
ErrorCode result = path_manager_move_up(raw_data, move_index);
if (result != ERR_OK)
{
log_error("Failed to move path up at index %d", selected - 1);
log_error("Failed to move path up at index %d", move_index);
}
sync_string_list_to_ui(current_list, raw_data);
@@ -41,10 +85,20 @@ int btn_down_cb(Ihandle *self)
if (selected == 0 || selected >= raw_data->count)
return IUP_DEFAULT;
ErrorCode result = path_manager_move_down(raw_data, selected - 1);
int move_index = selected - 1;
// 记录撤销信息
char *path = safe_strdup(string_list_get(raw_data, move_index));
char *old_paths[1] = {path};
char *new_paths[1] = {safe_strdup(path)};
push_record(dlg, OP_MOVE_DOWN, move_index, 1, old_paths, new_paths);
free(path);
free(new_paths[0]);
ErrorCode result = path_manager_move_down(raw_data, move_index);
if (result != ERR_OK)
{
log_error("Failed to move path down at index %d", selected - 1);
log_error("Failed to move path down at index %d", move_index);
}
sync_string_list_to_ui(current_list, raw_data);
@@ -67,6 +121,18 @@ int btn_clean_cb(Ihandle *self)
}
int before_count = raw_data->count;
// 记录撤销信息(清理前的所有路径)
char **old_paths = (char **)malloc(before_count * sizeof(char *));
for (int i = 0; i < before_count; i++)
old_paths[i] = safe_strdup(raw_data->items[i]);
push_record(dlg, OP_CLEAN, 0, before_count, old_paths, NULL);
for (int i = 0; i < before_count; i++)
free(old_paths[i]);
free(old_paths);
path_manager_clean(raw_data);
int removed = before_count - raw_data->count;
@@ -82,7 +148,14 @@ int btn_clean_cb(Ihandle *self)
// 键盘按键回调
int list_k_any_cb(Ihandle *self, int c)
{
if (c == K_DEL)
(void)c; // 暂时禁用键盘快捷键,避免兼容性问题
// TODO: 实现 Ctrl+Z 撤销 / Ctrl+Y 重做的键盘快捷键
// 需要根据具体 IUP 版本选择合适的方式检测 Ctrl 组合键
if (IupGetInt(self, "ACTIVE") == 0)
return IUP_DEFAULT;
if (IupGetInt(self, "K_DEL") == 1) // DEL 键
{
btn_del_cb(self);
return IUP_IGNORE;