diff --git a/include/controller/callbacks.h b/include/controller/callbacks.h index 1205c2b..38530ba 100644 --- a/include/controller/callbacks.h +++ b/include/controller/callbacks.h @@ -17,6 +17,7 @@ int btn_ok_cb(Ihandle *self); int btn_cancel_cb(Ihandle *self); int btn_help_cb(Ihandle *self); int btn_lang_cb(Ihandle *self); +int darkmode_cb(Ihandle *self); // 撤销/重做回调 int btn_undo_cb(Ihandle *self); diff --git a/include/ui/ui_utils.h b/include/ui/ui_utils.h index 9b57c4e..eec015c 100644 --- a/include/ui/ui_utils.h +++ b/include/ui/ui_utils.h @@ -17,4 +17,8 @@ void refresh_single_list_style(Ihandle *list); // 会先清空列表然后重新添加所有项,最后刷新样式 void sync_string_list_to_ui(Ihandle *list_ui, const StringList *str_list); +// 深色模式状态管理 +void set_dark_mode(int enabled); +int get_dark_mode(void); + #endif // UI_UTILS_H diff --git a/include/utils/ui_constants.h b/include/utils/ui_constants.h index b052aa8..bae36bc 100644 --- a/include/utils/ui_constants.h +++ b/include/utils/ui_constants.h @@ -35,6 +35,7 @@ #define CTRL_BTN_CANCEL "BTN_CANCEL" #define CTRL_BTN_HELP "BTN_HELP" #define CTRL_BTN_LANG "BTN_LANG" +#define CTRL_BTN_DARKMODE "BTN_DARKMODE" // 撤销/重做按钮 #define CTRL_BTN_UNDO "BTN_UNDO" diff --git a/locale/en_US/LC_MESSAGES/en_US.mo b/locale/en_US/LC_MESSAGES/en_US.mo index 439c72f..b4a29a4 100644 Binary files a/locale/en_US/LC_MESSAGES/en_US.mo and b/locale/en_US/LC_MESSAGES/en_US.mo differ diff --git a/locale/zh_CN/LC_MESSAGES/zh_CN.mo b/locale/zh_CN/LC_MESSAGES/zh_CN.mo index 375f38b..2048a6a 100644 Binary files a/locale/zh_CN/LC_MESSAGES/zh_CN.mo and b/locale/zh_CN/LC_MESSAGES/zh_CN.mo differ diff --git a/lua/config.lua b/lua/config.lua index 5d7ca2b..374ee5f 100644 --- a/lua/config.lua +++ b/lua/config.lua @@ -26,6 +26,18 @@ local config = { backcolor = "255 255 255" }, + -- 主题颜色 + theme = { + light_bg = "240 240 240", + light_list_bg = "255 255 255", + light_list_alt = "245 245 245", + light_fg = "0 0 0", + dark_bg = "30 30 30", + dark_list_bg = "40 40 40", + dark_list_alt = "50 50 50", + dark_fg = "220 220 220", + }, + -- 按钮设置(使用英文原文,供 gettext 翻译) button = { rastersize = "100x32", @@ -43,6 +55,8 @@ local config = { help = "Help", undo = "Undo", redo = "Redo", + darkmode = "Dark Mode", + lightmode = "Light Mode", }, -- 标签文本(使用英文原文,供 gettext 翻译) diff --git a/po/compile_mo.py b/po/compile_mo.py new file mode 100644 index 0000000..15ef553 --- /dev/null +++ b/po/compile_mo.py @@ -0,0 +1,67 @@ +import struct, re, os + +def compile_po_to_mo(po_path, mo_path): + with open(po_path, 'r', encoding='utf-8') as f: + content = f.read() + + entries = [] + for match in re.finditer( + r'msgid "(.+?)"\s*\nmsgstr "(.*?)"', + content, re.DOTALL + ): + msgid = match.group(1) + msgstr = match.group(2) + if msgid: + entries.append((msgid, msgstr)) + + if not entries: + print(f'No entries in {po_path}') + return + + N = len(entries) + header_sz = 28 + table_sz = N * 8 * 2 + + orig_off = header_sz + table_sz + orig_data = b'' + orig_offsets = [] + for eid, estr in entries: + orig_offsets.append(len(orig_data)) + orig_data += eid.encode('utf-8') + b'\x00' + + trans_off = orig_off + len(orig_data) + trans_data = b'' + trans_offsets = [] + for eid, estr in entries: + trans_offsets.append(len(trans_data)) + trans_data += estr.encode('utf-8') + b'\x00' + + buf = bytearray() + buf += struct.pack(' {mo_path} ({N} strings)') + +base = os.path.dirname(os.path.abspath(__file__)) +root = os.path.dirname(base) + +compile_po_to_mo(os.path.join(root, 'po', 'zh_CN.po'), os.path.join(root, 'locale', 'zh_CN', 'LC_MESSAGES', 'zh_CN.mo')) +compile_po_to_mo(os.path.join(root, 'po', 'en_US.po'), os.path.join(root, 'locale', 'en_US', 'LC_MESSAGES', 'en_US.mo')) +compile_po_to_mo(os.path.join(root, 'po', 'zh_CN.po'), os.path.join(root, 'build', 'locale', 'zh_CN', 'LC_MESSAGES', 'zh_CN.mo')) +compile_po_to_mo(os.path.join(root, 'po', 'en_US.po'), os.path.join(root, 'build', 'locale', 'en_US', 'LC_MESSAGES', 'en_US.mo')) diff --git a/po/en_US.po b/po/en_US.po index 7bb3616..89359c6 100644 --- a/po/en_US.po +++ b/po/en_US.po @@ -406,4 +406,12 @@ msgstr "Please select an item to delete first" #: src/controller/callbacks_basic.c msgid "This path already exists and will not be added again." -msgstr "This path already exists and will not be added again." \ No newline at end of file +msgstr "This path already exists and will not be added again." + +#: src/ui/main_window.c +msgid "Dark Mode" +msgstr "Dark Mode" + +#: src/ui/main_window.c +msgid "Light Mode" +msgstr "Light Mode" \ No newline at end of file diff --git a/po/messages.pot b/po/messages.pot index 69dbf35..82bb00b 100644 --- a/po/messages.pot +++ b/po/messages.pot @@ -124,3 +124,11 @@ msgstr "" #: src/controller/callbacks_nav.c msgid "Redo completed" msgstr "" + +#: src/ui/main_window.c +msgid "Dark Mode" +msgstr "" + +#: src/ui/main_window.c +msgid "Light Mode" +msgstr "" diff --git a/po/zh_CN.po b/po/zh_CN.po index 8412ab6..5c92c63 100644 --- a/po/zh_CN.po +++ b/po/zh_CN.po @@ -406,4 +406,12 @@ msgstr "请先选择要删除的项" #: src/controller/callbacks_basic.c msgid "This path already exists and will not be added again." -msgstr "该路径已存在,不会重复添加。" \ No newline at end of file +msgstr "该路径已存在,不会重复添加。" + +#: src/ui/main_window.c +msgid "Dark Mode" +msgstr "深色模式" + +#: src/ui/main_window.c +msgid "Light Mode" +msgstr "浅色模式" \ No newline at end of file diff --git a/src/controller/callbacks_sys.c b/src/controller/callbacks_sys.c index e13ba7f..a090e01 100644 --- a/src/controller/callbacks_sys.c +++ b/src/controller/callbacks_sys.c @@ -226,3 +226,36 @@ int dlg_k_any_cb(Ihandle *self, int c) } return IUP_DEFAULT; } + +// 深色模式切换回调 +int darkmode_cb(Ihandle *self) +{ + Ihandle *dlg = IupGetDialog(self); + int dark = !get_dark_mode(); + set_dark_mode(dark); + + IupSetAttribute(dlg, "BGCOLOR", dark + ? lua_config_get_string("theme", "dark_bg") + : lua_config_get_string("theme", "light_bg")); + + IupSetAttribute(self, "TITLE", _(dark + ? lua_config_get_string("button", "lightmode") + : lua_config_get_string("button", "darkmode"))); + + Ihandle *lists[] = { + IupGetDialogChild(dlg, CTRL_LIST_SYS), + IupGetDialogChild(dlg, CTRL_LIST_USER), + IupGetDialogChild(dlg, CTRL_LIST_MERGED), + NULL + }; + for (int i = 0; lists[i]; i++) + { + const char *list_bg = dark + ? lua_config_get_string("theme", "dark_list_bg") + : lua_config_get_string("theme", "light_list_bg"); + IupSetAttribute(lists[i], "BGCOLOR", list_bg); + refresh_single_list_style(lists[i]); + } + + return IUP_DEFAULT; +} diff --git a/src/core/lua_config.c b/src/core/lua_config.c index 4efddc9..88b8403 100644 --- a/src/core/lua_config.c +++ b/src/core/lua_config.c @@ -63,6 +63,14 @@ static const char *get_string_default(const char *section, const char *key) return "取消"; if (strcmp(key, "help") == 0) return "帮助(?)"; + if (strcmp(key, "undo") == 0) + return "撤销"; + if (strcmp(key, "redo") == 0) + return "重做"; + if (strcmp(key, "darkmode") == 0) + return "深色模式"; + if (strcmp(key, "lightmode") == 0) + return "浅色模式"; } else if (strcmp(section, "label") == 0) { @@ -74,11 +82,32 @@ static const char *get_string_default(const char *section, const char *key) return "系统变量 (System)"; if (strcmp(key, "tab_user") == 0) return "用户变量 (User)"; + if (strcmp(key, "tab_merged") == 0) + return "合并预览"; if (strcmp(key, "export_title") == 0) return "导出 PATH"; if (strcmp(key, "import_title") == 0) return "导入 PATH"; } + else if (strcmp(section, "theme") == 0) + { + if (strcmp(key, "light_bg") == 0) + return "240 240 240"; + if (strcmp(key, "light_list_bg") == 0) + return "255 255 255"; + if (strcmp(key, "light_list_alt") == 0) + return "245 245 245"; + if (strcmp(key, "light_fg") == 0) + return "0 0 0"; + if (strcmp(key, "dark_bg") == 0) + return "30 30 30"; + if (strcmp(key, "dark_list_bg") == 0) + return "40 40 40"; + if (strcmp(key, "dark_list_alt") == 0) + return "50 50 50"; + if (strcmp(key, "dark_fg") == 0) + return "220 220 220"; + } else if (strcmp(section, "layout") == 0) { if (strcmp(key, "vbox_gap") == 0) diff --git a/src/ui/main_window.c b/src/ui/main_window.c index f710758..eb8b6e2 100644 --- a/src/ui/main_window.c +++ b/src/ui/main_window.c @@ -1,4 +1,5 @@ #include "ui/main_window.h" +#include "ui/ui_utils.h" #include "controller/callbacks.h" #include "controller/callbacks_internal.h" #include "core/lua_config.h" @@ -14,7 +15,7 @@ static Ihandle *create_path_list(const char *name) IupSetAttribute(list, "NAME", name); IupSetAttribute(list, "EXPAND", "YES"); IupSetAttribute(list, "ITEMPADDING", lua_config_get_string("list", "item_padding")); - IupSetAttribute(list, "BACKCOLOR", lua_config_get_string("list", "backcolor")); + IupSetAttribute(list, "BGCOLOR", lua_config_get_string("list", "backcolor")); IupSetAttribute(list, "BORDER", "YES"); IupSetAttribute(list, "CANFOCUS", "YES"); IupSetAttribute(list, "HLINE", "NO"); @@ -47,10 +48,10 @@ Ihandle *create_main_window(void) IupSetAttribute(list_merged, "NAME", CTRL_LIST_MERGED); IupSetAttribute(list_merged, "EXPAND", "YES"); IupSetAttribute(list_merged, "ITEMPADDING", lua_config_get_string("list", "item_padding")); - IupSetAttribute(list_merged, "BACKCOLOR", lua_config_get_string("list", "backcolor")); + IupSetAttribute(list_merged, "BGCOLOR", lua_config_get_string("list", "backcolor")); IupSetAttribute(list_merged, "BORDER", "YES"); IupSetAttribute(list_merged, "HLINE", "NO"); - IupSetAttribute(list_merged, "ACTIVE", "NO"); // 只读 + // 不设置 ACTIVE=NO,否则会禁用滚动;合并列表无编辑回调,已是只读 // 创建搜索框 Ihandle *txt_search = IupText(NULL); @@ -113,6 +114,11 @@ Ihandle *create_main_window(void) IupSetAttribute(btn_lang, "NAME", CTRL_BTN_LANG); IupSetCallback(btn_lang, "ACTION", (Icallback)btn_lang_cb); + // 创建深色模式切换按钮 + Ihandle *btn_darkmode = IupButton(_(lua_config_get_string("button", "darkmode")), NULL); + IupSetAttribute(btn_darkmode, "NAME", CTRL_BTN_DARKMODE); + IupSetCallback(btn_darkmode, "ACTION", (Icallback)darkmode_cb); + // 设置按钮回调 IupSetCallback(btn_new, "ACTION", (Icallback)btn_new_cb); IupSetCallback(btn_edit, "ACTION", (Icallback)btn_edit_cb); @@ -140,6 +146,7 @@ Ihandle *create_main_window(void) IupSetAttribute(btn_undo, "RASTERSIZE", btn_size); IupSetAttribute(btn_redo, "RASTERSIZE", btn_size); IupSetAttribute(btn_lang, "RASTERSIZE", btn_size); + IupSetAttribute(btn_darkmode, "RASTERSIZE", btn_size); // 创建操作按钮垂直布局 Ihandle *vbox_btns = IupVbox( @@ -183,7 +190,7 @@ Ihandle *create_main_window(void) IupSetAttribute(btn_help, "RASTERSIZE", btn_size); // 创建底部按钮水平布局 - Ihandle *hbox_bottom = IupHbox(lbl_status, IupFill(), btn_help, btn_lang, btn_ok, btn_cancel, NULL); + Ihandle *hbox_bottom = IupHbox(lbl_status, IupFill(), btn_help, btn_darkmode, btn_lang, btn_ok, btn_cancel, NULL); IupSetAttribute(hbox_bottom, "GAP", lua_config_get_string("layout", "hbox_gap")); IupSetAttribute(hbox_bottom, "MARGIN", lua_config_get_string("layout", "hbox_margin")); IupSetAttribute(hbox_bottom, "ALIGNMENT", lua_config_get_string("layout", "hbox_alignment")); @@ -255,4 +262,11 @@ void refresh_main_window_ui(Ihandle *main_dlg) Ihandle *btn_lang = IupGetDialogChild(main_dlg, CTRL_BTN_LANG); if (btn_lang) IupSetAttribute(btn_lang, "TITLE", _("Language")); + + // 深色模式按钮文字根据当前模式更新 + Ihandle *btn_darkmode = IupGetDialogChild(main_dlg, CTRL_BTN_DARKMODE); + if (btn_darkmode) + IupSetAttribute(btn_darkmode, "TITLE", get_dark_mode() + ? _(lua_config_get_string("button", "lightmode")) + : _(lua_config_get_string("button", "darkmode"))); } \ No newline at end of file diff --git a/src/ui/ui_utils.c b/src/ui/ui_utils.c index 91a13c9..f17eae4 100644 --- a/src/ui/ui_utils.c +++ b/src/ui/ui_utils.c @@ -1,10 +1,23 @@ #include "ui/ui_utils.h" #include "utils/os_env.h" #include "utils/string_ext.h" +#include "core/lua_config.h" #include #include #include +static int g_dark_mode = 0; + +void set_dark_mode(int enabled) +{ + g_dark_mode = enabled; +} + +int get_dark_mode(void) +{ + return g_dark_mode; +} + // 刷新列表样式(斑马纹 + 有效性检查) void refresh_single_list_style(Ihandle *list) { @@ -12,30 +25,42 @@ void refresh_single_list_style(Ihandle *list) return; int count = IupGetInt(list, "COUNT"); + // 读取主题颜色,带默认值保护 + const char *alt_color = g_dark_mode + ? lua_config_get_string("theme", "dark_list_alt") + : lua_config_get_string("theme", "light_list_alt"); + const char *bg_color = g_dark_mode + ? lua_config_get_string("theme", "dark_list_bg") + : lua_config_get_string("theme", "light_list_bg"); + const char *fg_default = g_dark_mode + ? lua_config_get_string("theme", "dark_fg") + : lua_config_get_string("theme", "light_fg"); + + // 防止 NULL 或空字符串,使用硬编码默认值 + if (!alt_color || !*alt_color) alt_color = g_dark_mode ? "50 50 50" : "245 245 245"; + if (!bg_color || !*bg_color) bg_color = g_dark_mode ? "40 40 40" : "255 255 255"; + if (!fg_default || !*fg_default) fg_default = g_dark_mode ? "220 220 220" : "0 0 0"; + for (int i = 1; i <= count; i++) { char *item = IupGetAttributeId(list, "", i); if (!item) continue; - // 默认颜色:黑字 - char fg_color[32] = "0 0 0"; + char fg_color[32]; + sprintf(fg_color, "%s", fg_default); // 1. 检查有效性 if (!is_path_valid(item)) - { - // 无效路径:红色 - sprintf(fg_color, "255 0 0"); - } + sprintf(fg_color, "255 0 0"); // 无效路径:红色 - // 2. 检查重复 (只检查当前项之前的项,如果之前出现过,当前项标橙) + // 2. 检查重复 for (int j = 1; j < i; j++) { char *prev_item = IupGetAttributeId(list, "", j); - if (prev_item && _stricmp(item, prev_item) == 0) // Windows 路径不区分大小写 + if (prev_item && _stricmp(item, prev_item) == 0) { - // 重复路径:橙色 - sprintf(fg_color, "255 128 0"); + sprintf(fg_color, "255 128 0"); // 重复路径:橙色 break; } } @@ -43,14 +68,8 @@ void refresh_single_list_style(Ihandle *list) IupSetAttributeId(list, "ITEMFGCOLOR", i, fg_color); // 斑马纹背景 - if (i % 2 == 0) - { - IupSetAttributeId(list, "ITEMBGCOLOR", i, "245 245 245"); - } - else - { - IupSetAttributeId(list, "ITEMBGCOLOR", i, "255 255 255"); - } + IupSetAttributeId(list, "ITEMBGCOLOR", i, + (i % 2 == 0) ? alt_color : bg_color); } } @@ -60,7 +79,7 @@ void sync_string_list_to_ui(Ihandle *list_ui, const StringList *str_list) if (!list_ui || !str_list) return; IupSetAttribute(list_ui, "REMOVEITEM", "ALL"); - + for (int i = 0; i < str_list->count; i++) { const char *item = string_list_get(str_list, i); @@ -78,6 +97,6 @@ void sync_string_list_to_ui(Ihandle *list_ui, const StringList *str_list) } } IupSetInt(list_ui, "COUNT", str_list->count); - + refresh_single_list_style(list_ui); -} \ No newline at end of file +}