mirror of
https://github.com/LHY0125/PathEditor.git
synced 2026-05-10 02:09:46 +08:00
feat: 实现撤销/重做功能和CSV导出支持
- 添加撤销/重做管理器,支持添加、删除、编辑、移动等操作的撤销/重做 - 在应用上下文中集成撤销/重做管理器,最大支持50条历史记录 - 为所有基本操作(新建、编辑、删除、上移、下移、清理)添加撤销记录 - 扩展导出功能,支持CSV格式导出(除原有JSON格式外) - 添加路径格式验证函数,确保导入数据的有效性 - 更新UI文件对话框过滤器以包含CSV格式选项
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(cmake --build build)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,6 +30,7 @@ set(SOURCES
|
|||||||
src/core/app_context.c
|
src/core/app_context.c
|
||||||
src/core/lua_config.c
|
src/core/lua_config.c
|
||||||
src/core/import_export.c
|
src/core/import_export.c
|
||||||
|
src/core/undo_redo.c
|
||||||
src/controller/callbacks.c
|
src/controller/callbacks.c
|
||||||
src/controller/callbacks_basic.c
|
src/controller/callbacks_basic.c
|
||||||
src/controller/callbacks_nav.c
|
src/controller/callbacks_nav.c
|
||||||
|
|||||||
@@ -2,11 +2,13 @@
|
|||||||
#define APP_CONTEXT_H
|
#define APP_CONTEXT_H
|
||||||
|
|
||||||
#include "utils/string_ext.h"
|
#include "utils/string_ext.h"
|
||||||
|
#include "core/undo_redo.h"
|
||||||
|
|
||||||
// 应用上下文结构体,用于存储应用运行时的状态
|
// 应用上下文结构体,用于存储应用运行时的状态
|
||||||
typedef struct {
|
typedef struct {
|
||||||
StringList sys_paths;
|
StringList sys_paths;
|
||||||
StringList user_paths;
|
StringList user_paths;
|
||||||
|
UndoRedoManager *undo_redo_mgr; // 撤销/重做管理器
|
||||||
} AppContext;
|
} AppContext;
|
||||||
|
|
||||||
// 创建应用上下文
|
// 创建应用上下文
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
#ifndef UNDO_REDO_H
|
||||||
|
#define UNDO_REDO_H
|
||||||
|
|
||||||
|
#include "utils/string_ext.h"
|
||||||
|
|
||||||
|
// 操作类型
|
||||||
|
typedef enum {
|
||||||
|
OP_ADD, // 添加路径
|
||||||
|
OP_DELETE, // 删除路径
|
||||||
|
OP_EDIT, // 编辑路径
|
||||||
|
OP_MOVE_UP, // 上移
|
||||||
|
OP_MOVE_DOWN, // 下移
|
||||||
|
OP_CLEAN, // 清理(批量删除)
|
||||||
|
OP_CLEAR, // 清空列表
|
||||||
|
OP_IMPORT // 导入
|
||||||
|
} OperationType;
|
||||||
|
|
||||||
|
// 目标类型(哪个列表)
|
||||||
|
typedef enum {
|
||||||
|
TARGET_SYSTEM, // 系统变量
|
||||||
|
TARGET_USER // 用户变量
|
||||||
|
} TargetType;
|
||||||
|
|
||||||
|
// 单个操作记录
|
||||||
|
typedef struct {
|
||||||
|
OperationType type;
|
||||||
|
TargetType target;
|
||||||
|
int index;
|
||||||
|
int count; // 用于批量操作(如清理、导入)
|
||||||
|
char **old_paths; // 操作前的路径列表(用于撤销)
|
||||||
|
char **new_paths; // 操作后的路径列表(用于重做)
|
||||||
|
} OpRecord;
|
||||||
|
|
||||||
|
// 撤销/重做管理器
|
||||||
|
typedef struct {
|
||||||
|
OpRecord *records;
|
||||||
|
int max_size; // 最大历史记录数
|
||||||
|
int current; // 当前指针位置(-1表示最新操作之后)
|
||||||
|
int count; // 实际记录数
|
||||||
|
} UndoRedoManager;
|
||||||
|
|
||||||
|
// 创建撤销/重做管理器
|
||||||
|
UndoRedoManager *create_undo_redo_manager(int max_size);
|
||||||
|
|
||||||
|
// 销毁撤销/重做管理器
|
||||||
|
void destroy_undo_redo_manager(UndoRedoManager *mgr);
|
||||||
|
|
||||||
|
// 添加操作记录
|
||||||
|
int push_undo_record(UndoRedoManager *mgr, const OpRecord *record);
|
||||||
|
|
||||||
|
// 执行撤销
|
||||||
|
int undo(UndoRedoManager *mgr, StringList *sys_paths, StringList *user_paths);
|
||||||
|
|
||||||
|
// 执行重做
|
||||||
|
int redo(UndoRedoManager *mgr, StringList *sys_paths, StringList *user_paths);
|
||||||
|
|
||||||
|
// 检查是否可以撤销
|
||||||
|
int can_undo(const UndoRedoManager *mgr);
|
||||||
|
|
||||||
|
// 检查是否可以重做
|
||||||
|
int can_redo(const UndoRedoManager *mgr);
|
||||||
|
|
||||||
|
// 清空历史记录
|
||||||
|
void clear_undo_redo_history(UndoRedoManager *mgr);
|
||||||
|
|
||||||
|
#endif // UNDO_REDO_H
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
#include "controller/callbacks_internal.h"
|
#include "controller/callbacks_internal.h"
|
||||||
#include "core/path_manager.h"
|
#include "core/path_manager.h"
|
||||||
#include "core/lua_config.h"
|
#include "core/lua_config.h"
|
||||||
|
#include "core/undo_redo.h"
|
||||||
#include "utils/string_ext.h"
|
#include "utils/string_ext.h"
|
||||||
#include "utils/safe_string.h"
|
#include "utils/safe_string.h"
|
||||||
#include "utils/error_code.h"
|
#include "utils/error_code.h"
|
||||||
@@ -13,6 +14,37 @@
|
|||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.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)
|
int btn_new_cb(Ihandle *self)
|
||||||
{
|
{
|
||||||
@@ -31,6 +63,12 @@ int btn_new_cb(Ihandle *self)
|
|||||||
return IUP_DEFAULT;
|
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);
|
add_string_list(raw_data, buffer);
|
||||||
|
|
||||||
Ihandle *current_list = get_current_list(dlg);
|
Ihandle *current_list = get_current_list(dlg);
|
||||||
@@ -63,6 +101,15 @@ int btn_edit_cb(Ihandle *self)
|
|||||||
{
|
{
|
||||||
if (strlen(buffer) > 0)
|
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);
|
string_list_set(raw_data, selected - 1, buffer);
|
||||||
|
|
||||||
sync_string_list_to_ui(current_list, raw_data);
|
sync_string_list_to_ui(current_list, raw_data);
|
||||||
@@ -108,6 +155,12 @@ int btn_browse_cb(Ihandle *self)
|
|||||||
return IUP_DEFAULT;
|
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);
|
add_string_list(raw_data, value);
|
||||||
|
|
||||||
Ihandle *current_list = get_current_list(dlg);
|
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);
|
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)
|
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);
|
sync_string_list_to_ui(current_list, raw_data);
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ int btn_export_cb(Ihandle *self)
|
|||||||
IupSetAttribute(filedlg, "DIALOGTYPE", "SAVE");
|
IupSetAttribute(filedlg, "DIALOGTYPE", "SAVE");
|
||||||
IupSetAttribute(filedlg, "TITLE", lua_config_get_string("label", "export_title"));
|
IupSetAttribute(filedlg, "TITLE", lua_config_get_string("label", "export_title"));
|
||||||
IupSetAttribute(filedlg, "FILTER", "json");
|
IupSetAttribute(filedlg, "FILTER", "json");
|
||||||
IupSetAttribute(filedlg, "EXTFILTER", "JSON 文件 (*.json)|*.json");
|
IupSetAttribute(filedlg, "EXTFILTER", "JSON 文件 (*.json)|*.json|CSV 文件 (*.csv)|*.csv");
|
||||||
IupSetAttribute(filedlg, "DEFAULTEXT", "json");
|
IupSetAttribute(filedlg, "DEFAULTEXT", "json");
|
||||||
|
|
||||||
char default_name[64];
|
char default_name[64];
|
||||||
|
|||||||
@@ -2,11 +2,45 @@
|
|||||||
#include "controller/callbacks_internal.h"
|
#include "controller/callbacks_internal.h"
|
||||||
#include "core/path_manager.h"
|
#include "core/path_manager.h"
|
||||||
#include "core/lua_config.h"
|
#include "core/lua_config.h"
|
||||||
|
#include "core/undo_redo.h"
|
||||||
#include "utils/error_code.h"
|
#include "utils/error_code.h"
|
||||||
#include "utils/logger.h"
|
#include "utils/logger.h"
|
||||||
#include "utils/ui_constants.h"
|
#include "utils/ui_constants.h"
|
||||||
|
#include "utils/safe_string.h"
|
||||||
#include "ui/ui_utils.h"
|
#include "ui/ui_utils.h"
|
||||||
#include <stdio.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)
|
int btn_up_cb(Ihandle *self)
|
||||||
@@ -18,10 +52,20 @@ int btn_up_cb(Ihandle *self)
|
|||||||
return IUP_DEFAULT;
|
return IUP_DEFAULT;
|
||||||
|
|
||||||
StringList *raw_data = get_current_raw_data(dlg);
|
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)
|
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);
|
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)
|
if (selected == 0 || selected >= raw_data->count)
|
||||||
return IUP_DEFAULT;
|
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)
|
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);
|
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;
|
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);
|
path_manager_clean(raw_data);
|
||||||
int removed = before_count - raw_data->count;
|
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)
|
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);
|
btn_del_cb(self);
|
||||||
return IUP_IGNORE;
|
return IUP_IGNORE;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ AppContext *create_app_context(void)
|
|||||||
{
|
{
|
||||||
init_string_list(&ctx->sys_paths);
|
init_string_list(&ctx->sys_paths);
|
||||||
init_string_list(&ctx->user_paths);
|
init_string_list(&ctx->user_paths);
|
||||||
|
ctx->undo_redo_mgr = create_undo_redo_manager(50);
|
||||||
}
|
}
|
||||||
return ctx;
|
return ctx;
|
||||||
}
|
}
|
||||||
@@ -20,6 +21,7 @@ void destroy_app_context(AppContext *ctx)
|
|||||||
{
|
{
|
||||||
clear_string_list(&ctx->sys_paths);
|
clear_string_list(&ctx->sys_paths);
|
||||||
clear_string_list(&ctx->user_paths);
|
clear_string_list(&ctx->user_paths);
|
||||||
|
destroy_undo_redo_manager(ctx->undo_redo_mgr);
|
||||||
free(ctx);
|
free(ctx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+166
-12
@@ -79,21 +79,52 @@ static char *escape_json_string(const char *str)
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 导出路径数据到 JSON 文件
|
// 转义 CSV 字段中的特殊字符
|
||||||
ErrorCode export_paths_to_file(const ExportData *data, const char *filepath)
|
static char *escape_csv_field(const char *str)
|
||||||
{
|
{
|
||||||
if (!data || !filepath)
|
if (!str)
|
||||||
return ERR_NULL_PTR;
|
return NULL;
|
||||||
|
|
||||||
FILE *fp = fopen(filepath, "w");
|
int len = strlen(str);
|
||||||
if (!fp)
|
// 需要转义双引号和包含逗号、引号、换行的字段
|
||||||
|
char *result = (char *)malloc(len * 2 + 3);
|
||||||
|
if (!result)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
char *p = result;
|
||||||
|
*p++ = '"';
|
||||||
|
|
||||||
|
for (int i = 0; i < len; i++)
|
||||||
{
|
{
|
||||||
log_error("Failed to open file for export: %s", filepath);
|
unsigned char c = (unsigned char)str[i];
|
||||||
return ERR_FILE_NOT_FOUND;
|
switch (c)
|
||||||
|
{
|
||||||
|
case '"':
|
||||||
|
*p++ = '"';
|
||||||
|
*p++ = '"';
|
||||||
|
break;
|
||||||
|
case '\n':
|
||||||
|
*p++ = '\\';
|
||||||
|
*p++ = 'n';
|
||||||
|
break;
|
||||||
|
case '\r':
|
||||||
|
*p++ = '\\';
|
||||||
|
*p++ = 'r';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
*p++ = str[i];
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fprintf(fp, "\xEF\xBB\xBF");
|
*p++ = '"';
|
||||||
|
*p = '\0';
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出 PATH 到 JSON 文件
|
||||||
|
static ErrorCode export_paths_to_json(const ExportData *data, FILE *fp)
|
||||||
|
{
|
||||||
char datetime[64];
|
char datetime[64];
|
||||||
get_current_datetime(datetime, sizeof(datetime));
|
get_current_datetime(datetime, sizeof(datetime));
|
||||||
|
|
||||||
@@ -133,11 +164,92 @@ ErrorCode export_paths_to_file(const ExportData *data, const char *filepath)
|
|||||||
fprintf(fp, " ]\n");
|
fprintf(fp, " ]\n");
|
||||||
|
|
||||||
fprintf(fp, "}\n");
|
fprintf(fp, "}\n");
|
||||||
|
return ERR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出 PATH 到 CSV 文件
|
||||||
|
// 格式:type,path
|
||||||
|
// type: "system" 或 "user"
|
||||||
|
static ErrorCode export_paths_to_csv(const ExportData *data, FILE *fp)
|
||||||
|
{
|
||||||
|
// 写入 UTF-8 BOM
|
||||||
|
fprintf(fp, "\xEF\xBB\xBF");
|
||||||
|
|
||||||
|
// 写入 CSV 标题行
|
||||||
|
fprintf(fp, "type,path\n");
|
||||||
|
|
||||||
|
// 写入系统路径
|
||||||
|
for (int i = 0; i < data->system.count; i++)
|
||||||
|
{
|
||||||
|
if (data->system.items[i])
|
||||||
|
{
|
||||||
|
char *escaped = escape_csv_field(data->system.items[i]);
|
||||||
|
if (escaped)
|
||||||
|
{
|
||||||
|
fprintf(fp, "\"system\",\"%s\"\n", escaped);
|
||||||
|
free(escaped);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 写入用户路径
|
||||||
|
for (int i = 0; i < data->user.count; i++)
|
||||||
|
{
|
||||||
|
if (data->user.items[i])
|
||||||
|
{
|
||||||
|
char *escaped = escape_csv_field(data->user.items[i]);
|
||||||
|
if (escaped)
|
||||||
|
{
|
||||||
|
fprintf(fp, "\"user\",\"%s\"\n", escaped);
|
||||||
|
free(escaped);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ERR_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出 PATH 到文件
|
||||||
|
ErrorCode export_paths_to_file(const ExportData *data, const char *filepath)
|
||||||
|
{
|
||||||
|
if (!data || !filepath)
|
||||||
|
return ERR_NULL_PTR;
|
||||||
|
|
||||||
|
const char *ext = strrchr(filepath, '.');
|
||||||
|
if (ext && _stricmp(ext, ".csv") == 0)
|
||||||
|
{
|
||||||
|
return export_paths_to_format(data, filepath, EXPORT_CSV);
|
||||||
|
}
|
||||||
|
return export_paths_to_format(data, filepath, EXPORT_JSON);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出 PATH 到指定格式的文件
|
||||||
|
ErrorCode export_paths_to_format(const ExportData *data, const char *filepath, ExportFormat format)
|
||||||
|
{
|
||||||
|
if (!data || !filepath)
|
||||||
|
return ERR_NULL_PTR;
|
||||||
|
|
||||||
|
FILE *fp = fopen(filepath, "w");
|
||||||
|
if (!fp)
|
||||||
|
{
|
||||||
|
log_error("Failed to open file for export: %s", filepath);
|
||||||
|
return ERR_FILE_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
ErrorCode result;
|
||||||
|
if (format == EXPORT_CSV)
|
||||||
|
result = export_paths_to_csv(data, fp);
|
||||||
|
else
|
||||||
|
result = export_paths_to_json(data, fp);
|
||||||
|
|
||||||
fclose(fp);
|
fclose(fp);
|
||||||
log_info("Exported paths to file: sys=%d, user=%d, file=%s",
|
|
||||||
data->system.count, data->user.count, filepath);
|
if (result == ERR_OK)
|
||||||
return ERR_OK;
|
{
|
||||||
|
log_info("Exported paths to file: sys=%d, user=%d, format=%d, file=%s",
|
||||||
|
data->system.count, data->user.count, format, filepath);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 移除字符串首尾的空格、制表符、换行符和回车符
|
// 移除字符串首尾的空格、制表符、换行符和回车符
|
||||||
@@ -340,3 +452,45 @@ ErrorCode import_paths_from_file(const char *filepath, ExportData *data)
|
|||||||
data->system.count, data->user.count, filepath);
|
data->system.count, data->user.count, filepath);
|
||||||
return ERR_OK;
|
return ERR_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 验证路径格式是否有效
|
||||||
|
// 有效的 Windows 路径格式:
|
||||||
|
// - 绝对路径:C:\path\to\something
|
||||||
|
// - UNC 路径:\\server\share
|
||||||
|
// - 环境变量:%PATH%
|
||||||
|
// - 相对路径(带冒号后面跟着反斜杠或正斜杠的)
|
||||||
|
int is_valid_path_format(const char *path)
|
||||||
|
{
|
||||||
|
if (!path || *path == '\0')
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
// 检查是否包含冒号(驱动器路径)
|
||||||
|
const char *colon = strchr(path, ':');
|
||||||
|
|
||||||
|
// 检查是否以 \\ 开头(UNC 路径)
|
||||||
|
if (path[0] == '\\' && path[1] == '\\')
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
// 检查是否为驱动器路径(如 C:\)
|
||||||
|
if (colon && colon - path == 1)
|
||||||
|
{
|
||||||
|
char drive = path[0];
|
||||||
|
if ((drive >= 'A' && drive <= 'Z') || (drive >= 'a' && drive <= 'z'))
|
||||||
|
{
|
||||||
|
// 检查冒号后面是否是路径分隔符
|
||||||
|
const char *after_colon = colon + 1;
|
||||||
|
if (*after_colon == '\\' || *after_colon == '/' || *after_colon == '\0')
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否包含环境变量(%...%)
|
||||||
|
if (strchr(path, '%'))
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
// 检查路径是否包含反斜杠或正斜杠(相对路径)
|
||||||
|
if (strchr(path, '\\') || strchr(path, '/'))
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,314 @@
|
|||||||
|
#include "core/undo_redo.h"
|
||||||
|
#include "core/path_manager.h"
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include "utils/safe_string.h"
|
||||||
|
#include "utils/logger.h"
|
||||||
|
|
||||||
|
#define DEFAULT_MAX_UNDO_RECORDS 50
|
||||||
|
|
||||||
|
static char *copy_string(const char *str)
|
||||||
|
{
|
||||||
|
if (!str)
|
||||||
|
return NULL;
|
||||||
|
return _strdup(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void free_string_array(char **arr, int count)
|
||||||
|
{
|
||||||
|
if (!arr)
|
||||||
|
return;
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
if (arr[i])
|
||||||
|
free(arr[i]);
|
||||||
|
}
|
||||||
|
free(arr);
|
||||||
|
}
|
||||||
|
|
||||||
|
static char **copy_string_array(const char **arr, int count)
|
||||||
|
{
|
||||||
|
if (!arr || count <= 0)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
char **copy = (char **)malloc(count * sizeof(char *));
|
||||||
|
if (!copy)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
copy[i] = copy_string(arr[i]);
|
||||||
|
}
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void init_op_record(OpRecord *record)
|
||||||
|
{
|
||||||
|
memset(record, 0, sizeof(OpRecord));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void free_op_record(OpRecord *record)
|
||||||
|
{
|
||||||
|
if (record->old_paths)
|
||||||
|
free_string_array(record->old_paths, record->count);
|
||||||
|
if (record->new_paths)
|
||||||
|
free_string_array(record->new_paths, record->count);
|
||||||
|
init_op_record(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
UndoRedoManager *create_undo_redo_manager(int max_size)
|
||||||
|
{
|
||||||
|
if (max_size <= 0)
|
||||||
|
max_size = DEFAULT_MAX_UNDO_RECORDS;
|
||||||
|
|
||||||
|
UndoRedoManager *mgr = (UndoRedoManager *)malloc(sizeof(UndoRedoManager));
|
||||||
|
if (!mgr)
|
||||||
|
return NULL;
|
||||||
|
|
||||||
|
mgr->records = (OpRecord *)malloc(max_size * sizeof(OpRecord));
|
||||||
|
if (!mgr->records)
|
||||||
|
{
|
||||||
|
free(mgr);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
mgr->max_size = max_size;
|
||||||
|
mgr->current = -1;
|
||||||
|
mgr->count = 0;
|
||||||
|
|
||||||
|
for (int i = 0; i < max_size; i++)
|
||||||
|
init_op_record(&mgr->records[i]);
|
||||||
|
|
||||||
|
return mgr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void destroy_undo_redo_manager(UndoRedoManager *mgr)
|
||||||
|
{
|
||||||
|
if (!mgr)
|
||||||
|
return;
|
||||||
|
|
||||||
|
for (int i = 0; i < mgr->count; i++)
|
||||||
|
free_op_record(&mgr->records[i]);
|
||||||
|
|
||||||
|
free(mgr->records);
|
||||||
|
free(mgr);
|
||||||
|
}
|
||||||
|
|
||||||
|
int push_undo_record(UndoRedoManager *mgr, const OpRecord *record)
|
||||||
|
{
|
||||||
|
if (!mgr || !record)
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
// 如果 current 不是在最新位置(已经撤销过),清除重做历史
|
||||||
|
while (mgr->count > mgr->current + 1)
|
||||||
|
{
|
||||||
|
mgr->count--;
|
||||||
|
free_op_record(&mgr->records[mgr->count]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果已满,移除最旧的记录
|
||||||
|
if (mgr->count >= mgr->max_size)
|
||||||
|
{
|
||||||
|
// 移除第一条记录
|
||||||
|
free_op_record(&mgr->records[0]);
|
||||||
|
for (int i = 0; i < mgr->max_size - 1; i++)
|
||||||
|
mgr->records[i] = mgr->records[i + 1];
|
||||||
|
init_op_record(&mgr->records[mgr->max_size - 1]);
|
||||||
|
mgr->current--;
|
||||||
|
}
|
||||||
|
|
||||||
|
int pos = mgr->count;
|
||||||
|
mgr->records[pos] = *record;
|
||||||
|
mgr->records[pos].old_paths = copy_string_array((const char **)record->old_paths, record->count);
|
||||||
|
mgr->records[pos].new_paths = copy_string_array((const char **)record->new_paths, record->count);
|
||||||
|
|
||||||
|
mgr->current = pos;
|
||||||
|
mgr->count = pos + 1;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void apply_record(UndoRedoManager *mgr, int record_index, int is_undo)
|
||||||
|
{
|
||||||
|
(void)mgr;
|
||||||
|
(void)record_index;
|
||||||
|
(void)is_undo;
|
||||||
|
// 此函数已废弃,撤销/重做逻辑在 undo() 和 redo() 中直接实现
|
||||||
|
}
|
||||||
|
|
||||||
|
int undo(UndoRedoManager *mgr, StringList *sys_paths, StringList *user_paths)
|
||||||
|
{
|
||||||
|
if (!mgr || !can_undo(mgr))
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
OpRecord *rec = &mgr->records[mgr->current];
|
||||||
|
StringList *target = (rec->target == TARGET_SYSTEM) ? sys_paths : user_paths;
|
||||||
|
|
||||||
|
switch (rec->type)
|
||||||
|
{
|
||||||
|
case OP_ADD:
|
||||||
|
// 撤销添加:删除刚添加的路径
|
||||||
|
if (rec->count > 0 && target->count > 0)
|
||||||
|
{
|
||||||
|
// 删除最后添加的那条
|
||||||
|
free(target->items[target->count - 1]);
|
||||||
|
target->count--;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case OP_DELETE:
|
||||||
|
// 撤销删除:恢复被删除的路径
|
||||||
|
for (int i = 0; i < rec->count; i++)
|
||||||
|
{
|
||||||
|
if (rec->old_paths[i])
|
||||||
|
add_string_list(target, rec->old_paths[i]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case OP_EDIT:
|
||||||
|
// 撤销编辑:恢复到原值
|
||||||
|
if (rec->old_paths[0])
|
||||||
|
string_list_set(target, rec->index, rec->old_paths[0]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case OP_MOVE_UP:
|
||||||
|
case OP_MOVE_DOWN:
|
||||||
|
// 撤销移动:反向移动一次
|
||||||
|
if (rec->type == OP_MOVE_UP)
|
||||||
|
path_manager_move_down(target, rec->index - 1);
|
||||||
|
else
|
||||||
|
path_manager_move_up(target, rec->index + 1);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case OP_CLEAN:
|
||||||
|
case OP_IMPORT:
|
||||||
|
// 撤销清理/导入:恢复到原列表
|
||||||
|
clear_string_list(target);
|
||||||
|
for (int i = 0; i < rec->count; i++)
|
||||||
|
{
|
||||||
|
if (rec->old_paths[i])
|
||||||
|
add_string_list(target, rec->old_paths[i]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case OP_CLEAR:
|
||||||
|
// 撤销清空:恢复所有路径
|
||||||
|
for (int i = 0; i < rec->count; i++)
|
||||||
|
{
|
||||||
|
if (rec->old_paths[i])
|
||||||
|
add_string_list(target, rec->old_paths[i]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
mgr->current--;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int redo(UndoRedoManager *mgr, StringList *sys_paths, StringList *user_paths)
|
||||||
|
{
|
||||||
|
if (!mgr || !can_redo(mgr))
|
||||||
|
return -1;
|
||||||
|
|
||||||
|
mgr->current++;
|
||||||
|
OpRecord *rec = &mgr->records[mgr->current];
|
||||||
|
StringList *target = (rec->target == TARGET_SYSTEM) ? sys_paths : user_paths;
|
||||||
|
|
||||||
|
switch (rec->type)
|
||||||
|
{
|
||||||
|
case OP_ADD:
|
||||||
|
// 重做添加:重新添加路径
|
||||||
|
for (int i = 0; i < rec->count; i++)
|
||||||
|
{
|
||||||
|
if (rec->new_paths[i])
|
||||||
|
add_string_list(target, rec->new_paths[i]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case OP_DELETE:
|
||||||
|
// 重做删除:重新删除路径
|
||||||
|
for (int i = 0; i < rec->count; i++)
|
||||||
|
{
|
||||||
|
// 找到并删除对应路径
|
||||||
|
for (int j = 0; j < target->count; j++)
|
||||||
|
{
|
||||||
|
if (target->items[j] && rec->old_paths[i] &&
|
||||||
|
strcmp(target->items[j], rec->old_paths[i]) == 0)
|
||||||
|
{
|
||||||
|
free(target->items[j]);
|
||||||
|
// 移动后面的元素
|
||||||
|
for (int k = j; k < target->count - 1; k++)
|
||||||
|
target->items[k] = target->items[k + 1];
|
||||||
|
target->count--;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case OP_EDIT:
|
||||||
|
// 重做编辑:应用新值
|
||||||
|
if (rec->new_paths[0])
|
||||||
|
string_list_set(target, rec->index, rec->new_paths[0]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case OP_MOVE_UP:
|
||||||
|
case OP_MOVE_DOWN:
|
||||||
|
// 重做移动:再次移动
|
||||||
|
if (rec->type == OP_MOVE_UP)
|
||||||
|
path_manager_move_up(target, rec->index);
|
||||||
|
else
|
||||||
|
path_manager_move_down(target, rec->index);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case OP_CLEAN:
|
||||||
|
case OP_IMPORT:
|
||||||
|
// 重做清理/导入:应用新列表
|
||||||
|
clear_string_list(target);
|
||||||
|
for (int i = 0; i < rec->count; i++)
|
||||||
|
{
|
||||||
|
if (rec->new_paths[i])
|
||||||
|
add_string_list(target, rec->new_paths[i]);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case OP_CLEAR:
|
||||||
|
// 重做清空:清空列表
|
||||||
|
clear_string_list(target);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int can_undo(const UndoRedoManager *mgr)
|
||||||
|
{
|
||||||
|
if (!mgr)
|
||||||
|
return 0;
|
||||||
|
return mgr->current >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int can_redo(const UndoRedoManager *mgr)
|
||||||
|
{
|
||||||
|
if (!mgr)
|
||||||
|
return 0;
|
||||||
|
return mgr->current < mgr->count - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear_undo_redo_history(UndoRedoManager *mgr)
|
||||||
|
{
|
||||||
|
if (!mgr)
|
||||||
|
return;
|
||||||
|
|
||||||
|
for (int i = 0; i < mgr->count; i++)
|
||||||
|
free_op_record(&mgr->records[i]);
|
||||||
|
|
||||||
|
mgr->current = -1;
|
||||||
|
mgr->count = 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user