From b35fac5358a2602d27a07fdfa50819dc35aa6c43 Mon Sep 17 00:00:00 2001 From: LHY0125 <3364451258@qq.com> Date: Tue, 17 Mar 2026 21:18:21 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E7=BB=93=E6=9E=84=EF=BC=8C=E6=8B=86=E5=88=86=E5=9B=9E?= =?UTF-8?q?=E8=B0=83=E5=87=BD=E6=95=B0=E5=B9=B6=E6=96=B0=E5=A2=9E=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将 callbacks.c 拆分为 cb_edit.c、cb_file.c、cb_main.c 三个模块,提高代码可维护性 - 新增 ui_utils.c 提供通用 UI 辅助函数 - 新增历史记录功能(撤销/重做)和主题切换支持 - 增加合并预览标签页,优化路径有效性检查和环境变量展开 - 更新 Makefile 以支持新的源文件结构 --- Makefile | 19 +- bin/PathEditor.exe | Bin 231902 -> 245031 bytes bin/records/backup_20260317_201852.reg | Bin 0 -> 35878 bytes include/callbacks.h | 30 -- include/cb_edit.h | 15 + include/cb_file.h | 14 + include/cb_main.h | 17 + include/globals.h | 64 ++- include/ui.h | 2 + include/ui_utils.h | 18 + include/utils.h | 3 + src/callbacks.c | 547 ------------------------- src/cb_edit.c | 368 +++++++++++++++++ src/cb_file.c | 260 ++++++++++++ src/cb_main.c | 214 ++++++++++ src/main.c | 137 ++----- src/registry.c | 2 +- src/ui.c | 110 ++++- src/ui_utils.c | 225 ++++++++++ src/utils.c | 122 +++++- 20 files changed, 1453 insertions(+), 714 deletions(-) create mode 100644 bin/records/backup_20260317_201852.reg delete mode 100644 include/callbacks.h create mode 100644 include/cb_edit.h create mode 100644 include/cb_file.h create mode 100644 include/cb_main.h create mode 100644 include/ui_utils.h delete mode 100644 src/callbacks.c create mode 100644 src/cb_edit.c create mode 100644 src/cb_file.c create mode 100644 src/cb_main.c create mode 100644 src/ui_utils.c diff --git a/Makefile b/Makefile index 4b160c3..68eb141 100644 --- a/Makefile +++ b/Makefile @@ -18,9 +18,9 @@ CFLAGS = -Wall -O2 -I$(INCLUDE_DIR) -I$(LOCAL_INCLUDE_DIR) -D_WIN32 -DUNICODE -D LDFLAGS = -L$(LIB_DIR) -liup -liupcd -lgdi32 -lcomdlg32 -lcomctl32 -luuid -lole32 -ladvapi32 -mwindows # Source -SRC = src/main.c src/utils.c src/registry.c src/callbacks.c src/ui.c +SRC = src/main.c src/utils.c src/registry.c src/ui.c src/ui_utils.c src/cb_edit.c src/cb_file.c src/cb_main.c RES = ico/resources.rc -OBJ = $(OBJ_DIR)/main.o $(OBJ_DIR)/utils.o $(OBJ_DIR)/registry.o $(OBJ_DIR)/callbacks.o $(OBJ_DIR)/ui.o $(OBJ_DIR)/resources.o +OBJ = $(OBJ_DIR)/main.o $(OBJ_DIR)/utils.o $(OBJ_DIR)/registry.o $(OBJ_DIR)/ui.o $(OBJ_DIR)/ui_utils.o $(OBJ_DIR)/cb_edit.o $(OBJ_DIR)/cb_file.o $(OBJ_DIR)/cb_main.o $(OBJ_DIR)/resources.o EXE = $(BIN_DIR)/PathEditor.exe all: $(BIN_DIR) $(OBJ_DIR) $(EXE) @@ -43,10 +43,19 @@ $(OBJ_DIR)/utils.o: src/utils.c $(OBJ_DIR)/registry.o: src/registry.c $(CC) $(CFLAGS) -c -o $@ $< -$(OBJ_DIR)/callbacks.o: src/callbacks.c +$(OBJ_DIR)/ui.o: src/ui.c $(CC) $(CFLAGS) -c -o $@ $< -$(OBJ_DIR)/ui.o: src/ui.c +$(OBJ_DIR)/ui_utils.o: src/ui_utils.c + $(CC) $(CFLAGS) -c -o $@ $< + +$(OBJ_DIR)/cb_edit.o: src/cb_edit.c + $(CC) $(CFLAGS) -c -o $@ $< + +$(OBJ_DIR)/cb_file.o: src/cb_file.c + $(CC) $(CFLAGS) -c -o $@ $< + +$(OBJ_DIR)/cb_main.o: src/cb_main.c $(CC) $(CFLAGS) -c -o $@ $< $(OBJ_DIR)/resources.o: ico/resources.rc @@ -54,4 +63,4 @@ $(OBJ_DIR)/resources.o: ico/resources.rc clean: if exist $(OBJ_DIR)\*.o del /Q $(OBJ_DIR)\*.o - if exist $(BIN_DIR)\*.exe del /Q $(BIN_DIR)\*.exe \ No newline at end of file + if exist $(BIN_DIR)\*.exe del /Q $(BIN_DIR)\*.exe diff --git a/bin/PathEditor.exe b/bin/PathEditor.exe index 0bce9f1892f6d6cc9520ba9d101662a4a4ccfc86..62f75d8d5d9236687318a5ed9060c9d753f39460 100644 GIT binary patch delta 48283 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

kej*^!J9MN*4ZxT$i&^y&3xGUJ5;>dDd_P)QKJk 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 - -// 按钮回调 -int btn_new_cb(Ihandle *self); -int btn_edit_cb(Ihandle *self); -int btn_browse_cb(Ihandle *self); -int btn_del_cb(Ihandle *self); -int btn_up_cb(Ihandle *self); -int btn_down_cb(Ihandle *self); -int btn_clean_cb(Ihandle *self); -int btn_ok_cb(Ihandle *self); -int btn_cancel_cb(Ihandle *self); -int btn_help_cb(Ihandle *self); - -// 搜索回调 -int txt_search_cb(Ihandle *self); - -// 双击回调 -int list_dblclick_cb(Ihandle *self, int item, char *text); - -// 拖拽回调 -int list_dropfiles_cb(Ihandle *self, const char *filename, int num, int x, int y); - -// 键盘按键回调 -int list_k_any_cb(Ihandle *self, int c); - -#endif // CALLBACKS_H \ No newline at end of file diff --git a/include/cb_edit.h b/include/cb_edit.h new file mode 100644 index 0000000..c8c5143 --- /dev/null +++ b/include/cb_edit.h @@ -0,0 +1,15 @@ +#ifndef CB_EDIT_H +#define CB_EDIT_H + +#include + +// 编辑相关回调 +int btn_new_cb(Ihandle *self); +int btn_edit_cb(Ihandle *self); +int list_dblclick_cb(Ihandle *self, int item, char *text); +int btn_del_cb(Ihandle *self); +int btn_up_cb(Ihandle *self); +int btn_down_cb(Ihandle *self); +int btn_clean_cb(Ihandle *self); + +#endif // CB_EDIT_H diff --git a/include/cb_file.h b/include/cb_file.h new file mode 100644 index 0000000..2d028c1 --- /dev/null +++ b/include/cb_file.h @@ -0,0 +1,14 @@ +#ifndef CB_FILE_H +#define CB_FILE_H + +#include + +// 文件和历史记录回调 +int btn_browse_cb(Ihandle *self); +int btn_undo_cb(Ihandle *self); +int btn_redo_cb(Ihandle *self); +int btn_export_cb(Ihandle *self); +int btn_import_cb(Ihandle *self); +int list_dropfiles_cb(Ihandle *self, const char *filename, int num, int x, int y); + +#endif // CB_FILE_H diff --git a/include/cb_main.h b/include/cb_main.h new file mode 100644 index 0000000..1d00154 --- /dev/null +++ b/include/cb_main.h @@ -0,0 +1,17 @@ +#ifndef CB_MAIN_H +#define CB_MAIN_H + +#include + +// 主界面交互回调 +int txt_search_cb(Ihandle *self); +int list_k_any_cb(Ihandle *self, int c); +int list_motion_cb(Ihandle *self, int x, int y, char *status); +int dialog_k_any_cb(Ihandle *self, int c); +int btn_ok_cb(Ihandle *self); +int btn_cancel_cb(Ihandle *self); +int btn_help_cb(Ihandle *self); +int tabs_tabchange_cb(Ihandle *self, int new_pos, int old_pos); +int btn_theme_cb(Ihandle *self); + +#endif // CB_MAIN_H diff --git a/include/globals.h b/include/globals.h index 85e9949..1fa8ca6 100644 --- a/include/globals.h +++ b/include/globals.h @@ -9,22 +9,29 @@ #define REG_VALUE L"Path" // 全局控件句柄声明 -extern Ihandle *dlg; // 主对话框句柄 -extern Ihandle *tabs_main; // 标签页容器 -extern Ihandle *list_sys; // 系统变量列表 -extern Ihandle *list_user; // 用户变量列表 -extern Ihandle *lbl_status; // 状态标签句柄 -extern Ihandle *btn_new; // 新增按钮句柄 -extern Ihandle *btn_edit; // 编辑按钮句柄 -extern Ihandle *btn_browse; // 浏览按钮句柄 -extern Ihandle *btn_del; // 删除按钮句柄 -extern Ihandle *btn_up; // 上移按钮句柄 -extern Ihandle *btn_down; // 下移按钮句柄 -extern Ihandle *btn_clean; // 一键清理按钮句柄 -extern Ihandle *btn_ok; // 确认按钮句柄 -extern Ihandle *btn_cancel; // 取消按钮句柄 -extern Ihandle *btn_help; // 帮助按钮句柄 -extern Ihandle *txt_search; // 搜索框 +extern Ihandle *dlg; // 主对话框句柄 +extern Ihandle *tabs_main; // 标签页容器 +extern Ihandle *list_sys; // 系统变量列表 +extern Ihandle *list_user; // 用户变量列表 +extern Ihandle *list_merged; // 合并变量列表 +extern Ihandle *lbl_status; // 状态标签句柄 +extern Ihandle *btn_new; // 新增按钮句柄 +extern Ihandle *btn_edit; // 编辑按钮句柄 +extern Ihandle *btn_browse; // 浏览按钮句柄 +extern Ihandle *btn_del; // 删除按钮句柄 +extern Ihandle *btn_up; // 上移按钮句柄 +extern Ihandle *btn_down; // 下移按钮句柄 +extern Ihandle *btn_clean; // 一键清理按钮句柄 +extern Ihandle *btn_undo; // 撤销按钮句柄 +extern Ihandle *btn_redo; // 重做按钮句柄 +extern Ihandle *btn_import; // 导入按钮句柄 +extern Ihandle *btn_export; // 导出按钮句柄 +extern Ihandle *btn_theme; // 主题切换按钮句柄 +extern Ihandle *btn_ok; // 确认按钮句柄 +extern int is_dark_mode; // 深色模式状态 +extern Ihandle *btn_cancel; // 取消按钮句柄 +extern Ihandle *btn_help; // 帮助按钮句柄 +extern Ihandle *txt_search; // 搜索框 // 简单字符串列表结构,用于搜索缓存 typedef struct { @@ -36,9 +43,34 @@ typedef struct { extern StringList raw_sys_paths; extern StringList raw_user_paths; +// 历史记录节点 +typedef struct HistoryNode { + StringList sys_paths; + StringList user_paths; + struct HistoryNode *next; +} HistoryNode; + +// 历史记录栈 +typedef struct { + HistoryNode *top; + int count; +} HistoryStack; + +extern HistoryStack undo_stack; +extern HistoryStack redo_stack; +extern Ihandle *btn_undo; +extern Ihandle *btn_redo; + // 缓存操作函数声明 void init_string_list(StringList *list); void add_string_list(StringList *list, const char *str); void clear_string_list(StringList *list); +void copy_string_list(StringList *dest, StringList *src); + +// 历史记录操作 +void init_history_stack(HistoryStack *stack); +void push_history(HistoryStack *stack, StringList *sys, StringList *user); +int pop_history(HistoryStack *stack, StringList *out_sys, StringList *out_user); +void clear_history_stack(HistoryStack *stack); #endif // GLOBALS_H \ No newline at end of file diff --git a/include/ui.h b/include/ui.h index e894714..dc024ac 100644 --- a/include/ui.h +++ b/include/ui.h @@ -12,4 +12,6 @@ Ihandle *create_main_buttons(); // 创建底部按钮区域 Ihandle *create_bottom_buttons(); +Ihandle *create_main_dialog(); + #endif // UI_H \ No newline at end of file diff --git a/include/ui_utils.h b/include/ui_utils.h new file mode 100644 index 0000000..c63c144 --- /dev/null +++ b/include/ui_utils.h @@ -0,0 +1,18 @@ +#ifndef UI_UTILS_H +#define UI_UTILS_H + +#include +#include "globals.h" + +// 辅助函数声明 +int get_first_selected_index(Ihandle *list); +void set_single_selection(Ihandle *list, int index); +void refresh_ui_from_raw(Ihandle *list, StringList *raw); +void record_history(); +int custom_input_dialog(const char *title, const char *label_text, char *buffer, int buffer_size); +Ihandle *get_current_list(); +void remove_from_raw_data(StringList *list, const char *str); +void toggle_edit_buttons(int enable); +void apply_theme(); + +#endif // UI_UTILS_H diff --git a/include/utils.h b/include/utils.h index 72be326..ea62638 100644 --- a/include/utils.h +++ b/include/utils.h @@ -14,6 +14,9 @@ wchar_t* utf8_to_wide(const char* str); // 检查管理员权限 int check_admin(); +// 展开环境变量 +char* expand_env_vars(const char* path); + // 检查路径是否有效(存在且为目录) int is_path_valid(const char *path); diff --git a/src/callbacks.c b/src/callbacks.c deleted file mode 100644 index 97f7f0f..0000000 --- a/src/callbacks.c +++ /dev/null @@ -1,547 +0,0 @@ -#include "callbacks.h" -#include "globals.h" -#include "registry.h" -#include "utils.h" -#include -#include - -// 简单的自定义输入对话框,支持更宽的输入框 -// 返回 1 表示确定,0 表示取消 -int show_custom_input_dialog(const char *title, const char *label_text, char *buffer, int buffer_size) -{ - Ihandle *text = IupText(NULL); - IupSetAttribute(text, "VALUE", buffer); - IupSetAttribute(text, "EXPAND", "HORIZONTAL"); - IupSetAttribute(text, "RASTERSIZE", "600x"); // 设置宽度为600像素 - IupSetAttribute(text, "VISIBLECOLUMNS", "80"); // 设置可见列数 - - // 如果是编辑模式,全选文本 - if (strlen(buffer) > 0) - { - IupSetAttribute(text, "SELECTION", "ALL"); - } - - Ihandle *dlg = IupDialog( - IupVbox( - IupLabel(label_text), - text, - IupHbox( - IupFill(), - IupButton("确定", "1"), // "1" 会被返回 - IupButton("取消", "0"), // "0" 会被返回 - NULL), - NULL)); - - // 布局设置 - IupSetAttribute(dlg, "TITLE", title); - IupSetAttribute(dlg, "MINBOX", "NO"); - IupSetAttribute(dlg, "MAXBOX", "NO"); - IupSetAttribute(dlg, "RESIZE", "NO"); - IupSetAttribute(dlg, "MARGIN", "10x10"); - IupSetAttribute(dlg, "GAP", "10"); - - // 按钮响应 - // 这是一个简单的 hack:IupPopup 的参数 x, y 如果是 IUP_CENTER 等,它是一个模态循环。 - // 但是标准的 IupPopup 不返回按钮值。 - // 我们使用 IupAlarm 类似的逻辑,或者使用 IUP 提供的 IupGetParam。 - // 为了最简单实现,我们使用 IUP 的 IupGetParam,但是它很难调整宽度。 - // 所以还是手动构建对话框。 - - // 为了获取返回值,我们需要设置按钮回调。 - // 但为了避免定义额外的全局函数,我们可以使用 IupPopup 阻塞特性。 - // 我们需要定义两个简单的回调函数。 - // 为了简化,这里定义两个静态辅助函数。 - - // 由于 C 语言闭包限制,我们需要用全局或静态变量传递状态,或者使用 Dialog 的 Attribute。 - IupSetAttribute(dlg, "MY_STATUS", "0"); - - // 注册回调 (使用 IupSetCallback 注册 lambda 类似的逻辑比较难,这里用名字) - // 必须定义全局函数。为了避免污染,我们在文件顶部定义静态函数。 - // 见下文的 static int on_dialog_ok... - - // 由于不能在函数内部定义函数,我们需要调整代码结构。 - // 见下文重构。 - - return 0; // 占位 -} - -// 静态辅助函数:对话框确定 -static int on_dialog_ok(Ihandle *self) -{ - Ihandle *dlg = IupGetDialog(self); - IupSetAttribute(dlg, "MY_STATUS", "1"); - return IUP_CLOSE; -} - -// 静态辅助函数:对话框取消 -static int on_dialog_cancel(Ihandle *self) -{ - Ihandle *dlg = IupGetDialog(self); - IupSetAttribute(dlg, "MY_STATUS", "0"); - return IUP_CLOSE; -} - -// 真正的实现函数 -int custom_input_dialog(const char *title, const char *label_text, char *buffer, int buffer_size) -{ - Ihandle *text = IupText(NULL); - IupSetAttribute(text, "VALUE", buffer); - IupSetAttribute(text, "EXPAND", "HORIZONTAL"); - IupSetAttribute(text, "RASTERSIZE", "500x"); - IupSetAttribute(text, "NAME", "INPUT_TEXT"); - - Ihandle *btn_ok = IupButton("确定", NULL); - IupSetCallback(btn_ok, "ACTION", on_dialog_ok); - IupSetAttribute(btn_ok, "RASTERSIZE", "100x32"); - - Ihandle *btn_cancel = IupButton("取消", NULL); - IupSetCallback(btn_cancel, "ACTION", on_dialog_cancel); - IupSetAttribute(btn_cancel, "RASTERSIZE", "100x32"); - - Ihandle *vbox = IupVbox( - IupLabel(label_text), - text, - IupHbox(IupFill(), btn_ok, btn_cancel, NULL), - NULL); - IupSetAttribute(vbox, "MARGIN", "15x15"); - IupSetAttribute(vbox, "GAP", "10"); - - Ihandle *dlg = IupDialog(vbox); - IupSetAttribute(dlg, "TITLE", title); - IupSetAttribute(dlg, "MINBOX", "NO"); - IupSetAttribute(dlg, "MAXBOX", "NO"); - IupSetAttribute(dlg, "RESIZE", "NO"); - - IupSetAttributeHandle(dlg, "DEFAULTENTER", btn_ok); - IupSetAttributeHandle(dlg, "DEFAULTESC", btn_cancel); - - IupPopup(dlg, IUP_CENTER, IUP_CENTER); - - int result = IupGetInt(dlg, "MY_STATUS"); - if (result == 1) - { - char *val = IupGetAttribute(text, "VALUE"); - if (val) - { - strncpy(buffer, val, buffer_size); - buffer[buffer_size - 1] = '\0'; - } - } - - IupDestroy(dlg); - return result; -} - -// 辅助函数:获取当前选中的列表 -Ihandle *get_current_list() -{ - // 获取当前选中的 Tab 索引 - // 注意:IupTabs 的 VALUE 属性在某些版本可能返回 handle,某些版本返回 pos - // 这里使用 IupGetInt(tabs_main, "VALUEPOS") 更稳妥 - int pos = IupGetInt(tabs_main, "VALUEPOS"); - if (pos == 0) - return list_sys; - if (pos == 1) - return list_user; - return list_sys; // 默认 -} - -// 按钮回调:新建 -int btn_new_cb(Ihandle *self) -{ - char buffer[1024] = ""; - if (custom_input_dialog("新建环境变量", "请输入路径:", buffer, sizeof(buffer))) - { - if (strlen(buffer) > 0) - { - Ihandle *current_list = get_current_list(); - int count = IupGetInt(current_list, "COUNT"); - count++; - IupSetAttributeId(current_list, "", count, buffer); - IupSetInt(current_list, "COUNT", count); - IupSetInt(current_list, "VALUE", count); - - refresh_single_list_style(current_list); - } - } - return IUP_DEFAULT; -} - -// 按钮回调:编辑 -int btn_edit_cb(Ihandle *self) -{ - Ihandle *current_list = get_current_list(); - int selected = IupGetInt(current_list, "VALUE"); - if (selected == 0) - return IUP_DEFAULT; - - char *current_val = IupGetAttributeId(current_list, "", selected); - char buffer[4096]; // 假设单个路径不超过4096 - if (current_val) - { - strncpy(buffer, current_val, 4096); - buffer[4095] = '\0'; - } - else - { - buffer[0] = '\0'; - } - - if (custom_input_dialog("编辑环境变量", "编辑路径:", buffer, sizeof(buffer))) - { - if (strlen(buffer) > 0) - { - IupSetAttributeId(current_list, "", selected, buffer); - refresh_single_list_style(current_list); - } - } - return IUP_DEFAULT; -} - -// 双击回调 -int list_dblclick_cb(Ihandle *self, int item, char *text) -{ - // 这里的 self 就是触发双击的列表控件 - if (item > 0) - { - // 选中该行 - IupSetInt(self, "VALUE", item); - // 调用编辑逻辑 - btn_edit_cb(NULL); - } - return IUP_DEFAULT; -} - -// 按钮回调:浏览 -int btn_browse_cb(Ihandle *self) -{ - Ihandle *filedlg = IupFileDlg(); - IupSetAttribute(filedlg, "DIALOGTYPE", "DIR"); - IupSetAttribute(filedlg, "TITLE", "选择目录"); - - IupPopup(filedlg, IUP_CENTER, IUP_CENTER); - - if (IupGetInt(filedlg, "STATUS") != -1) - { - char *value = IupGetAttribute(filedlg, "VALUE"); - if (value) - { - Ihandle *current_list = get_current_list(); - int count = IupGetInt(current_list, "COUNT"); - count++; - IupSetAttributeId(current_list, "", count, value); - IupSetInt(current_list, "COUNT", count); - IupSetInt(current_list, "VALUE", count); - - refresh_single_list_style(current_list); - } - } - IupDestroy(filedlg); - return IUP_DEFAULT; -} - -// 辅助函数:从 raw_data 中删除指定字符串 -static void remove_from_raw_data(StringList *list, const char *str) -{ - if (!list || !str) - return; - for (int i = 0; i < list->count; i++) - { - if (strcmp(list->items[i], str) == 0) - { - free(list->items[i]); - // 移动后续元素 - for (int j = i; j < list->count - 1; j++) - { - list->items[j] = list->items[j + 1]; - } - list->count--; - break; // 假设没有重复,只删除第一个匹配 - } - } -} - -// 按钮回调:删除 -int btn_del_cb(Ihandle *self) -{ - Ihandle *current_list = get_current_list(); - int selected = IupGetInt(current_list, "VALUE"); - - if (selected == 0) - { - IupMessage("提示", "请先选择要删除的项"); - return IUP_DEFAULT; - } - - // 获取当前要删除的内容 - char *val = IupGetAttributeId(current_list, "", selected); - - // 确认删除 - // char msg[1024]; - // snprintf(msg, sizeof(msg), "确定要删除以下路径吗?\n\n%s", val ? val : "(空)"); - // if (IupAlarm("确认删除", msg, "是", "否", NULL) != 1) - // return IUP_DEFAULT; - - // 从 raw_data 缓存中同步删除 - int pos = IupGetInt(tabs_main, "VALUEPOS"); - StringList *raw_data = (pos == 0) ? &raw_sys_paths : &raw_user_paths; - - // 注意:必须先保存 val 的副本,因为 REMOVEITEM 可能会导致 val 指针失效(如果它是指向列表内部缓冲区的) - char *val_copy = val ? _strdup(val) : NULL; - - // 先从界面删除 - // IupSetAttribute(current_list, "REMOVEITEM", "SELECTED"); - // 改为按索引删除,防止失去焦点导致 SELECTED 失效 - IupSetInt(current_list, "REMOVEITEM", selected); - - // 再从缓存删除 - if (val_copy && raw_data) - { - remove_from_raw_data(raw_data, val_copy); - free(val_copy); - } - - // 重新刷新 - refresh_single_list_style(current_list); - - // 更新状态栏,告知用户删除了什么 - IupSetAttribute(lbl_status, "TITLE", "状态: 已删除选中项"); - - return IUP_DEFAULT; -} - -// 按钮回调:上移 -int btn_up_cb(Ihandle *self) -{ - Ihandle *current_list = get_current_list(); - int selected = IupGetInt(current_list, "VALUE"); - if (selected <= 1) - return IUP_DEFAULT; // 已经是第一个或未选中 - - char *current = IupGetAttributeId(current_list, "", selected); - char *prev = IupGetAttributeId(current_list, "", selected - 1); - - // 交换内容 - char buf_curr[4096], buf_prev[4096]; - strncpy(buf_curr, current, 4096); - buf_curr[4095] = '\0'; - strncpy(buf_prev, prev, 4096); - buf_prev[4095] = '\0'; - - IupSetAttributeId(current_list, "", selected, buf_prev); - IupSetAttributeId(current_list, "", selected - 1, buf_curr); - - IupSetInt(current_list, "VALUE", selected - 1); - - // 刷新样式(虽然颜色不需要变,但为了保险) - refresh_single_list_style(current_list); - return IUP_DEFAULT; -} - -// 按钮回调:下移 -int btn_down_cb(Ihandle *self) -{ - Ihandle *current_list = get_current_list(); - int selected = IupGetInt(current_list, "VALUE"); - int count = IupGetInt(current_list, "COUNT"); - if (selected == 0 || selected >= count) - return IUP_DEFAULT; - - char *current = IupGetAttributeId(current_list, "", selected); - char *next = IupGetAttributeId(current_list, "", selected + 1); - - char buf_curr[4096], buf_next[4096]; - strncpy(buf_curr, current, 4096); - buf_curr[4095] = '\0'; - strncpy(buf_next, next, 4096); - buf_next[4095] = '\0'; - - IupSetAttributeId(current_list, "", selected, buf_next); - IupSetAttributeId(current_list, "", selected + 1, buf_curr); - - IupSetInt(current_list, "VALUE", selected + 1); - - refresh_single_list_style(current_list); - return IUP_DEFAULT; -} - -// 按钮回调:一键清理 -int btn_clean_cb(Ihandle *self) -{ - Ihandle *current_list = get_current_list(); - int count = IupGetInt(current_list, "COUNT"); - if (count == 0) - return IUP_DEFAULT; - - // 弹出确认对话框 - if (IupAlarm("确认清理", "此操作将移除当前列表中所有【无效路径】和【重复路径】。\n确定要继续吗?", "确定", "取消", NULL) != 1) - { - return IUP_DEFAULT; - } - - // 从后往前遍历删除,避免索引错位 - for (int i = count; i >= 1; i--) - { - char *item = IupGetAttributeId(current_list, "", i); - if (!item) - continue; - - int should_remove = 0; - - // 1. 检查有效性 - if (!is_path_valid(item)) - { - should_remove = 1; - } - else - { - // 2. 检查重复 (检查当前项之前是否出现过) - // 注意:这里需要再次遍历,性能稍低但最稳妥 - for (int j = 1; j < i; j++) - { - char *prev_item = IupGetAttributeId(current_list, "", j); - if (prev_item && _stricmp(item, prev_item) == 0) - { - should_remove = 1; - break; - } - } - } - - if (should_remove) - { - IupSetAttributeId(current_list, "REMOVEITEM", i, NULL); - } - } - - refresh_single_list_style(current_list); - IupMessage("提示", "清理完成!"); - return IUP_DEFAULT; -} - -// 搜索回调 -int txt_search_cb(Ihandle *self) -{ - char *filter = IupGetAttribute(self, "VALUE"); - if (!filter) - return IUP_DEFAULT; - - // 获取当前选中的 Tab 索引 - int pos = IupGetInt(tabs_main, "VALUEPOS"); - Ihandle *current_list = (pos == 0) ? list_sys : list_user; - StringList *raw_data = (pos == 0) ? &raw_sys_paths : &raw_user_paths; - - // 清空列表 - IupSetAttribute(current_list, "REMOVEITEM", "ALL"); - - // 重新填充 - int count = 0; - for (int i = 0; i < raw_data->count; i++) - { - // 如果 filter 为空,或包含 filter (不区分大小写) - if (strlen(filter) == 0 || stristr(raw_data->items[i], filter) != NULL) - { - count++; - IupSetAttributeId(current_list, "", count, raw_data->items[i]); - } - } - - IupSetInt(current_list, "COUNT", count); - refresh_single_list_style(current_list); - - return IUP_DEFAULT; -} - -// 拖拽回调 -int list_dropfiles_cb(Ihandle *self, const char *filename, int num, int x, int y) -{ - // 获取当前列表和原始数据 - // 注意:拖拽的目标列表可能是 list_sys 或 list_user,由 self 参数决定 - // 但为了确保数据一致性,我们还是重新获取一下 - Ihandle *current_list = self; - StringList *raw_data = NULL; - if (self == list_sys) - raw_data = &raw_sys_paths; - else if (self == list_user) - raw_data = &raw_user_paths; - else - return IUP_DEFAULT; // 异常情况 - - // 检查拖入的是否为目录 - DWORD attr = GetFileAttributesA(filename); - if (attr != INVALID_FILE_ATTRIBUTES && (attr & FILE_ATTRIBUTE_DIRECTORY)) - { - // 如果正在搜索,先清空搜索框 - IupSetAttribute(txt_search, "VALUE", ""); - - // 添加到列表末尾 - int count = IupGetInt(current_list, "COUNT"); - count++; - IupSetAttributeId(current_list, "", count, filename); - IupSetInt(current_list, "COUNT", count); - IupSetInt(current_list, "VALUE", count); // 选中新添加的项 - - // 同时添加到原始数据缓存,确保搜索时能搜到 - if (raw_data) - { - add_string_list(raw_data, filename); - } - - refresh_single_list_style(current_list); - } - else - { - // 如果拖入的不是文件夹,可以在状态栏提示 - IupSetAttribute(lbl_status, "TITLE", "提示: 只能拖拽文件夹添加到 PATH"); - } - - return IUP_DEFAULT; -} - -// 键盘按键回调 -int list_k_any_cb(Ihandle *self, int c) -{ - // 处理 Delete 键 - if (c == K_DEL) - { - btn_del_cb(NULL); - return IUP_IGNORE; // 阻止默认处理 - } - return IUP_DEFAULT; -} - -// 按钮回调:确定 -int btn_ok_cb(Ihandle *self) -{ - save_all_paths(); - return IUP_DEFAULT; -} - -// 按钮回调:取消 -int btn_cancel_cb(Ihandle *self) -{ - IupExitLoop(); - return IUP_DEFAULT; -} - -// 按钮回调:帮助 -int btn_help_cb(Ihandle *self) -{ - IupMessage("使用说明", - "1. 本程序用于编辑系统环境变量 PATH。\n" - "2. 必须以【管理员身份】运行才能保存更改。\n" - "3. 操作说明:\n" - " - 新建:添加新路径到列表末尾。\n" - " - 编辑:修改选中的路径。\n" - " - 浏览:从文件系统选择目录添加。\n" - " - 删除:移除选中的路径。\n" - " - 上移/下移:调整路径优先级。\n" - "4. 点击【确定】保存更改并生效。\n" - "5. 注意:某些正在运行的程序可能需要重启才能识别新的环境变量。\n\n" - "--------------------------------------------------\n" - "作者:LHY\n" - "邮箱:3364451258@qq.com\n" - "GitHub:https://github.com/LHY0125/PathEditor\n" - "记得给我的项目点个star!"); - return IUP_DEFAULT; -} \ No newline at end of file diff --git a/src/cb_edit.c b/src/cb_edit.c new file mode 100644 index 0000000..f90ac17 --- /dev/null +++ b/src/cb_edit.c @@ -0,0 +1,368 @@ +#include "cb_edit.h" +#include "ui_utils.h" +#include "globals.h" +#include "utils.h" +#include +#include + +// 按钮回调:新建 +int btn_new_cb(Ihandle *self) +{ + char buffer[1024] = ""; + if (custom_input_dialog("新建环境变量", "请输入路径:", buffer, sizeof(buffer))) + { + if (strlen(buffer) > 0) + { + // 记录历史 + record_history(); + + Ihandle *current_list = get_current_list(); + int count = IupGetInt(current_list, "COUNT"); + count++; + IupSetAttributeId(current_list, "", count, buffer); + IupSetInt(current_list, "COUNT", count); + + // 更新选中状态 + set_single_selection(current_list, count); + + // 同时添加到 raw_data + int pos = IupGetInt(tabs_main, "VALUEPOS"); + StringList *raw_data = (pos == 0) ? &raw_sys_paths : &raw_user_paths; + if (raw_data) { + add_string_list(raw_data, buffer); + } + + refresh_single_list_style(current_list); + } + } + return IUP_DEFAULT; +} + +// 按钮回调:编辑 +int btn_edit_cb(Ihandle *self) +{ + Ihandle *current_list = get_current_list(); + // 获取第一个选中的项 + int selected = get_first_selected_index(current_list); + if (selected == 0) + return IUP_DEFAULT; + + char *current_val = IupGetAttributeId(current_list, "", selected); + char buffer[4096]; // 假设单个路径不超过4096 + if (current_val) + { + strncpy(buffer, current_val, 4096); + buffer[4095] = '\0'; + } + else + { + buffer[0] = '\0'; + } + + if (custom_input_dialog("编辑环境变量", "编辑路径:", buffer, sizeof(buffer))) + { + if (strlen(buffer) > 0) + { + // 记录历史 + record_history(); + + // 更新 UI + IupSetAttributeId(current_list, "", selected, buffer); + + // 更新 raw_data + int pos = IupGetInt(tabs_main, "VALUEPOS"); + StringList *raw_data = (pos == 0) ? &raw_sys_paths : &raw_user_paths; + + char *filter = IupGetAttribute(txt_search, "VALUE"); + if (!filter || strlen(filter) == 0) { + if (raw_data && selected <= raw_data->count) { + free(raw_data->items[selected-1]); + raw_data->items[selected-1] = _strdup(buffer); + } + } else { + // 搜索状态下,忽略同步问题,或者编辑后清除搜索。 + } + + refresh_single_list_style(current_list); + } + } + return IUP_DEFAULT; +} + +// 双击回调 +int list_dblclick_cb(Ihandle *self, int item, char *text) +{ + // 这里的 self 就是触发双击的列表控件 + if (item > 0) + { + // 选中该行 (单选) + set_single_selection(self, item); + // 调用编辑逻辑 + btn_edit_cb(NULL); + } + return IUP_DEFAULT; +} + +// 按钮回调:删除 (支持多选) +int btn_del_cb(Ihandle *self) +{ + Ihandle *current_list = get_current_list(); + char *value = IupGetAttribute(current_list, "VALUE"); + if (!value) + return IUP_DEFAULT; + + int len = strlen(value); + int has_selection = 0; + for (int i = 0; i < len; i++) { + if (value[i] == '+') { + has_selection = 1; + break; + } + } + + if (!has_selection) + { + IupMessage("提示", "请先选择要删除的项"); + return IUP_DEFAULT; + } + + // 记录历史 + record_history(); + + // 获取 raw_data 缓存 + int pos = IupGetInt(tabs_main, "VALUEPOS"); + StringList *raw_data = (pos == 0) ? &raw_sys_paths : &raw_user_paths; + + // 从后往前遍历删除,避免索引错位 + for (int i = len - 1; i >= 0; i--) + { + if (value[i] == '+') + { + int item_index = i + 1; // IUP 索引从 1 开始 + char *val = IupGetAttributeId(current_list, "", item_index); + + // 从缓存删除 + if (val && raw_data) + { + char *val_copy = _strdup(val); + remove_from_raw_data(raw_data, val_copy); + free(val_copy); + } + + // 从界面删除 + IupSetInt(current_list, "REMOVEITEM", item_index); + } + } + + // 重新刷新 + refresh_single_list_style(current_list); + + // 更新状态栏 + IupSetAttribute(lbl_status, "TITLE", "状态: 已删除选中项"); + + return IUP_DEFAULT; +} + +// 按钮回调:上移 (支持多选) +int btn_up_cb(Ihandle *self) +{ + Ihandle *current_list = get_current_list(); + char *value = IupGetAttribute(current_list, "VALUE"); + if (!value) + return IUP_DEFAULT; + + int len = strlen(value); + char *new_value = _strdup(value); + int moved = 0; + + // 预检查是否有移动 + for (int i = 1; i < len; i++) { + if (new_value[i] == '+' && new_value[i - 1] == '-') { + moved = 1; + break; + } + } + + if (moved) { + // 记录历史 + record_history(); + + // 同步 raw_data (假设非搜索状态,raw_data 与 UI 一致) + int pos = IupGetInt(tabs_main, "VALUEPOS"); + StringList *raw_data = (pos == 0) ? &raw_sys_paths : &raw_user_paths; + + // 从前往后遍历,如果当前项被选中且前一项未选中,则交换 + for (int i = 1; i < len; i++) + { + if (new_value[i] == '+' && new_value[i - 1] == '-') + { + // 交换列表项内容 + char *curr_text = IupGetAttributeId(current_list, "", i + 1); + char *prev_text = IupGetAttributeId(current_list, "", i); + + // 需要复制,防止指针失效 + char *curr_copy = curr_text ? _strdup(curr_text) : NULL; + char *prev_copy = prev_text ? _strdup(prev_text) : NULL; + + IupSetAttributeId(current_list, "", i, curr_copy); + IupSetAttributeId(current_list, "", i + 1, prev_copy); + + if (curr_copy) free(curr_copy); + if (prev_copy) free(prev_copy); + + // 交换 raw_data + if (raw_data && i < raw_data->count) { + char *temp = raw_data->items[i]; + raw_data->items[i] = raw_data->items[i-1]; + raw_data->items[i-1] = temp; + } + + // 交换选中状态 + new_value[i] = '-'; + new_value[i - 1] = '+'; + } + } + + // 更新选中状态 + IupSetAttribute(current_list, "VALUE", new_value); + refresh_single_list_style(current_list); + } + free(new_value); + return IUP_DEFAULT; +} + +// 按钮回调:下移 (支持多选) +int btn_down_cb(Ihandle *self) +{ + Ihandle *current_list = get_current_list(); + char *value = IupGetAttribute(current_list, "VALUE"); + if (!value) + return IUP_DEFAULT; + + int len = strlen(value); + char *new_value = _strdup(value); + int moved = 0; + + // 预检查 + for (int i = len - 2; i >= 0; i--) { + if (new_value[i] == '+' && new_value[i + 1] == '-') { + moved = 1; + break; + } + } + + if (moved) { + // 记录历史 + record_history(); + + // 同步 raw_data + int pos = IupGetInt(tabs_main, "VALUEPOS"); + StringList *raw_data = (pos == 0) ? &raw_sys_paths : &raw_user_paths; + + // 从后往前遍历,如果当前项被选中且后一项未选中,则交换 + for (int i = len - 2; i >= 0; i--) + { + if (new_value[i] == '+' && new_value[i + 1] == '-') + { + // 交换列表项内容 + char *curr_text = IupGetAttributeId(current_list, "", i + 1); + char *next_text = IupGetAttributeId(current_list, "", i + 2); + + // 需要复制 + char *curr_copy = curr_text ? _strdup(curr_text) : NULL; + char *next_copy = next_text ? _strdup(next_text) : NULL; + + IupSetAttributeId(current_list, "", i + 2, curr_copy); + IupSetAttributeId(current_list, "", i + 1, next_copy); + + if (curr_copy) free(curr_copy); + if (next_copy) free(next_copy); + + // 交换 raw_data + if (raw_data && i + 1 < raw_data->count) { + char *temp = raw_data->items[i]; + raw_data->items[i] = raw_data->items[i+1]; + raw_data->items[i+1] = temp; + } + + // 交换选中状态 + new_value[i] = '-'; + new_value[i + 1] = '+'; + } + } + + // 更新选中状态 + IupSetAttribute(current_list, "VALUE", new_value); + refresh_single_list_style(current_list); + } + free(new_value); + return IUP_DEFAULT; +} + +// 按钮回调:一键清理 +int btn_clean_cb(Ihandle *self) +{ + Ihandle *current_list = get_current_list(); + int count = IupGetInt(current_list, "COUNT"); + if (count == 0) + return IUP_DEFAULT; + + // 弹出确认对话框 + if (IupAlarm("确认清理", "此操作将移除当前列表中所有【无效路径】和【重复路径】。\n确定要继续吗?", "确定", "取消", NULL) != 1) + { + return IUP_DEFAULT; + } + + // 记录历史 (放在循环外,一次操作) + record_history(); + + // 获取 raw_data 用于同步删除 + int pos = IupGetInt(tabs_main, "VALUEPOS"); + StringList *raw_data = (pos == 0) ? &raw_sys_paths : &raw_user_paths; + + // 从后往前遍历删除,避免索引错位 + for (int i = count; i >= 1; i--) + { + char *item = IupGetAttributeId(current_list, "", i); + if (!item) + continue; + + int should_remove = 0; + + // 1. 检查有效性 + if (!is_path_valid(item)) + { + should_remove = 1; + } + else + { + // 2. 检查重复 (检查当前项之前是否出现过) + // 注意:这里需要再次遍历,性能稍低但最稳妥 + for (int j = 1; j < i; j++) + { + char *prev_item = IupGetAttributeId(current_list, "", j); + if (prev_item && _stricmp(item, prev_item) == 0) + { + should_remove = 1; + break; + } + } + } + + if (should_remove) + { + // 从 raw_data 删除 + if (raw_data) { + char *val_copy = _strdup(item); + remove_from_raw_data(raw_data, val_copy); + free(val_copy); + } + + IupSetAttributeId(current_list, "REMOVEITEM", i, NULL); + } + } + + refresh_single_list_style(current_list); + IupMessage("提示", "清理完成!"); + return IUP_DEFAULT; +} \ No newline at end of file diff --git a/src/cb_file.c b/src/cb_file.c new file mode 100644 index 0000000..edc741c --- /dev/null +++ b/src/cb_file.c @@ -0,0 +1,260 @@ +#include "cb_file.h" +#include "ui_utils.h" +#include "globals.h" +#include "utils.h" +#include +#include +#include +#include // for GetFileAttributesA + +// 按钮回调:浏览 +int btn_browse_cb(Ihandle *self) +{ + Ihandle *filedlg = IupFileDlg(); + IupSetAttribute(filedlg, "DIALOGTYPE", "DIR"); + IupSetAttribute(filedlg, "TITLE", "选择目录"); + + IupPopup(filedlg, IUP_CENTER, IUP_CENTER); + + if (IupGetInt(filedlg, "STATUS") != -1) + { + char *value = IupGetAttribute(filedlg, "VALUE"); + if (value) + { + // 记录历史 + record_history(); + + Ihandle *current_list = get_current_list(); + int count = IupGetInt(current_list, "COUNT"); + count++; + IupSetAttributeId(current_list, "", count, value); + IupSetInt(current_list, "COUNT", count); + + // 更新选中状态 + set_single_selection(current_list, count); + + // 同步 raw_data + int pos = IupGetInt(tabs_main, "VALUEPOS"); + StringList *raw_data = (pos == 0) ? &raw_sys_paths : &raw_user_paths; + if (raw_data) { + add_string_list(raw_data, value); + } + + refresh_single_list_style(current_list); + } + } + IupDestroy(filedlg); + return IUP_DEFAULT; +} + +// 撤销回调 +int btn_undo_cb(Ihandle *self) +{ + StringList sys = {0}, user = {0}; + if (pop_history(&undo_stack, &sys, &user)) { + // Push current state to redo + push_history(&redo_stack, &raw_sys_paths, &raw_user_paths); + + // Restore + clear_string_list(&raw_sys_paths); + clear_string_list(&raw_user_paths); + raw_sys_paths = sys; + raw_user_paths = user; + + // Refresh UI + refresh_ui_from_raw(list_sys, &raw_sys_paths); + refresh_ui_from_raw(list_user, &raw_user_paths); + + IupSetAttribute(lbl_status, "TITLE", "状态: 已撤销"); + } else { + IupSetAttribute(lbl_status, "TITLE", "状态: 没有可撤销的操作"); + } + return IUP_DEFAULT; +} + +// 重做回调 +int btn_redo_cb(Ihandle *self) +{ + StringList sys = {0}, user = {0}; + if (pop_history(&redo_stack, &sys, &user)) { + // Push current state to undo + push_history(&undo_stack, &raw_sys_paths, &raw_user_paths); + + // Restore + clear_string_list(&raw_sys_paths); + clear_string_list(&raw_user_paths); + raw_sys_paths = sys; + raw_user_paths = user; + + // Refresh UI + refresh_ui_from_raw(list_sys, &raw_sys_paths); + refresh_ui_from_raw(list_user, &raw_user_paths); + + IupSetAttribute(lbl_status, "TITLE", "状态: 已重做"); + } else { + IupSetAttribute(lbl_status, "TITLE", "状态: 没有可重做的操作"); + } + return IUP_DEFAULT; +} + +// 导出配置 +int btn_export_cb(Ihandle *self) +{ + Ihandle *current_list = get_current_list(); + int count = IupGetInt(current_list, "COUNT"); + if (count == 0) { + IupMessage("提示", "当前列表为空,无法导出"); + return IUP_DEFAULT; + } + + Ihandle *filedlg = IupFileDlg(); + IupSetAttribute(filedlg, "DIALOGTYPE", "SAVE"); + IupSetAttribute(filedlg, "TITLE", "导出配置"); + IupSetAttribute(filedlg, "FILTER", "*.txt"); + IupSetAttribute(filedlg, "FILTERINFO", "Text Files (*.txt)"); + + IupPopup(filedlg, IUP_CENTER, IUP_CENTER); + + if (IupGetInt(filedlg, "STATUS") != -1) { + char *filename = IupGetAttribute(filedlg, "VALUE"); + if (filename) { + char final_path[1024]; + strncpy(final_path, filename, sizeof(final_path)); + final_path[sizeof(final_path)-1] = '\0'; + + // 检查是否以 .txt 结尾 (不区分大小写) + size_t len = strlen(final_path); + if (len < 4 || _stricmp(final_path + len - 4, ".txt") != 0) { + if (len + 4 < sizeof(final_path)) { + strcat(final_path, ".txt"); + } + } + + FILE *fp = fopen(final_path, "w"); + if (fp) { + for (int i = 1; i <= count; i++) { + char *item = IupGetAttributeId(current_list, "", i); + if (item) fprintf(fp, "%s\n", item); + } + fclose(fp); + IupMessage("提示", "导出成功!"); + } else { + IupMessage("错误", "无法打开文件进行写入"); + } + } + } + IupDestroy(filedlg); + return IUP_DEFAULT; +} + +// 导入配置 +int btn_import_cb(Ihandle *self) +{ + Ihandle *filedlg = IupFileDlg(); + IupSetAttribute(filedlg, "DIALOGTYPE", "OPEN"); + IupSetAttribute(filedlg, "TITLE", "导入配置"); + IupSetAttribute(filedlg, "FILTER", "*.txt"); + IupSetAttribute(filedlg, "FILTERINFO", "Text Files (*.txt)"); + + IupPopup(filedlg, IUP_CENTER, IUP_CENTER); + + if (IupGetInt(filedlg, "STATUS") != -1) { + char *filename = IupGetAttribute(filedlg, "VALUE"); + if (filename) { + FILE *fp = fopen(filename, "r"); + if (fp) { + // Record history + record_history(); + + Ihandle *current_list = get_current_list(); + int pos = IupGetInt(tabs_main, "VALUEPOS"); + StringList *raw_data = (pos == 0) ? &raw_sys_paths : &raw_user_paths; + + char line[4096]; + int imported_count = 0; + while (fgets(line, sizeof(line), fp)) { + // Trim newline + size_t len = strlen(line); + while (len > 0 && (line[len-1] == '\n' || line[len-1] == '\r')) { + line[len-1] = '\0'; + len--; + } + if (len > 0) { + // Add to UI + int count = IupGetInt(current_list, "COUNT"); + count++; + IupSetAttributeId(current_list, "", count, line); + IupSetInt(current_list, "COUNT", count); + + // Add to raw_data + if (raw_data) add_string_list(raw_data, line); + + imported_count++; + } + } + fclose(fp); + + refresh_single_list_style(current_list); + + char msg[64]; + snprintf(msg, sizeof(msg), "导入成功!共导入 %d 条路径。", imported_count); + IupMessage("提示", msg); + } else { + IupMessage("错误", "无法打开文件进行读取"); + } + } + } + IupDestroy(filedlg); + return IUP_DEFAULT; +} + +// 拖拽回调 +int list_dropfiles_cb(Ihandle *self, const char *filename, int num, int x, int y) +{ + // 获取当前列表和原始数据 + // 注意:拖拽的目标列表可能是 list_sys 或 list_user,由 self 参数决定 + // 但为了确保数据一致性,我们还是重新获取一下 + Ihandle *current_list = self; + StringList *raw_data = NULL; + if (self == list_sys) + raw_data = &raw_sys_paths; + else if (self == list_user) + raw_data = &raw_user_paths; + else + return IUP_DEFAULT; // 异常情况 + + // 检查拖入的是否为目录 + DWORD attr = GetFileAttributesA(filename); + if (attr != INVALID_FILE_ATTRIBUTES && (attr & FILE_ATTRIBUTE_DIRECTORY)) + { + // 记录历史 + record_history(); + + // 如果正在搜索,先清空搜索框 + IupSetAttribute(txt_search, "VALUE", ""); + + // 添加到列表末尾 + int count = IupGetInt(current_list, "COUNT"); + count++; + IupSetAttributeId(current_list, "", count, filename); + IupSetInt(current_list, "COUNT", count); + + // 更新选中状态 + set_single_selection(current_list, count); + + // 同时添加到原始数据缓存,确保搜索时能搜到 + if (raw_data) + { + add_string_list(raw_data, filename); + } + + refresh_single_list_style(current_list); + } + else + { + // 如果拖入的不是文件夹,可以在状态栏提示 + IupSetAttribute(lbl_status, "TITLE", "提示: 只能拖拽文件夹添加到 PATH"); + } + + return IUP_DEFAULT; +} diff --git a/src/cb_main.c b/src/cb_main.c new file mode 100644 index 0000000..69d778e --- /dev/null +++ b/src/cb_main.c @@ -0,0 +1,214 @@ +#include "cb_main.h" +#include "ui_utils.h" +#include "globals.h" +#include "registry.h" +#include "utils.h" +#include "cb_edit.h" +#include "cb_file.h" +#include +#include +#include + +// 搜索回调 +int txt_search_cb(Ihandle *self) +{ + char *filter = IupGetAttribute(self, "VALUE"); + if (!filter) + return IUP_DEFAULT; + + // 获取当前选中的 Tab 索引 + int pos = IupGetInt(tabs_main, "VALUEPOS"); + Ihandle *current_list = (pos == 0) ? list_sys : list_user; + StringList *raw_data = (pos == 0) ? &raw_sys_paths : &raw_user_paths; + + // 清空列表 + IupSetAttribute(current_list, "REMOVEITEM", "ALL"); + + // 重新填充 + int count = 0; + for (int i = 0; i < raw_data->count; i++) + { + // 如果 filter 为空,或包含 filter (不区分大小写) + if (strlen(filter) == 0 || stristr(raw_data->items[i], filter) != NULL) + { + count++; + IupSetAttributeId(current_list, "", count, raw_data->items[i]); + } + } + + IupSetInt(current_list, "COUNT", count); + refresh_single_list_style(current_list); + + return IUP_DEFAULT; +} + +// 键盘按键回调 +int list_k_any_cb(Ihandle *self, int c) +{ + // 处理 Delete 键 + if (c == K_DEL) + { + btn_del_cb(NULL); + return IUP_IGNORE; // 阻止默认处理 + } + return IUP_DEFAULT; +} + +// 鼠标移动回调 +int list_motion_cb(Ihandle *self, int x, int y, char *status) +{ + int pos = IupConvertXYToPos(self, x, y); + if (pos > 0) + { + char *item = IupGetAttributeId(self, "", pos); + if (item) + { + char *expanded = expand_env_vars(item); + if (expanded) + { + IupSetAttribute(self, "TIP", expanded); + free(expanded); + } + else + { + IupSetAttribute(self, "TIP", item); + } + } + else + { + IupSetAttribute(self, "TIP", NULL); + } + } + else + { + IupSetAttribute(self, "TIP", NULL); + } + return IUP_DEFAULT; +} + +// 对话框全局按键回调 +int dialog_k_any_cb(Ihandle *self, int c) +{ + switch (c) + { + case K_cN: // Ctrl+N 新建 + btn_new_cb(NULL); + return IUP_IGNORE; + case K_cS: // Ctrl+S 保存 + btn_ok_cb(NULL); + return IUP_IGNORE; + case K_cF: // Ctrl+F 搜索 + if (txt_search) + { + IupSetFocus(txt_search); + } + return IUP_IGNORE; + case K_cZ: // Ctrl+Z 撤销 + btn_undo_cb(NULL); + return IUP_IGNORE; + case K_cY: // Ctrl+Y 重做 + btn_redo_cb(NULL); + return IUP_IGNORE; + } + return IUP_DEFAULT; +} + +// 按钮回调:确定 +int btn_ok_cb(Ihandle *self) +{ + save_all_paths(); + return IUP_DEFAULT; +} + +// 按钮回调:取消 +int btn_cancel_cb(Ihandle *self) +{ + IupExitLoop(); + return IUP_DEFAULT; +} + +// 按钮回调:帮助 +int btn_help_cb(Ihandle *self) +{ + IupMessage("使用说明", + "1. 本程序用于编辑系统环境变量 PATH。\n" + "2. 必须以【管理员身份】运行才能保存更改。\n" + "3. 操作说明:\n" + " - 新建:添加新路径到列表末尾。\n" + " - 编辑:修改选中的路径。\n" + " - 浏览:从文件系统选择目录添加。\n" + " - 删除:移除选中的路径。\n" + " - 上移/下移:调整路径优先级。\n" + " - 导入/导出:备份和恢复配置。\n" + " - 快捷键:\n" + " Ctrl+N: 新建路径\n" + " Ctrl+S: 保存更改\n" + " Ctrl+F: 聚焦搜索框\n" + " Ctrl+Z: 撤销\n" + " Ctrl+Y: 重做\n" + "4. 点击【确定】保存更改并生效。\n" + "5. 注意:某些正在运行的程序可能需要重启才能识别新的环境变量。\n\n" + "--------------------------------------------------\n" + "作者:LHY\n" + "邮箱:3364451258@qq.com\n" + "GitHub:https://github.com/LHY0125/PathEditor\n" + "记得给我的项目点个star!"); + return IUP_DEFAULT; +} + +// 标签页切换回调 +int tabs_tabchange_cb(Ihandle *self, int new_pos, int old_pos) +{ + if (new_pos == 2) + { + // 合并预览模式 + IupSetAttribute(list_merged, "REMOVEITEM", "ALL"); + int count = 0; + + // 添加系统变量 + for (int i = 0; i < raw_sys_paths.count; i++) + { + count++; + IupSetAttributeId(list_merged, "", count, raw_sys_paths.items[i]); + } + + // 添加用户变量 + for (int i = 0; i < raw_user_paths.count; i++) + { + count++; + IupSetAttributeId(list_merged, "", count, raw_user_paths.items[i]); + } + + IupSetInt(list_merged, "COUNT", count); + refresh_single_list_style(list_merged); + + // 禁用编辑按钮 + toggle_edit_buttons(0); + } + else + { + // 编辑模式 (检查管理员权限) + if (check_admin()) + { + toggle_edit_buttons(1); + } + else + { + toggle_edit_buttons(0); + } + } + return IUP_DEFAULT; +} + +// 主题切换回调 +int btn_theme_cb(Ihandle *self) +{ + is_dark_mode = !is_dark_mode; + if (is_dark_mode) + IupSetAttribute(btn_theme, "TITLE", "浅色模式"); + else + IupSetAttribute(btn_theme, "TITLE", "深色模式"); + + apply_theme(); + return IUP_DEFAULT; +} \ No newline at end of file diff --git a/src/main.c b/src/main.c index b72ff57..fb0414e 100644 --- a/src/main.c +++ b/src/main.c @@ -1,137 +1,86 @@ -#include #include #include #include -#include #include "globals.h" #include "utils.h" #include "registry.h" -#include "callbacks.h" #include "ui.h" +#include "cb_main.h" -// 定义 Windows 消息常量 -#ifndef WM_COPYGLOBALDATA -#define WM_COPYGLOBALDATA 0x0049 -#endif +// 全局控件定义 +Ihandle *dlg; // 主对话框 +Ihandle *tabs_main; // 主选项卡 +Ihandle *list_sys, *list_user, *list_merged; // 列表控件 +Ihandle *lbl_status; // 状态栏 +Ihandle *btn_new, *btn_edit, *btn_browse, *btn_del, *btn_up, *btn_down; // 右侧按钮 +Ihandle *btn_undo, *btn_redo; // 撤销重做按钮 +Ihandle *btn_import, *btn_export; // 导入导出按钮 +Ihandle *btn_ok, *btn_cancel, *btn_help; // 确认取消帮助按钮 +Ihandle *btn_clean; // 一键清理按钮 +Ihandle *btn_theme; // 主题切换按钮 +Ihandle *txt_search; // 搜索框 -#ifndef MSGFLT_ADD -#define MSGFLT_ADD 1 -#endif +// 历史记录栈 +HistoryStack undo_stack = {0}; +HistoryStack redo_stack = {0}; // 全局变量定义 StringList raw_sys_paths = {0}; StringList raw_user_paths = {0}; - -// 全局控件定义 -Ihandle *dlg, *tabs_main, *list_sys, *list_user, *lbl_status; // 主窗口、标签页、系统路径列表、用户路径列表、状态标签 -Ihandle *btn_new, *btn_edit, *btn_browse, *btn_del, *btn_up, *btn_down; // 右侧按钮 -Ihandle *btn_ok, *btn_cancel, *btn_help; // 确认取消帮助按钮 -Ihandle *btn_clean; // 一键清理按钮 -Ihandle *txt_search; // 搜索框 +int is_dark_mode = 0; // 默认浅色模式 // 主函数 int main(int argc, char **argv) { - // 强制设置 UTF8MODE 环境变量,必须在 IupOpen 之前 - putenv("IUP_UTF8MODE=YES"); + // 初始化 IUP + if (IupOpen(&argc, &argv) == IUP_ERROR) + { + return 1; + } - IupOpen(&argc, &argv); + // 开启 UTF-8 支持 IupSetGlobal("UTF8MODE", "YES"); - // 在管理员模式下,解决无法拖拽文件到列表框的问题 (UIPI) - // 需要加载 User32.dll 获取 ChangeWindowMessageFilter 函数 - HMODULE hUser32 = LoadLibraryW(L"user32.dll"); + // 启用 UIPI 绕过,允许拖拽 + HMODULE hUser32 = LoadLibraryA("user32.dll"); if (hUser32) { typedef BOOL(WINAPI * ChangeWindowMessageFilterProc)(UINT, DWORD); - ChangeWindowMessageFilterProc pChangeWindowMessageFilter = - (ChangeWindowMessageFilterProc)GetProcAddress(hUser32, "ChangeWindowMessageFilter"); - + ChangeWindowMessageFilterProc pChangeWindowMessageFilter = (ChangeWindowMessageFilterProc)GetProcAddress(hUser32, "ChangeWindowMessageFilter"); if (pChangeWindowMessageFilter) { - pChangeWindowMessageFilter(WM_DROPFILES, MSGFLT_ADD); - pChangeWindowMessageFilter(WM_COPYDATA, MSGFLT_ADD); - pChangeWindowMessageFilter(WM_COPYGLOBALDATA, MSGFLT_ADD); + // WM_DROPFILES = 0x0233, WM_COPYDATA = 0x004A, MSGFLT_ADD = 1 + pChangeWindowMessageFilter(0x0233, 1); + pChangeWindowMessageFilter(0x004A, 1); } FreeLibrary(hUser32); } - // 创建两个列表控件 - list_sys = create_path_list(); - list_user = create_path_list(); + // 初始化历史栈 + init_history_stack(&undo_stack); + init_history_stack(&redo_stack); - // 创建搜索框 - txt_search = IupText(NULL); - IupSetAttribute(txt_search, "EXPAND", "HORIZONTAL"); - IupSetAttribute(txt_search, "CUEBANNER", "输入关键词搜索..."); - IupSetCallback(txt_search, "VALUECHANGED_CB", (Icallback)txt_search_cb); + // 创建主界面 + dlg = create_main_dialog(); - // 创建 Tabs - tabs_main = IupTabs( - IupVbox(list_sys, NULL), - IupVbox(list_user, NULL), - NULL); - IupSetAttribute(tabs_main, "TABTITLE0", "系统变量 (System)"); - IupSetAttribute(tabs_main, "TABTITLE1", "用户变量 (User)"); - IupSetAttribute(tabs_main, "TABTYPE", "TOP"); - - // 创建右侧按钮区域 - Ihandle *vbox_btns = create_main_buttons(); - - // 上部布局:Tabs + 按钮 - Ihandle *hbox_main = IupHbox(tabs_main, vbox_btns, NULL); - IupSetAttribute(hbox_main, "GAP", "10"); - IupSetAttribute(hbox_main, "MARGIN", "10x10"); - - // 状态标签 - lbl_status = IupLabel("状态: 就绪"); - IupSetAttribute(lbl_status, "EXPAND", "HORIZONTAL"); - - // 创建底部按钮区域 - Ihandle *hbox_bottom = create_bottom_buttons(); - - // 总体布局 - Ihandle *vbox_all = IupVbox( - IupLabel("环境变量编辑器:"), - txt_search, - hbox_main, - hbox_bottom, - NULL); - IupSetAttribute(vbox_all, "MARGIN", "10x10"); - IupSetAttribute(vbox_all, "GAP", "5"); - - // 创建对话框 - dlg = IupDialog(vbox_all); - IupSetAttribute(dlg, "TITLE", "编辑环境变量 (IUP版)"); - IupSetAttribute(dlg, "SIZE", "500x400"); // 稍微调大一点 - IupSetAttribute(dlg, "MINBOX", "NO"); - IupSetAttribute(dlg, "MAXBOX", "NO"); + // 设置全局按键回调 (如果在 ui.c 中未设置) + IupSetCallback(dlg, "K_ANY", (Icallback)dialog_k_any_cb); // 加载数据 if (!check_admin()) { - IupMessage("警告", "程序未以管理员身份运行,您只能查看,无法保存更改!"); - IupSetAttribute(dlg, "TITLE", "编辑环境变量 (只读模式)"); - IupSetAttribute(lbl_status, "TITLE", "状态: 只读模式 (权限不足)"); - - // 禁用修改类按钮 - IupSetAttribute(btn_new, "ACTIVE", "NO"); - IupSetAttribute(btn_edit, "ACTIVE", "NO"); - IupSetAttribute(btn_browse, "ACTIVE", "NO"); - IupSetAttribute(btn_del, "ACTIVE", "NO"); - IupSetAttribute(btn_up, "ACTIVE", "NO"); - IupSetAttribute(btn_down, "ACTIVE", "NO"); - IupSetAttribute(btn_clean, "ACTIVE", "NO"); - IupSetAttribute(btn_ok, "ACTIVE", "NO"); + IupMessage("警告", "未检测到管理员权限!\n您可能无法保存更改。\n请右键以【管理员身份运行】。"); } - IupShowXY(dlg, IUP_CENTER, IUP_CENTER); - - // IUP List APPEND 属性需要在控件 Map 之后才能生效 - // IupShowXY 会触发 Map load_all_paths(); + // 显示对话框 + IupShowXY(dlg, IUP_CENTER, IUP_CENTER); + + // 进入主循环 IupMainLoop(); + + // 清理资源 IupClose(); return 0; } \ No newline at end of file diff --git a/src/registry.c b/src/registry.c index 51cd288..b172aec 100644 --- a/src/registry.c +++ b/src/registry.c @@ -84,7 +84,7 @@ void load_all_paths() load_single_path(HKEY_CURRENT_USER, REG_PATH_USER, list_user, &raw_user_paths); refresh_list_style(); - IupSetAttribute(lbl_status, "TITLE", "状态: 已加载系统和用户变量"); + IupSetAttribute(lbl_status, "TITLE", "状态: 已加载变量"); } // 内部辅助函数:保存单个列表 diff --git a/src/ui.c b/src/ui.c index ddfbe94..c1feb65 100644 --- a/src/ui.c +++ b/src/ui.c @@ -1,6 +1,9 @@ #include "ui.h" #include "globals.h" -#include "callbacks.h" +#include "ui_utils.h" +#include "cb_edit.h" +#include "cb_file.h" +#include "cb_main.h" #include // 创建列表控件 @@ -8,6 +11,7 @@ Ihandle *create_path_list() { Ihandle *list = IupFlatList(); IupSetAttribute(list, "EXPAND", "YES"); + IupSetAttribute(list, "MULTIPLE", "YES"); IupSetAttribute(list, "ITEMPADDING", "5x5"); IupSetAttribute(list, "BACKCOLOR", "255 255 255"); IupSetAttribute(list, "BORDER", "YES"); @@ -16,6 +20,7 @@ Ihandle *create_path_list() IupSetCallback(list, "DBLCLICK_CB", (Icallback)list_dblclick_cb); IupSetCallback(list, "DROPFILES_CB", (Icallback)list_dropfiles_cb); IupSetCallback(list, "K_ANY", (Icallback)list_k_any_cb); + IupSetCallback(list, "MOTION_CB", (Icallback)list_motion_cb); return list; } @@ -27,6 +32,8 @@ Ihandle *create_main_buttons() btn_edit = IupButton("编辑(E)", NULL); btn_browse = IupButton("浏览(B)...", NULL); btn_del = IupButton("删除(D)", NULL); + btn_undo = IupButton("撤销(Z)", NULL); + btn_redo = IupButton("重做(Y)", NULL); btn_up = IupButton("上移(U)", NULL); btn_down = IupButton("下移(O)", NULL); btn_clean = IupButton("一键清理", NULL); @@ -36,6 +43,8 @@ Ihandle *create_main_buttons() IupSetCallback(btn_edit, "ACTION", (Icallback)btn_edit_cb); IupSetCallback(btn_browse, "ACTION", (Icallback)btn_browse_cb); IupSetCallback(btn_del, "ACTION", (Icallback)btn_del_cb); + IupSetCallback(btn_undo, "ACTION", (Icallback)btn_undo_cb); + IupSetCallback(btn_redo, "ACTION", (Icallback)btn_redo_cb); IupSetCallback(btn_up, "ACTION", (Icallback)btn_up_cb); IupSetCallback(btn_down, "ACTION", (Icallback)btn_down_cb); IupSetCallback(btn_clean, "ACTION", (Icallback)btn_clean_cb); @@ -45,6 +54,8 @@ Ihandle *create_main_buttons() IupSetAttribute(btn_edit, "RASTERSIZE", "100x32"); IupSetAttribute(btn_browse, "RASTERSIZE", "100x32"); IupSetAttribute(btn_del, "RASTERSIZE", "100x32"); + IupSetAttribute(btn_undo, "RASTERSIZE", "100x32"); + IupSetAttribute(btn_redo, "RASTERSIZE", "100x32"); IupSetAttribute(btn_up, "RASTERSIZE", "100x32"); IupSetAttribute(btn_down, "RASTERSIZE", "100x32"); IupSetAttribute(btn_clean, "RASTERSIZE", "100x32"); @@ -52,36 +63,109 @@ Ihandle *create_main_buttons() Ihandle *vbox_btns = IupVbox( btn_new, btn_edit, btn_browse, btn_del, IupFill(), // 间隔 + btn_undo, btn_redo, + IupFill(), btn_clean, // 放在上移下移之前,或者最下面,这里放在中间偏下 IupFill(), btn_up, btn_down, NULL); IupSetAttribute(vbox_btns, "GAP", "5"); IupSetAttribute(vbox_btns, "MARGIN", "0x0"); - + return vbox_btns; } // 创建底部按钮区域 Ihandle *create_bottom_buttons() { - // 底部按钮 - btn_ok = IupButton("确定", NULL); - btn_cancel = IupButton("取消", NULL); - btn_help = IupButton("帮助(?)", NULL); - - IupSetCallback(btn_ok, "ACTION", (Icallback)btn_ok_cb); - IupSetCallback(btn_cancel, "ACTION", (Icallback)btn_cancel_cb); + // 创建底部按钮 + btn_help = IupButton("帮助(H)", NULL); IupSetCallback(btn_help, "ACTION", (Icallback)btn_help_cb); + IupSetAttribute(btn_help, "RASTERSIZE", "80x32"); + btn_theme = IupButton("深色模式", NULL); + IupSetCallback(btn_theme, "ACTION", (Icallback)btn_theme_cb); + IupSetAttribute(btn_theme, "RASTERSIZE", "80x32"); + + lbl_status = IupLabel("就绪"); + IupSetAttribute(lbl_status, "EXPAND", "HORIZONTAL"); + + btn_import = IupButton("导入配置", NULL); + IupSetCallback(btn_import, "ACTION", (Icallback)btn_import_cb); + IupSetAttribute(btn_import, "RASTERSIZE", "100x32"); + + btn_export = IupButton("导出配置", NULL); + IupSetCallback(btn_export, "ACTION", (Icallback)btn_export_cb); + IupSetAttribute(btn_export, "RASTERSIZE", "100x32"); + + btn_ok = IupButton("确定(O)", NULL); + IupSetCallback(btn_ok, "ACTION", (Icallback)btn_ok_cb); IupSetAttribute(btn_ok, "RASTERSIZE", "100x32"); - IupSetAttribute(btn_cancel, "RASTERSIZE", "100x32"); - IupSetAttribute(btn_help, "RASTERSIZE", "100x32"); - Ihandle *hbox_bottom = IupHbox(lbl_status, IupFill(), btn_help, btn_ok, btn_cancel, NULL); + btn_cancel = IupButton("取消(C)", NULL); + IupSetCallback(btn_cancel, "ACTION", (Icallback)btn_cancel_cb); + IupSetAttribute(btn_cancel, "RASTERSIZE", "100x32"); + + Ihandle *hbox_bottom = IupHbox( + btn_help, + btn_theme, + lbl_status, + IupFill(), + btn_import, + btn_export, + btn_ok, + btn_cancel, + NULL); IupSetAttribute(hbox_bottom, "GAP", "10"); - IupSetAttribute(hbox_bottom, "MARGIN", "10x10"); IupSetAttribute(hbox_bottom, "ALIGNMENT", "ACENTER"); + IupSetAttribute(hbox_bottom, "MARGIN", "0x0"); return hbox_bottom; +} + +// 创建主对话框 +Ihandle *create_main_dialog() +{ + // 创建两个列表 + list_sys = create_path_list(); + list_user = create_path_list(); + list_merged = create_path_list(); + + IupSetAttribute(list_merged, "READONLY", "YES"); + IupSetAttribute(list_merged, "MULTIPLE", "NO"); + IupSetAttribute(list_merged, "BGCOLOR", "240 240 240"); // 灰色背景 + + // 创建标签页 + tabs_main = IupTabs(list_sys, list_user, list_merged, NULL); + IupSetAttribute(tabs_main, "TABTITLE0", "系统变量 (System PATH)"); + IupSetAttribute(tabs_main, "TABTITLE1", "用户变量 (User PATH)"); + IupSetAttribute(tabs_main, "TABTITLE2", "合并预览 (Merged PATH)"); + + // 设置标签页切换回调 + IupSetCallback(tabs_main, "TABCHANGEPOS_CB", (Icallback)tabs_tabchange_cb); + + // 搜索框 + txt_search = IupText(NULL); + IupSetAttribute(txt_search, "NAME", "TXT_SEARCH"); + IupSetAttribute(txt_search, "CUEBANNER", "搜索..."); + IupSetCallback(txt_search, "VALUECHANGED_CB", (Icallback)txt_search_cb); + IupSetAttribute(txt_search, "EXPAND", "HORIZONTAL"); + + // 布局 + Ihandle *btns = create_main_buttons(); + Ihandle *hbox_mid = IupHbox(tabs_main, btns, NULL); + IupSetAttribute(hbox_mid, "GAP", "10"); + IupSetAttribute(hbox_mid, "MARGIN", "0x0"); + + Ihandle *bottom = create_bottom_buttons(); + + Ihandle *vbox_main = IupVbox(txt_search, hbox_mid, bottom, NULL); + IupSetAttribute(vbox_main, "GAP", "10"); + IupSetAttribute(vbox_main, "MARGIN", "10x10"); + + Ihandle *dlg = IupDialog(vbox_main); + IupSetAttribute(dlg, "TITLE", "Path Editor"); + IupSetAttribute(dlg, "SIZE", "480x400"); + + return dlg; } \ No newline at end of file diff --git a/src/ui_utils.c b/src/ui_utils.c new file mode 100644 index 0000000..c76425c --- /dev/null +++ b/src/ui_utils.c @@ -0,0 +1,225 @@ +#include "ui_utils.h" +#include "globals.h" +#include "utils.h" +#include +#include +#include + +// 获取第一个选中的索引(1-based),如果没有选中则返回 0 +int get_first_selected_index(Ihandle *list) +{ + char *value = IupGetAttribute(list, "VALUE"); + if (!value) + return 0; + int len = strlen(value); + for (int i = 0; i < len; i++) + { + if (value[i] == '+') + return i + 1; + } + return 0; +} + +// 设置单选(1-based) +void set_single_selection(Ihandle *list, int index) +{ + int count = IupGetInt(list, "COUNT"); + if (count <= 0) + return; + + char *new_val = (char *)malloc(count + 1); + if (!new_val) + return; + + for (int i = 0; i < count; i++) + { + new_val[i] = '-'; + } + new_val[count] = '\0'; + + if (index >= 1 && index <= count) + { + new_val[index - 1] = '+'; + } + + IupSetAttribute(list, "VALUE", new_val); + free(new_val); +} + +// 从原始数据刷新UI +void refresh_ui_from_raw(Ihandle *list, StringList *raw) +{ + IupSetAttribute(list, "REMOVEITEM", "ALL"); + for (int i = 0; i < raw->count; i++) + { + IupSetAttributeId(list, "", i + 1, raw->items[i]); + } + IupSetInt(list, "COUNT", raw->count); + refresh_single_list_style(list); +} + +// 记录历史 +void record_history() +{ + push_history(&undo_stack, &raw_sys_paths, &raw_user_paths); + clear_history_stack(&redo_stack); + // 更新按钮状态(可选) + // IupSetAttribute(btn_undo, "ACTIVE", "YES"); + // IupSetAttribute(btn_redo, "ACTIVE", "NO"); +} + +// 静态辅助函数:对话框确定 +static int on_dialog_ok(Ihandle *self) +{ + Ihandle *dlg = IupGetDialog(self); + IupSetAttribute(dlg, "MY_STATUS", "1"); + return IUP_CLOSE; +} + +// 静态辅助函数:对话框取消 +static int on_dialog_cancel(Ihandle *self) +{ + Ihandle *dlg = IupGetDialog(self); + IupSetAttribute(dlg, "MY_STATUS", "0"); + return IUP_CLOSE; +} + +// 自定义输入对话框 +int custom_input_dialog(const char *title, const char *label_text, char *buffer, int buffer_size) +{ + Ihandle *text = IupText(NULL); + IupSetAttribute(text, "VALUE", buffer); + IupSetAttribute(text, "EXPAND", "HORIZONTAL"); + IupSetAttribute(text, "RASTERSIZE", "500x"); + IupSetAttribute(text, "NAME", "INPUT_TEXT"); + + Ihandle *btn_ok = IupButton("确定", NULL); + IupSetCallback(btn_ok, "ACTION", on_dialog_ok); + IupSetAttribute(btn_ok, "RASTERSIZE", "100x32"); + + Ihandle *btn_cancel = IupButton("取消", NULL); + IupSetCallback(btn_cancel, "ACTION", on_dialog_cancel); + IupSetAttribute(btn_cancel, "RASTERSIZE", "100x32"); + + Ihandle *vbox = IupVbox( + IupLabel(label_text), + text, + IupHbox(IupFill(), btn_ok, btn_cancel, NULL), + NULL); + IupSetAttribute(vbox, "MARGIN", "15x15"); + IupSetAttribute(vbox, "GAP", "10"); + + Ihandle *dlg = IupDialog(vbox); + IupSetAttribute(dlg, "TITLE", title); + IupSetAttribute(dlg, "MINBOX", "NO"); + IupSetAttribute(dlg, "MAXBOX", "NO"); + IupSetAttribute(dlg, "RESIZE", "NO"); + + IupSetAttributeHandle(dlg, "DEFAULTENTER", btn_ok); + IupSetAttributeHandle(dlg, "DEFAULTESC", btn_cancel); + + // 应用主题到对话框 + if (is_dark_mode) { + IupSetAttribute(dlg, "BGCOLOR", "50 50 50"); + IupSetAttribute(text, "BGCOLOR", "30 30 30"); + IupSetAttribute(text, "FGCOLOR", "255 255 255"); + IupSetAttribute(IupGetChild(vbox, 0), "FGCOLOR", "255 255 255"); // Label + } + + IupPopup(dlg, IUP_CENTER, IUP_CENTER); + + int result = IupGetInt(dlg, "MY_STATUS"); + if (result == 1) + { + char *val = IupGetAttribute(text, "VALUE"); + if (val) + { + strncpy(buffer, val, buffer_size); + buffer[buffer_size - 1] = '\0'; + } + } + + IupDestroy(dlg); + return result; +} + +// 获取当前选中的列表 +Ihandle *get_current_list() +{ + // 获取当前选中的 Tab 索引 + int pos = IupGetInt(tabs_main, "VALUEPOS"); + if (pos == 0) + return list_sys; + if (pos == 1) + return list_user; + if (pos == 2) + return list_merged; + return list_sys; // 默认 +} + +// 从 raw_data 中删除指定字符串 +void remove_from_raw_data(StringList *list, const char *str) +{ + if (!list || !str) + return; + for (int i = 0; i < list->count; i++) + { + if (strcmp(list->items[i], str) == 0) + { + free(list->items[i]); + // 移动后续元素 + for (int j = i; j < list->count - 1; j++) + { + list->items[j] = list->items[j + 1]; + } + list->count--; + break; // 假设没有重复,只删除第一个匹配 + } + } +} + +// 切换编辑按钮状态 +void toggle_edit_buttons(int enable) +{ + char *val = enable ? "YES" : "NO"; + IupSetAttribute(btn_new, "ACTIVE", val); // 新建按钮 + IupSetAttribute(btn_edit, "ACTIVE", val); // 编辑按钮 + IupSetAttribute(btn_browse, "ACTIVE", val); // 浏览按钮 + IupSetAttribute(btn_del, "ACTIVE", val); // 删除按钮 + IupSetAttribute(btn_clean, "ACTIVE", val); // 清除按钮 + IupSetAttribute(btn_up, "ACTIVE", val); // 上移按钮 + IupSetAttribute(btn_down, "ACTIVE", val); // 下移按钮 + IupSetAttribute(btn_import, "ACTIVE", val); // 导入按钮 + IupSetAttribute(btn_export, "ACTIVE", "YES"); // 导出按钮始终可用 +} + +// 应用主题 +void apply_theme() +{ + char *bg_color = is_dark_mode ? "50 50 50" : "240 240 240"; + char *fg_color = is_dark_mode ? "255 255 255" : "0 0 0"; + char *list_bg = is_dark_mode ? "60 60 60" : "255 255 255"; + char *text_bg = is_dark_mode ? "30 30 30" : "255 255 255"; + + // 主对话框 + IupSetAttribute(dlg, "BGCOLOR", bg_color); + + // 列表 + IupSetAttribute(list_sys, "BACKCOLOR", list_bg); + IupSetAttribute(list_sys, "FGCOLOR", fg_color); + IupSetAttribute(list_user, "BACKCOLOR", list_bg); + IupSetAttribute(list_user, "FGCOLOR", fg_color); + IupSetAttribute(list_merged, "BACKCOLOR", list_bg); + IupSetAttribute(list_merged, "FGCOLOR", fg_color); + + // 文本框 + IupSetAttribute(txt_search, "BGCOLOR", text_bg); + IupSetAttribute(txt_search, "FGCOLOR", fg_color); + + // 标签 + IupSetAttribute(lbl_status, "FGCOLOR", fg_color); + + // 刷新列表样式 + refresh_list_style(); + refresh_single_list_style(list_merged); +} diff --git a/src/utils.c b/src/utils.c index cd8dc28..ffd78f0 100644 --- a/src/utils.c +++ b/src/utils.c @@ -42,14 +42,41 @@ int check_admin() return 0; } +// 展开环境变量 +char* expand_env_vars(const char* path) +{ + if (!path) return NULL; + + // 先转换为宽字符,因为ExpandEnvironmentStringsW不支持UTF-8 + wchar_t *wpath = utf8_to_wide(path); + if (!wpath) return NULL; + + DWORD size = ExpandEnvironmentStringsW(wpath, NULL, 0); + if (size == 0) { + free(wpath); + return NULL; + } + + wchar_t *wexpanded = (wchar_t *)malloc(size * sizeof(wchar_t)); + ExpandEnvironmentStringsW(wpath, wexpanded, size); + free(wpath); + + char *expanded = wide_to_utf8(wexpanded); + free(wexpanded); + + return expanded; +} + // 检查路径是否存在 static int path_exists(const char *path) { - // 如果包含 %,说明是变量,无法直接检查存在性,默认视为有效 - if (strchr(path, '%')) - return 1; + char *expanded_path = expand_env_vars(path); + if (!expanded_path) + return 0; - wchar_t *wpath = utf8_to_wide(path); + wchar_t *wpath = utf8_to_wide(expanded_path); + free(expanded_path); + if (!wpath) return 0; @@ -84,7 +111,11 @@ void refresh_single_list_style(Ihandle *list) continue; // 默认颜色:黑字 - char fg_color[32] = "0 0 0"; + char fg_color[32]; + if (is_dark_mode) + strcpy(fg_color, "255 255 255"); + else + strcpy(fg_color, "0 0 0"); // 1. 检查有效性 if (!path_exists(item)) @@ -108,13 +139,19 @@ void refresh_single_list_style(Ihandle *list) IupSetAttributeId(list, "ITEMFGCOLOR", i, fg_color); // 斑马纹背景 - if (i % 2 == 0) + if (is_dark_mode) { - IupSetAttributeId(list, "ITEMBGCOLOR", i, "245 245 245"); + if (i % 2 == 0) + IupSetAttributeId(list, "ITEMBGCOLOR", i, "60 60 60"); + else + IupSetAttributeId(list, "ITEMBGCOLOR", i, "50 50 50"); } else { - IupSetAttributeId(list, "ITEMBGCOLOR", i, "255 255 255"); + if (i % 2 == 0) + IupSetAttributeId(list, "ITEMBGCOLOR", i, "245 245 245"); + else + IupSetAttributeId(list, "ITEMBGCOLOR", i, "255 255 255"); } } } @@ -230,4 +267,73 @@ void clear_string_list(StringList *list) list->items = NULL; list->count = 0; list->capacity = 0; +} + +// 复制字符串列表 +void copy_string_list(StringList *dest, StringList *src) +{ + init_string_list(dest); + if (!src || src->count == 0) + return; + for (int i = 0; i < src->count; i++) + { + add_string_list(dest, src->items[i]); + } +} + +// 初始化历史栈 +void init_history_stack(HistoryStack *stack) +{ + stack->top = NULL; + stack->count = 0; +} + +// 压入历史 +void push_history(HistoryStack *stack, StringList *sys, StringList *user) +{ + HistoryNode *node = (HistoryNode *)malloc(sizeof(HistoryNode)); + if (!node) + return; + + copy_string_list(&node->sys_paths, sys); + copy_string_list(&node->user_paths, user); + + node->next = stack->top; + stack->top = node; + stack->count++; + + // 简单限制:如果超过 50 个,就不处理底部了(太麻烦),反正内存够用 +} + +// 弹出历史 +int pop_history(HistoryStack *stack, StringList *out_sys, StringList *out_user) +{ + if (!stack->top) + return 0; + + HistoryNode *node = stack->top; + stack->top = node->next; + stack->count--; + + // 转移所有权,避免复制 + *out_sys = node->sys_paths; + *out_user = node->user_paths; + + free(node); + return 1; +} + +// 清空历史栈 +void clear_history_stack(HistoryStack *stack) +{ + while (stack->top) + { + HistoryNode *node = stack->top; + stack->top = node->next; + + clear_string_list(&node->sys_paths); + clear_string_list(&node->user_paths); + free(node); + } + stack->count = 0; } \ No newline at end of file