From 951e5ec5bd468d36d9fd8e514d6f32728c8fe072 Mon Sep 17 00:00:00 2001 From: Syahda Fahreza Date: Fri, 1 May 2026 18:33:01 +0700 Subject: [PATCH 01/15] Add Indonesian localization and UI language option Add Indonesian (id_ID) localization and a selectable UI language option. Key changes: - Add src/localization_id_id.h and register Indonesian in localization arrays; expose localization_get_language_name. - Add config_language (default = auto) to config.c/.h and persist/load it from settings. - Add language combobox and related UI strings/controls in resource.h and .rc; include a UTF-8 .rc copy (res/voidImageViewer_utf8.rc). - Bump application version to 1.0.1.0 (nsis/version.nsh and resource version info) and add scratch_version.ps1 to help update versions. - Adjust some default config values (store settings in %APPDATA% by default, change navigation sort to name ascending). - Minor UI layout adjustments and new control IDs for reset window, commands, and keyboard shortcut labels. - Add auxiliary files: comments.json, issues.json, test_math.c, and update project file (vs2026/voidImageViewer.vcxproj). These changes add language support and UI elements to let users choose their preferred language and ensure the new strings/resources are included and versioned. --- README.md | 6 +- comments.json | Bin 0 -> 16590 bytes issues.json | Bin 0 -> 255982 bytes nsis/version.nsh | 2 +- res/resource.h | 21 ++- res/voidImageViewer.rc | 123 +++++++------- res/voidImageViewer_utf8.rc | 294 ++++++++++++++++++++++++++++++++ scratch_version.ps1 | 4 + src/config.c | 9 +- src/config.h | 1 + src/localization.c | 30 ++++ src/localization.h | 5 + src/localization_en_us.h | 3 + src/localization_id_id.h | 300 +++++++++++++++++++++++++++++++++ src/localization_zh_cn.h | 3 + src/version.h | 4 +- src/viv.c | 147 +++++++++++----- src/viv.h | 18 +- test_math.c | 6 + vs2026/voidImageViewer.vcxproj | 4 +- 20 files changed, 870 insertions(+), 110 deletions(-) create mode 100644 comments.json create mode 100644 issues.json create mode 100644 res/voidImageViewer_utf8.rc create mode 100644 scratch_version.ps1 create mode 100644 src/localization_id_id.h create mode 100644 test_math.c diff --git a/README.md b/README.md index 1d27215e..a98a9335 100644 --- a/README.md +++ b/README.md @@ -26,21 +26,21 @@ void Image Viewer main window: void Image Viewer General Options: -![Void Image Viewer Options General](https://www.voidtools.com/voidImageViewer.Options.General10.png) +image


void Image Viewer View Options: -![Void Image Viewer Options View](https://www.voidtools.com/voidImageViewer.Options.View10.png) +image


void Image Viewer Controls Options: -![Void Image Viewer Image Controls](https://www.voidtools.com/voidImageViewer.Options.Controls10.png) +image


diff --git a/comments.json b/comments.json new file mode 100644 index 0000000000000000000000000000000000000000..a65ec5fd093a71efa3fda518cd022a93f7060400 GIT binary patch literal 16590 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
wRkkCHHpnp9E}>p2<+ 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 + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (U.S.) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +#ifdef _WIN32 +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US +#pragma code_page(1252) +#endif //_WIN32 + +///////////////////////////////////////////////////////////////////////////// +// +// Dialog +// + +IDD_GENERAL DIALOGEX 0, 0, 244, 200 +STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_CLIPSIBLINGS +EXSTYLE WS_EX_CONTROLPARENT +FONT 8, "MS Shell Dlg", 0, 0, 0x0 +BEGIN + LTEXT "&Language:",IDC_LANGUAGE_STATIC,0,2,50,12,SS_CENTERIMAGE + COMBOBOX IDC_LANGUAGE_COMBOBOX,54,0,135,30,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + CONTROL "&Store settings in %APPDATA%\\voidImageViewer",IDC_APPDATA, + "Button",BS_AUTOCHECKBOX | WS_TABSTOP,0,20,186,10 + CONTROL "Allow multiple &instances",IDC_MULTIPLE_INSTANCES, + "Button",BS_AUTOCHECKBOX | WS_TABSTOP,0,38,186,10 + CONTROL "Start &menu shortcuts",IDC_STARTMENU,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,0,56,186,10 + GROUPBOX "Associations",IDC_STATIC,0,74,54,124 + CONTROL "&BMP",IDC_BMP,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,6,86,42,10 + CONTROL "&GIF",IDC_GIF,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,6,98,42,10 + CONTROL "IC&O",IDC_ICO,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,6,110,42,10 + CONTROL "JP&EG",IDC_JPEG,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,6,122,42,10 + CONTROL "&JPG",IDC_JPG,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,6,134,42,10 + CONTROL "&PNG",IDC_PNG,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,6,146,42,10 + CONTROL "&TIF",IDC_TIF,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,6,158,42,10 + CONTROL "TIF&F",IDC_TIFF,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,6,170,42,10 + CONTROL "&WEBP",IDC_WEBP,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,6,182,42,10 + PUSHBUTTON "Check &All",IDC_CHECKALL,60,80,54,14 + PUSHBUTTON "Check &None",IDC_CHECKNONE,60,98,54,14 +END + +IDD_OPTIONS DIALOGEX 0, 0, 360, 271 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_VISIBLE | WS_CLIPSIBLINGS | WS_CLIPCHILDREN | WS_CAPTION | WS_SYSMENU +EXSTYLE WS_EX_CONTROLPARENT +CAPTION "Options - Void Image Viewer" +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + DEFPUSHBUTTON "OK",IDOK,248,252,50,14,WS_GROUP + PUSHBUTTON "Cancel",IDCANCEL,302,252,50,14 + CONTROL "",IDC_TREE1,"SysTreeView32",TVS_SHOWSELALWAYS | TVS_TRACKSELECT | WS_BORDER | WS_TABSTOP,6,6,84,240 + CONTROL "",IDC_TAB1,"SysTabControl32",WS_TABSTOP,96,6,260,240 + CONTROL "",IDC_TAB2,"SysTabControl32",WS_TABSTOP,96,6,260,240 + CONTROL "",IDC_TAB3,"SysTabControl32",WS_TABSTOP,96,6,260,240 + LTEXT "Static",IDC_PAGEPLACEHOLDER,106,26,236,214,NOT WS_VISIBLE +END + +IDD_VIEW DIALOGEX 0, 0, 244, 183 +STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_CLIPSIBLINGS +EXSTYLE WS_EX_CONTROLPARENT +FONT 8, "MS Shell Dlg", 0, 0, 0x0 +BEGIN + LTEXT "&Shrink blit mode:",IDC_SHRINK_BLIT_MODE_STATIC,0,0,100,12,SS_CENTERIMAGE + COMBOBOX IDC_COMBO1,104,0,135,30,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "&Magnify blit mode:",IDC_MAGNIFY_BLIT_MODE_STATIC,0,19,100,8 + COMBOBOX IDC_COMBO2,104,17,135,30,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "&Title bar format:",IDC_TITLE_BAR_FORMAT_STATIC,0,36,100,8 + COMBOBOX IDC_TITLE_BAR_FORMAT,104,34,135,30,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + CONTROL "Auto si&ze window:",IDC_AUTO_ZOOM,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,0,53,100,10 + COMBOBOX IDC_COMBO4,104,51,60,30,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + CONTROL "&Play animations at least once in slideshow",IDC_LOOP_ANIMATIONS_ONCE_STATIC, + "Button",BS_AUTOCHECKBOX | WS_TABSTOP,0,70,236,10 + CONTROL "Preload &next image",IDC_PRELOAD_NEXT_IMAGE_STATIC, + "Button",BS_AUTOCHECKBOX | WS_TABSTOP,0,87,236,10 + CONTROL "Cache &last image",IDC_CACHE_LAST_IMAGE_STATIC, + "Button",BS_AUTOCHECKBOX | WS_TABSTOP,0,103,236,10 + LTEXT "&Windowed background color:",IDC_WINDOWEDBACKGROUNDCOLOR_STATIC,0,122,120,8 + PUSHBUTTON "",IDC_WINDOWEDBACKGROUNDCOLOR,124,120,50,14,BS_BITMAP + LTEXT "&Fullscreen background color:",IDC_FULLSCREENBACKGROUNDCOLOR_STATIC,0,140,120,8 + PUSHBUTTON "",IDC_FULLSCREENBACKGROUNDCOLOR,124,138,50,14,BS_BITMAP + PUSHBUTTON "Reset Window Size && Position",IDC_RESET_WINDOW,0,158,150,14 +END + +IDD_CONTROLS DIALOGEX 0, 0, 244, 216 +STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_CLIPSIBLINGS +EXSTYLE WS_EX_CONTROLPARENT +FONT 8, "MS Shell Dlg", 0, 0, 0x0 +BEGIN + LTEXT "&Left click action:",IDC_LEFT_CLICK_ACTION_STATIC,0,0,70,12,SS_CENTERIMAGE + COMBOBOX IDC_LEFTCLICKACTION,74,0,165,30,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "&Right click action:",IDC_RIGHT_CLICK_ACTION_STATIC,0,19,70,8 + COMBOBOX IDC_RIGHTCLICKACTION,74,17,165,30,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "&Mouse wheel action:",IDC_MOUSE_WHEEL_ACTION_STATIC,0,36,70,8 + COMBOBOX IDC_MOUSEWHEELACTION,74,34,165,30,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "&Commands:",IDC_COMMANDS_STATIC,0,53,70,8 + LISTBOX IDC_COMMANDS_LIST,0,64,244,78,LBS_NOINTEGRALHEIGHT | WS_VSCROLL | WS_TABSTOP + GROUPBOX "Settings for selected command",IDC_SETTINGS_FOR_SELECTED_COMMAND_STATIC,0,148,244,67 + LISTBOX IDC_KEYS_LIST,7,158,125,50,LBS_NOINTEGRALHEIGHT | WS_VSCROLL | WS_TABSTOP + PUSHBUTTON "&Add...",IDC_ADD_KEY,188,158,50,14 + PUSHBUTTON "&Edit...",IDC_EDIT_KEY,187,177,50,14 + PUSHBUTTON "Remo&ve",IDC_REMOVE_KEY,187,195,50,14 +END + +IDD_CUSTOM_RATE DIALOGEX 0, 0, 202, 63 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CLIPSIBLINGS | WS_CLIPCHILDREN | WS_CAPTION | WS_SYSMENU +CAPTION "Set Custom Rate" +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + EDITTEXT IDC_CUSTOM_RATE_EDIT,54,12,66,12,ES_AUTOHSCROLL | ES_NUMBER,WS_EX_RIGHT + DEFPUSHBUTTON "OK",IDOK,90,36,48,14 + PUSHBUTTON "Cancel",IDCANCEL,144,36,48,14 + LTEXT "&Custom rate:",IDC_STATIC,6,14,45,8 + COMBOBOX IDC_CUSTOM_RATE_TYPE_COMBO,126,12,66,30,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP +END + +IDD_ABOUT DIALOGEX 0, 0, 228, 189 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CLIPSIBLINGS | WS_CLIPCHILDREN | WS_CAPTION | WS_SYSMENU +CAPTION "About void Image Viewer" +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + DEFPUSHBUTTON "OK",IDOK,120,168,50,14 + PUSHBUTTON "Cancel",IDCANCEL,174,168,50,14 + LTEXT "",IDC_ABOUTBACK,0,0,228,54 + EDITTEXT IDC_ABOUTTITLE,30,18,192,30,ES_AUTOHSCROLL | ES_READONLY | NOT WS_BORDER + EDITTEXT IDC_ABOUTVOIDIMAGEVIEWER,30,66,192,12,ES_AUTOHSCROLL | ES_READONLY | NOT WS_BORDER + EDITTEXT IDC_ABOUTVERSION,30,84,192,12,ES_AUTOHSCROLL | ES_READONLY | NOT WS_BORDER + EDITTEXT IDC_ABOUTCOPYRIGHT,30,102,192,12,ES_AUTOHSCROLL | ES_READONLY | NOT WS_BORDER + EDITTEXT IDC_ABOUTEMAIL,30,120,192,12,ES_AUTOHSCROLL | ES_READONLY | NOT WS_BORDER + EDITTEXT IDC_ABOUTWEBSITE,30,138,192,12,ES_AUTOHSCROLL | ES_READONLY | NOT WS_BORDER +END + +IDD_EDIT_KEY DIALOGEX 0, 0, 186, 119 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "Edit Keyboard Shortcut" +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + LTEXT "Shortcut &key:",IDC_STATIC,7,7,44,8 + EDITTEXT IDC_EDIT_KEY_EDIT,7,17,172,14,ES_AUTOHSCROLL | ES_WANTRETURN + LTEXT "Shortcut key currently used by:",IDC_STATIC,7,36,102,8 + LISTBOX IDC_EDIT_KEY_CURRENTLY_USED_BY_LIST,7,47,172,43,LBS_NOINTEGRALHEIGHT | WS_VSCROLL | WS_TABSTOP + DEFPUSHBUTTON "OK",IDOK,70,98,50,14 + PUSHBUTTON "Cancel",IDCANCEL,129,98,50,14 +END + +IDD_RENAME DIALOGEX 0, 0, 186, 51 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "Rename" +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + EDITTEXT IDC_RENAME_EDIT,7,7,172,14,ES_AUTOHSCROLL + DEFPUSHBUTTON "OK",IDOK,74,30,50,14 + PUSHBUTTON "Cancel",IDCANCEL,129,30,50,14 + EDITTEXT IDC_RENAME_OLD_EDIT,7,30,40,14,ES_AUTOHSCROLL | NOT WS_VISIBLE +END + +IDD_JUMPTO DIALOGEX 0, 0, 187, 229 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CLIPSIBLINGS | WS_CLIPCHILDREN | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME +EXSTYLE WS_EX_CONTROLPARENT +CAPTION "Jump To" +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + EDITTEXT IDC_JUMPTO_EDIT,7,7,173,14,ES_AUTOHSCROLL | WS_CLIPSIBLINGS | WS_CLIPCHILDREN + LISTBOX IDC_JUMPTO_LIST,7,29,173,170,WS_CLIPSIBLINGS | WS_CLIPCHILDREN | WS_VSCROLL | WS_TABSTOP + DEFPUSHBUTTON "OK",IDOK,74,208,50,14,WS_CLIPSIBLINGS | WS_CLIPCHILDREN + PUSHBUTTON "Cancel",IDCANCEL,130,208,50,14,WS_CLIPSIBLINGS | WS_CLIPCHILDREN +END + +IDD_EVERYTHING DIALOGEX 0, 0, 186, 63 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "Search Everything" +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + EDITTEXT IDC_EVERYTHING_EDIT,6,6,173,14,ES_AUTOHSCROLL + CONTROL "Randomize",IDC_SEARCH_EVERYTHING_RANDOM,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,6,24,51,10 + DEFPUSHBUTTON "OK",IDOK,72,42,50,14 + PUSHBUTTON "Cancel",IDCANCEL,127,42,50,14 +END + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_ICON1 ICON "voidImageViewer.ico" +IDI_PREV ICON "prev.ico" +IDI_PLAY ICON "play.ico" +IDI_NEXT ICON "next.ico" +IDI_PAUSE ICON "pause.ico" +IDI_1TO1 ICON "1to1-8bit.ico" +IDI_BESTFIT ICON "bestfit.ico" + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +VS_VERSION_INFO VERSIONINFO + FILEVERSION 1,0,0,15 + PRODUCTVERSION 1,0,0,15 + FILEFLAGSMASK 0x17L +#ifdef _DEBUG + FILEFLAGS 0x1L +#else + FILEFLAGS 0x0L +#endif + FILEOS 0x4L + FILETYPE 0x0L + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904b0" + BEGIN + VALUE "FileDescription", "voidImageViewer" + VALUE "FileVersion", "1.0.0.15" + VALUE "InternalName", "voidImageViewer" + VALUE "LegalCopyright", "Copyright © 2026 voidtools" + VALUE "OriginalFilename", "voidImageViewer.exe" + VALUE "ProductName", "voidImageViewer" + VALUE "ProductVersion", "1.0.0.15" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1200 + END +END + +#endif // English (U.S.) resources +///////////////////////////////////////////////////////////////////////////// + + +///////////////////////////////////////////////////////////////////////////// +// English (Australia) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENA) +#ifdef _WIN32 +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_AUS +#pragma code_page(1252) +#endif //_WIN32 + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""afxres.h""\r\0" +END + +#endif // APSTUDIO_INVOKED + +#endif // English (Australia) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED + diff --git a/scratch_version.ps1 b/scratch_version.ps1 new file mode 100644 index 00000000..91d9bb86 --- /dev/null +++ b/scratch_version.ps1 @@ -0,0 +1,4 @@ +$c = Get-Content res\voidImageViewer.rc -Raw +$c = $c -replace '1,0,0,15', '1,0,1,0' +$c = $c -replace '1\.0\.0\.15', '1.0.1.0' +Set-Content res\voidImageViewer.rc $c -Encoding Default diff --git a/src/config.c b/src/config.c index 719eb0c1..30d0ac7e 100644 --- a/src/config.c +++ b/src/config.c @@ -29,7 +29,8 @@ static void _config_write_string(HANDLE h,const char *ascii_key,const wchar_t *s static void _config_write_utf8(HANDLE h,const utf8_t *s); static void _config_save_settings_by_location(const wchar_t *path,int is_root); -BYTE config_appdata = 0; // store settings in %APPDATA%\voidimageviewer or in the same location as voidimageviewer.exe +BYTE config_appdata = 1; // store settings in %APPDATA%\voidimageviewer or in the same location as voidimageviewer.exe +BYTE config_language = 255; // 255 = auto BYTE config_keep_centered = 1; // when zooming out, don't recenter the image. (keep cursor under the same pixel) int config_x = 0; int config_y = 0; @@ -40,8 +41,8 @@ int config_slideshow_rate = 5000; BYTE config_allow_shrinking = 1; // prevent resizing an image below 100% BYTE config_shrink_blit_mode = CONFIG_SHRINK_BLIT_MODE_HALFTONE; // shrink filter BYTE config_mag_filter = CONFIG_MAG_FILTER_COLORONCOLOR; // magnify filter -BYTE config_nav_sort = CONFIG_NAV_SORT_DATE_MODIFIED; // current navigation sort. -BYTE config_nav_sort_ascending = 0; // sort navigation ascending or descending. +BYTE config_nav_sort = CONFIG_NAV_SORT_NAME; // current navigation sort. +BYTE config_nav_sort_ascending = 1; // sort navigation ascending or descending. BYTE config_keep_aspect_ratio = 1; // stretch images with the original aspect ratio. BYTE config_fill_window = 0; // stretch the image to fill the window BYTE config_fullscreen_fill_window = 1; // same as fill_window, except this setting is used when we are fullscreen @@ -102,6 +103,7 @@ static void _config_load_settings_by_location(const wchar_t *path,int is_root) if (ini) { config_x = ini_get_int(ini,(const utf8_t *)"x",config_x); + config_language = ini_get_int(ini,(const utf8_t *)"language",config_language); config_y = ini_get_int(ini,(const utf8_t *)"y",config_y); config_wide = ini_get_int(ini,(const utf8_t *)"wide",config_wide); config_high = ini_get_int(ini,(const utf8_t *)"high",config_high); @@ -286,6 +288,7 @@ static void _config_save_settings_by_location(const wchar_t *path,int is_root) else { _config_write_int(h,"x",config_x); + _config_write_int(h,"language",config_language); _config_write_int(h,"y",config_y); _config_write_int(h,"wide",config_wide); _config_write_int(h,"high",config_high); diff --git a/src/config.h b/src/config.h index 6a6afa13..644129dd 100644 --- a/src/config.h +++ b/src/config.h @@ -59,6 +59,7 @@ void config_load_settings(void); void config_save_settings(int appdata); extern BYTE config_appdata; +extern BYTE config_language; extern BYTE config_keep_centered; extern int config_x; extern int config_y; diff --git a/src/localization.c b/src/localization.c index 691e4716..9e58b292 100644 --- a/src/localization.c +++ b/src/localization.c @@ -24,11 +24,13 @@ #include "viv.h" #include "localization_en_us.h" #include "localization_zh_cn.h" +#include "localization_id_id.h" static const utf8_t **_localization_language_array[LOCALIZATION_LANGUAGE_COUNT] = { _localization_string_array_en_us, // LOCALIZATION_LANGUAGE_ENGLISH _localization_string_array_zh_cn, // LOCALIZATION_LANGUAGE_CHINESE_SIMPLIFIED + _localization_string_array_id_id, // LOCALIZATION_LANGUAGE_INDONESIAN }; BYTE localization_language = LOCALIZATION_LANGUAGE_ENGLISH; @@ -54,6 +56,16 @@ const utf8_t *localization_get_en_us_string(localization_id_t localization_id) void localization_init(void) { + // If config_language is not Auto, use it + if (config_language != 255) + { + if (config_language < LOCALIZATION_LANGUAGE_COUNT) + { + localization_language = config_language; + return; + } + } + // Detect system language if (os_GetUserDefaultUILanguage) { @@ -69,5 +81,23 @@ void localization_init(void) { localization_language = LOCALIZATION_LANGUAGE_CHINESE_SIMPLIFIED; } + else if (langid == 0x0421) // Indonesian + { + localization_language = LOCALIZATION_LANGUAGE_INDONESIAN; + } + else + { + localization_language = LOCALIZATION_LANGUAGE_ENGLISH; + } } } + +const utf8_t *localization_get_language_name(BYTE language) +{ + if (language < LOCALIZATION_LANGUAGE_COUNT) + { + return _localization_language_array[language][LOCALIZATION_ID_LANGUAGE]; + } + + return (const utf8_t *)""; +} diff --git a/src/localization.h b/src/localization.h index 99248c5d..34e3734e 100644 --- a/src/localization.h +++ b/src/localization.h @@ -29,6 +29,7 @@ enum { LOCALIZATION_LANGUAGE_ENGLISH = 0, LOCALIZATION_LANGUAGE_CHINESE_SIMPLIFIED, + LOCALIZATION_LANGUAGE_INDONESIAN, LOCALIZATION_LANGUAGE_COUNT, }; @@ -40,6 +41,7 @@ typedef BYTE localization_id_t; // Returns UTF-8 encoded string const utf8_t *localization_get_string(localization_id_t localization_id); const utf8_t *localization_get_en_us_string(localization_id_t localization_id); +const utf8_t *localization_get_language_name(BYTE language); // Initialize language system (detects system language) void localization_init(void); @@ -49,6 +51,8 @@ enum { // Localization LOCALIZATION_ID_LANGUAGE = 0, + LOCALIZATION_ID_LANGUAGE_STATIC, + LOCALIZATION_ID_LANGUAGE_AUTO, // Application name LOCALIZATION_ID_APP_NAME, @@ -255,6 +259,7 @@ enum LOCALIZATION_ID_CACHE_LAST_IMAGE_STATIC, LOCALIZATION_ID_WINDOWED_BACKGROUND_COLOR_STATIC, LOCALIZATION_ID_FULLSCREEN_BACKGROUND_COLOR_STATIC, + LOCALIZATION_ID_OPTIONS_VIEW_RESET_WINDOW_BUTTON, LOCALIZATION_ID_LEFT_CLICK_ACTION_STATIC, LOCALIZATION_ID_RIGHT_CLICK_ACTION_STATIC, LOCALIZATION_ID_MOUSE_WHEEL_ACTION_STATIC, diff --git a/src/localization_en_us.h b/src/localization_en_us.h index 5747262c..9d2b01f6 100644 --- a/src/localization_en_us.h +++ b/src/localization_en_us.h @@ -26,6 +26,8 @@ static const utf8_t *_localization_string_array_en_us[LOCALIZATION_ID_COUNT] = { // Application name "English (US)", // LOCALIZATION_ID_LANGUAGE + "&Language:", // LOCALIZATION_ID_LANGUAGE_STATIC + "System Default", // LOCALIZATION_ID_LANGUAGE_AUTO // Application name "void Image Viewer", // LOCALIZATION_ID_APP_NAME @@ -232,6 +234,7 @@ static const utf8_t *_localization_string_array_en_us[LOCALIZATION_ID_COUNT] = "Cache &last image", // LOCALIZATION_ID_CACHE_LAST_IMAGE_STATIC, "&Windowed background color:", // LOCALIZATION_ID_WINDOWED_BACKGROUND_COLOR_STATIC, "&Fullscreen background color:", // LOCALIZATION_ID_FULLSCREEN_BACKGROUND_COLOR_STATIC, + "Reset Window Size & Position", // LOCALIZATION_ID_OPTIONS_VIEW_RESET_WINDOW_BUTTON, "&Left click action:", // LOCALIZATION_ID_LEFT_CLICK_ACTION_STATIC, "&Right click action:", // LOCALIZATION_ID_RIGHT_CLICK_ACTION_STATIC, "&Mouse wheel action:", // LOCALIZATION_ID_MOUSE_WHEEL_ACTION_STATIC, diff --git a/src/localization_id_id.h b/src/localization_id_id.h new file mode 100644 index 00000000..ef5a2fb3 --- /dev/null +++ b/src/localization_id_id.h @@ -0,0 +1,300 @@ +// +// Copyright 2026 hesphoros +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +// Indonesian localization +// Save as UTF-8 without BOM. + +static const utf8_t *_localization_string_array_id_id[LOCALIZATION_ID_COUNT] = +{ + // Nama bahasa + "Bahasa Indonesia", // LOCALIZATION_ID_LANGUAGE + "&Bahasa:", // LOCALIZATION_ID_LANGUAGE_STATIC + "Default Sistem", // LOCALIZATION_ID_LANGUAGE_AUTO + + // Nama aplikasi + "void Image Viewer", // LOCALIZATION_ID_APP_NAME + + // Menu Berkas + "&Berkas", // LOCALIZATION_ID_FILE, + "&Buka Berkas...", // LOCALIZATION_ID_OPEN_FILE, + "Buka &Folder...", // LOCALIZATION_ID_OPEN_FOLDER, + "Buka Pencarian &Everything...", // LOCALIZATION_ID_OPEN_EVERYTHING_SEARCH, + "&Tambah Berkas...", // LOCALIZATION_ID_ADD_FILE, + "Tambah Folder...", // LOCALIZATION_ID_ADD_FOLDER, + "Tambah Pencarian Everything...", // LOCALIZATION_ID_ADD_EVERYTHING_SEARCH, + "Buka &Lokasi Berkas...", // LOCALIZATION_ID_OPEN_FILE_LOCATION, + "&Ubah...", // LOCALIZATION_ID_EDIT, + "P&ratinjau...", // LOCALIZATION_ID_PREVIEW, + "&Cetak...", // LOCALIZATION_ID_PRINT, + "Jadikan Wallpaper Des&ktop", // LOCALIZATION_ID_SET_DESKTOP_WALLPAPER, + "&Tutup", // LOCALIZATION_ID_CLOSE, + "&Hapus", // LOCALIZATION_ID_DELETE, + "Hapus (Tong Sampah)", // LOCALIZATION_ID_DELETE_RECYCLE, + "Hapus (Permanen)", // LOCALIZATION_ID_DELETE_PERMANENTLY, + "Ubah &Nama", // LOCALIZATION_ID_RENAME, + "P&roperti", // LOCALIZATION_ID_PROPERTIES, + "&Keluar", // LOCALIZATION_ID_EXIT, + + // Menu Ubah + "&Ubah", // LOCALIZATION_ID_EDIT_MENU, + "Po&tong", // LOCALIZATION_ID_CUT, + "&Salin", // LOCALIZATION_ID_COPY, + "Salin Nama Berkas", // LOCALIZATION_ID_COPY_FILENAME, + "Salin &Gambar", // LOCALIZATION_ID_COPY_IMAGE, + "&Tempel", // LOCALIZATION_ID_PASTE, + "Putar Se&arah Jarum Jam", // LOCALIZATION_ID_ROTATE_CLOCKWISE, + "Putar Ber&lawanan Jarum Jam", // LOCALIZATION_ID_ROTATE_COUNTERCLOCKWISE, + "Salin ke &Folder...", // LOCALIZATION_ID_COPY_TO, + "Pi&ndah ke Folder...", // LOCALIZATION_ID_MOVE_TO, + + // Menu Tampilan + "&Tampilan", // LOCALIZATION_ID_VIEW, + "Keterangan", // LOCALIZATION_ID_CAPTION, + "Bingkai", // LOCALIZATION_ID_FRAME, + "&Menu", // LOCALIZATION_ID_MENU, + "Bilah &Status", // LOCALIZATION_ID_STATUS_BAR, + "K&ontrol", // LOCALIZATION_ID_CONTROLS, + "&Preset", // LOCALIZATION_ID_PRESET, + "&Minimalis", // LOCALIZATION_ID_MINIMAL, + "&Ringkas", // LOCALIZATION_ID_COMPACT, + "&Normal", // LOCALIZATION_ID_NORMAL, + "&Layar Penuh", // LOCALIZATION_ID_FULLSCREEN, + "&Slideshow", // LOCALIZATION_ID_SLIDESHOW, + "&Ukuran Jendela", // LOCALIZATION_ID_VIEW_WINDOW_SIZE, + "50%", // LOCALIZATION_ID_VIEW_WINDOW_SIZE_50_PERCENT, + "100%", // LOCALIZATION_ID_VIEW_WINDOW_SIZE_100_PERCENT, + "200%", // LOCALIZATION_ID_VIEW_WINDOW_SIZE_200_PERCENT, + "&Pas Otomatis", // LOCALIZATION_ID_VIEW_WINDOW_SIZE_AUTO_FIT, + "&Segarkan", // LOCALIZATION_ID_REFRESH, + "&Izinkan Pengecilan", // LOCALIZATION_ID_ALLOW_SHRINKING, + "&Pertahankan Rasio Aspek", // LOCALIZATION_ID_KEEP_ASPECT_RATIO, + "&Isi Jendela", // LOCALIZATION_ID_FILL_WINDOW, + "1:1", // LOCALIZATION_ID_ONE_TO_ONE, + "Paling &Pas", // LOCALIZATION_ID_BEST_FIT, + "Geser && &Pindai", // LOCALIZATION_ID_PAN_SCAN, + "&Perbesar Ukuran", // LOCALIZATION_ID_INCREASE_SIZE, + "&Perkecil Ukuran", // LOCALIZATION_ID_DECREASE_SIZE, + "Pe&rlebar", // LOCALIZATION_ID_INCREASE_WIDTH, + "Pe&nyempit", // LOCALIZATION_ID_DECREASE_WIDTH, + "Per&tinggi", // LOCALIZATION_ID_INCREASE_HEIGHT, + "Per&pendek", // LOCALIZATION_ID_DECREASE_HEIGHT, + "&Zoom", // LOCALIZATION_ID_ZOOM, + "Zoom &Masuk", // LOCALIZATION_ID_ZOOM_IN, + "Zoom &Keluar", // LOCALIZATION_ID_ZOOM_OUT, + "&Atur Ulang", // LOCALIZATION_ID_RESET, + "Geser Ke &Atas", // LOCALIZATION_ID_MOVE_UP, + "Geser Ke &Bawah", // LOCALIZATION_ID_MOVE_DOWN, + "Geser Ke &Kiri", // LOCALIZATION_ID_MOVE_LEFT, + "Geser Ke &Kanan", // LOCALIZATION_ID_MOVE_RIGHT, + "Geser Atas Kiri", // LOCALIZATION_ID_MOVE_UP_LEFT, + "Geser Atas Kanan", // LOCALIZATION_ID_MOVE_UP_RIGHT, + "Geser Bawah Kiri", // LOCALIZATION_ID_MOVE_DOWN_LEFT, + "Geser Bawah Kanan", // LOCALIZATION_ID_MOVE_DOWN_RIGHT, + "Geser ke &Tengah", // LOCALIZATION_ID_MOVE_CENTER, + "Atur &Ulang", // LOCALIZATION_ID_PANSCAN_RESET, + "Selalu di &Atas", // LOCALIZATION_ID_ON_TOP, + "&Selalu", // LOCALIZATION_ID_ALWAYS, + "S&aat Memutar Slideshow atau Animasi", // LOCALIZATION_ID_WHILE_PLAYING_OR_ANIMATING, + "&Jangan Pernah", // LOCALIZATION_ID_NEVER, + "&Opsi...", // LOCALIZATION_ID_OPTIONS, + + // Menu Slideshow + "&Slideshow", // LOCALIZATION_ID_SLIDESHOW_MENU, + "&Putar/Jeda", // LOCALIZATION_ID_PLAY_PAUSE, + "&Kecepatan", // LOCALIZATION_ID_RATE, + "&Kurangi Kecepatan", // LOCALIZATION_ID_DECREASE_RATE, + "&Tambah Kecepatan", // LOCALIZATION_ID_INCREASE_RATE, + "250 Milidetik", // LOCALIZATION_ID_RATE_250_MILLISECONDS, + "500 Milidetik", // LOCALIZATION_ID_RATE_500_MILLISECONDS, + "&1 Detik", // LOCALIZATION_ID_RATE_1_SECOND, + "&2 Detik", // LOCALIZATION_ID_RATE_2_SECONDS, + "&3 Detik", // LOCALIZATION_ID_RATE_3_SECONDS, + "&4 Detik", // LOCALIZATION_ID_RATE_4_SECONDS, + "&5 Detik", // LOCALIZATION_ID_RATE_5_SECONDS, + "&6 Detik", // LOCALIZATION_ID_RATE_6_SECONDS, + "&7 Detik", // LOCALIZATION_ID_RATE_7_SECONDS, + "&8 Detik", // LOCALIZATION_ID_RATE_8_SECONDS, + "&9 Detik", // LOCALIZATION_ID_RATE_9_SECONDS, + "1&0 Detik", // LOCALIZATION_ID_RATE_10_SECONDS, + "20 Detik", // LOCALIZATION_ID_RATE_20_SECONDS, + "30 Detik", // LOCALIZATION_ID_RATE_30_SECONDS, + "40 Detik", // LOCALIZATION_ID_RATE_40_SECONDS, + "50 Detik", // LOCALIZATION_ID_RATE_50_SECONDS, + "1 Menit", // LOCALIZATION_ID_RATE_1_MINUTE, + "Kustom...", // LOCALIZATION_ID_CUSTOM, + + // Menu Animasi + "&Animasi", // LOCALIZATION_ID_ANIMATION, + "&Putar/Jeda", // LOCALIZATION_ID_ANIMATION_PLAY_PAUSE, + "Lompat ke &Depan", // LOCALIZATION_ID_ANIMATION_JUMP_FORWARD, + "Lompat ke &Belakang", // LOCALIZATION_ID_ANIMATION_JUMP_BACKWARD, + "Lompat Pendek ke &Depan", // LOCALIZATION_ID_ANIMATION_SHORT_JUMP_FORWARD, + "Lompat Pendek ke &Belakang", // LOCALIZATION_ID_ANIMATION_SHORT_JUMP_BACKWARD, + "Lompat Jauh ke &Depan", // LOCALIZATION_ID_ANIMATION_LONG_JUMP_FORWARD, + "Lompat Jauh ke &Belakang", // LOCALIZATION_ID_ANIMATION_LONG_JUMP_BACKWARD, + "Langkah &Frame", // LOCALIZATION_ID_ANIMATION_FRAME_STEP, + "Frame &Sebelumnya", // LOCALIZATION_ID_ANIMATION_PREVIOUS_FRAME, + "Frame Per&tama", // LOCALIZATION_ID_ANIMATION_FIRST_FRAME, + "Frame Ter&akhir", // LOCALIZATION_ID_ANIMATION_LAST_FRAME, + "&Kurangi Kecepatan", // LOCALIZATION_ID_ANIMATION_DECREASE_RATE, + "&Tambah Kecepatan", // LOCALIZATION_ID_ANIMATION_INCREASE_RATE, + "Atur &Ulang Kecepatan", // LOCALIZATION_ID_ANIMATION_RESET_RATE, + + // Menu Navigasi + "&Navigasi", // LOCALIZATION_ID_NAVIGATE, + "&Selanjutnya", // LOCALIZATION_ID_NEXT, + "&Sebelumnya", // LOCALIZATION_ID_PREVIOUS, + "&Home", // LOCALIZATION_ID_HOME, + "&End", // LOCALIZATION_ID_END, + "&Urutkan", // LOCALIZATION_ID_SORT, + "&Nama", // LOCALIZATION_ID_SORT_NAME, + "&Path Lengkap", // LOCALIZATION_ID_SORT_FULL_PATH, + "&Ukuran", // LOCALIZATION_ID_SORT_SIZE, + "Tanggal &Modifikasi", // LOCALIZATION_ID_SORT_DATE_MODIFIED, + "Tanggal &Dibuat", // LOCALIZATION_ID_SORT_DATE_CREATED, + "&Menaik (Ascending)", // LOCALIZATION_ID_SORT_ASCENDING, + "&Menurun (Descending)", // LOCALIZATION_ID_SORT_DESCENDING, + "Acak", // LOCALIZATION_ID_SHUFFLE, + "&Lompat Ke...", // LOCALIZATION_ID_JUMP_TO, + + // Menu Bantuan + "&Bantuan", // LOCALIZATION_ID_HELP, + "&Bantuan", // LOCALIZATION_ID_HELP_MENU, + "&Opsi Command Line", // LOCALIZATION_ID_COMMAND_LINE_OPTIONS, + "&Halaman Utama", // LOCALIZATION_ID_HOME_PAGE, + "&Donasi", // LOCALIZATION_ID_DONATE, + "&Tentang", // LOCALIZATION_ID_ABOUT, + "https://www.voidtools.com/support/voidimageviewer/", // LOCALIZATION_ID_HELP_SUPPORT_URL, + "https://www.voidtools.com/donate/", // LOCALIZATION_ID_HELP_DONATE_URL, + "https://www.voidtools.com/", // LOCALIZATION_ID_HELP_WEBSITE_URL, + + // Bilah Alat (Toolbar) + "Gambar Sebelumnya", // LOCALIZATION_ID_TOOLBAR_PREVIOUS_IMAGE_BUTTON, + "Gambar Selanjutnya", // LOCALIZATION_ID_TOOLBAR_NEXT_IMAGE_BUTTON, + "Putar Slideshow", // LOCALIZATION_ID_TOOLBAR_PLAY_SLIDESHOW_BUTTON, + "Jeda Slideshow", // LOCALIZATION_ID_TOOLBAR_PAUSE_SLIDESHOW_BUTTON, + "Paling Pas", // LOCALIZATION_ID_TOOLBAR_BEST_FIT_BUTTON, + "Ukuran Asli", // LOCALIZATION_ID_TOOLBAR_ACTUAL_SIZE_BUTTON, + + // Bilah Status + "PRAPEMUATAN", // LOCALIZATION_ID_STATUS_BAR_PRELOAD, + "Memuat...", // LOCALIZATION_ID_STATUS_BAR_LOADING, + "Berkas tidak ditemukan.", // LOCALIZATION_ID_STATUS_BAR_FILE_NOT_FOUND, + "Gagal memuat gambar.", // LOCALIZATION_ID_STATUS_BAR_FAILED_TO_LOAD_IMAGE, + "Slideshow diputar", // LOCALIZATION_ID_STATUS_BAR_SLIDESHOW_PLAYING, + "Pos %0.3f %0.3f, Zoom %0.3f %0.3f, Rasio Aspek %0.3f", // LOCALIZATION_ID_STATUS_BAR_POS_ZOOM_FORMAT, + "Kecepatan animasi %0.3f", // LOCALIZATION_ID_STATUS_BAR_ANIMATION_RATE_FORMAT, + "Kecepatan slideshow %d %s", // LOCALIZATION_ID_STATUS_BAR_SLIDESHOW_RATE_FORMAT, + "menit", // LOCALIZATION_ID_STATUS_BAR_MINUTES, + "detik", // LOCALIZATION_ID_STATUS_BAR_SECONDS, + "milidetik", // LOCALIZATION_ID_STATUS_BAR_MILLISECONDS, + + // Dialog Opsi + "Opsi - void Image Viewer", // LOCALIZATION_ID_OPTIONS_CAPTION, + "Umum", // LOCALIZATION_ID_OPTIONS_GENERAL_DIALOG, + "Tampilan", // LOCALIZATION_ID_OPTIONS_VIEW_DIALOG, + "Kontrol", // LOCALIZATION_ID_OPTIONS_CONTROLS_DIALOG, + "OK", // LOCALIZATION_ID_OK_BUTTON, + "Batal", // LOCALIZATION_ID_CANCEL_BUTTON, + "&Simpan pengaturan di %APPDATA%\\voidImageViewer", // LOCALIZATION_ID_STORE_SETTINGS_APPDATA, + "Izinkan beberapa &instansi", // LOCALIZATION_ID_ALLOW_MULTIPLE_INSTANCES, + "Pintasan &menu Start", // LOCALIZATION_ID_STARTMENU_SHORTCUTS, + "Asosiasi", // LOCALIZATION_ID_ASSOCIATIONS, + "Pilih &Semua", // LOCALIZATION_ID_CHECK_ALL, + "Pilih &Tidak Ada", // LOCALIZATION_ID_CHECK_NONE, + "Mode blit &pengecilan:", // LOCALIZATION_ID_SHRINK_BLIT_MODE_STATIC, + "Mode blit &pembesaran:", // LOCALIZATION_ID_MAGNIFY_BLIT_MODE, + "Nearest", // LOCALIZATION_ID_BLIT_MODE_NEAREST_COMBOBOXITEM, + "Linear", // LOCALIZATION_ID_BLIT_MODE_LINEAR_COMBOBOXITEM, + "Ukuran jendela &otomatis:", // LOCALIZATION_ID_OPTIONS_VIEW_AUTO_SIZE_WINDOW_STATIC, + "50%", // LOCALIZATION_ID_OPTIONS_VIEW_AUTO_SIZE_WINDOW_50_PERCENT_COMBOBOXITEM, + "100%", // LOCALIZATION_ID_OPTIONS_VIEW_AUTO_SIZE_WINDOW_100_PERCENT_COMBOBOXITEM, + "200%", // LOCALIZATION_ID_OPTIONS_VIEW_AUTO_SIZE_WINDOW_200_PERCENT_COMBOBOXITEM, + "Pas Otomatis", // LOCALIZATION_ID_OPTIONS_VIEW_AUTO_SIZE_WINDOW_AUTO_FIT_COMBOBOXITEM, + "&Putar animasi setidaknya sekali dalam slideshow", // LOCALIZATION_ID_PLAY_ANIMATIONS_ONCE_STATIC, + "Prapemuatan gambar &selanjutnya", // LOCALIZATION_ID_PRELOAD_NEXT_IMAGE_STATIC, + "Simpan gambar &terakhir di cache", // LOCALIZATION_ID_CACHE_LAST_IMAGE_STATIC, + "Warna latar &jendela:", // LOCALIZATION_ID_WINDOWED_BACKGROUND_COLOR_STATIC, + "Warna latar &layar penuh:", // LOCALIZATION_ID_FULLSCREEN_BACKGROUND_COLOR_STATIC, + "Atur Ulang Ukuran && Posisi Jendela", // LOCALIZATION_ID_OPTIONS_VIEW_RESET_WINDOW_BUTTON, + "Aksi klik &kiri:", // LOCALIZATION_ID_LEFT_CLICK_ACTION_STATIC, + "Aksi klik &kanan:", // LOCALIZATION_ID_RIGHT_CLICK_ACTION_STATIC, + "Aksi roda &mouse:", // LOCALIZATION_ID_MOUSE_WHEEL_ACTION_STATIC, + "&Perintah:", // LOCALIZATION_ID_COMMANDS_STATIC, + "Pengaturan untuk perintah terpilih", // LOCALIZATION_ID_SETTINGS_FOR_SELECTED_COMMAND, + "&Tambah...", // LOCALIZATION_ID_ADD_KEY_BUTTON, + "&Ubah...", // LOCALIZATION_ID_EDIT_KEY_BUTTON, + "&Hapus", // LOCALIZATION_ID_REMOVE_KEY_BUTTON, + "Tambah Pintasan Keyboard", // LOCALIZATION_ID_ADD_KEYBOARD_SHORTCUT_CAPTION, + "Ubah Pintasan Keyboard", // LOCALIZATION_ID_EDIT_KEYBOARD_SHORTCUT_CAPTION, + "&Tombol pintasan:", // LOCALIZATION_ID_SHORTCUT_KEY, + "Tombol pintasan sedang digunakan oleh:", // LOCALIZATION_ID_SHORTCUT_KEY_CURRENTLY_USED_BY, + "&Hapus", // LOCALIZATION_ID_REMOVE, + "Atur Kecepatan Kustom", // LOCALIZATION_ID_SET_CUSTOM_RATE_CAPTION, + "Kecepatan &kustom:", // LOCALIZATION_ID_CUSTOM_RATE_STATIC, + "menit", // LOCALIZATION_ID_CUSTOM_RATE_MINUTES, + "detik", // LOCALIZATION_ID_CUSTOM_RATE_SECONDS, + "milidetik", // LOCALIZATION_ID_CUSTOM_RATE_MILLISECONDS, + "Tentang void Image Viewer", // LOCALIZATION_ID_ABOUT_CAPTION, + "Ubah Nama", // LOCALIZATION_ID_RENAME_CAPTION, + "Lompat Ke", // LOCALIZATION_ID_JUMP_TO_TITLE, + "Pencarian Everything", // LOCALIZATION_ID_SEARCH_EVERYTHING, + "Hak Cipta \xC2\xA9 %d voidtools", // LOCALIZATION_ID_ABOUT_COPYRIGHT_FORMAT, + "support@voidtools.com", // LOCALIZATION_ID_ABOUT_EMAIL, + "www.voidtools.com", // LOCALIZATION_ID_ABOUT_WEBSITE, + "Buka Gambar", // LOCALIZATION_ID_OPEN_IMAGE_CAPTION, + "Semua Berkas Gambar", // LOCALIZATION_ID_OPEN_ALL_IMAGE_FILES, + "Semua Berkas", // LOCALIZATION_ID_OPEN_ALL_FILES, + "Gulir", // LOCALIZATION_ID_OPTIONS_ACTION_SCROLL_COMBOBOXITEM, + "Putar/Jeda Slideshow", // LOCALIZATION_ID_OPTIONS_ACTION_PLAY_PAUSE_SLIDESHOW_COMBOBOXITEM, + "Putar/Jeda Animasi", // LOCALIZATION_ID_OPTIONS_ACTION_PLAY_PAUSE_ANIMATION_COMBOBOXITEM, + "Zoom Masuk", // LOCALIZATION_ID_OPTIONS_ACTION_ZOOM_IN_COMBOBOXITEM, + "Gambar Selanjutnya", // LOCALIZATION_ID_OPTIONS_ACTION_NEXT_IMAGE_COMBOBOXITEM, + "Gulir 1:1", // LOCALIZATION_ID_OPTIONS_ACTION_ONE_TO_ONE_SCROLL_COMBOBOXITEM, + "Gulir/Pindah Jendela", // LOCALIZATION_ID_OPTIONS_ACTION_SCROLL_MOVE_WINDOW_COMBOBOXITEM, + "Menu Konteks", // LOCALIZATION_ID_OPTIONS_ACTION_CONTEXT_MENU_COMBOBOXITEM, + "Zoom Keluar", // LOCALIZATION_ID_OPTIONS_ACTION_ZOOM_OUT_COMBOBOXITEM, + "Gambar Sebelumnya", // LOCALIZATION_ID_OPTIONS_ACTION_PREVIOUS_IMAGE_COMBOBOXITEM, + "Zoom", // LOCALIZATION_ID_OPTIONS_ACTION_ZOOM_COMBOBOXITEM, + "Selanjutnya/Sebelumnya", // LOCALIZATION_ID_OPTIONS_ACTION_NEXT_PREV_COMBOBOXITEM, + "Sebelumnya/Selanjutnya", // LOCALIZATION_ID_OPTIONS_ACTION_PREV_NEXT_COMBOBOXITEM, + "Format bilah &judul:", // LOCALIZATION_ID_OPTIONS_TITLE_BAR_FORMAT_STATIC, + "Path Lengkap", // LOCALIZATION_ID_OPTIONS_TITLE_BAR_FORMAT_FULL_PATH_COMBOBOXITEM, + "Hanya Nama Berkas", // LOCALIZATION_ID_OPTIONS_TITLE_BAR_FORMAT_FILENAME_ONLY_COMBOBOXITEM, + "Tidak Ada", // LOCALIZATION_ID_OPTIONS_TITLE_BAR_FORMAT_NONE_COMBOBOXITEM, + "Everything tidak tersedia", // LOCALIZATION_ID_EVERYTHING_NOT_AVAILABLE_MESSAGE, + "Tambah Pencarian Everything", // LOCALIZATION_ID_EVERYTHING_ADD_EVERYTHING_SEARCH_CAPTION, + "Muat Pencarian Everything", // LOCALIZATION_ID_EVERYTHING_LOAD_EVERYTHING_SEARCH_CAPTION, + "Acak", // LOCALIZATION_ID_RANDOMIZE, + "Salin Ke", // LOCALIZATION_ID_COPY_TO_CAPTION, + "Pindah Ke", // LOCALIZATION_ID_MOVE_TO_CAPTION, + + // Deskripsi Asosiasi + "Gambar Bitmap", + "Gambar GIF Animasi", + "Berkas Ikon", + "Gambar JPEG", + "Gambar JPEG", + "Gambar PNG", + "Gambar TIFF", + "Gambar TIFF", + "Gambar WebP", +}; diff --git a/src/localization_zh_cn.h b/src/localization_zh_cn.h index 3d135697..1f5678cc 100644 --- a/src/localization_zh_cn.h +++ b/src/localization_zh_cn.h @@ -26,6 +26,8 @@ static const utf8_t *_localization_string_array_zh_cn[LOCALIZATION_ID_COUNT] = { // Language "简体中文", // LOCALIZATION_ID_LANGUAGE + "语言(&L):", // LOCALIZATION_ID_LANGUAGE_STATIC + "系统默认", // LOCALIZATION_ID_LANGUAGE_AUTO // Application name "void Image Viewer", // LOCALIZATION_ID_APP_NAME @@ -233,6 +235,7 @@ static const utf8_t *_localization_string_array_zh_cn[LOCALIZATION_ID_COUNT] = "缓存最后一张图像(&L)", // LOCALIZATION_ID_CACHE_LAST_IMAGE_STATIC, "窗口背景颜色(&W):", // LOCALIZATION_ID_WINDOWED_BACKGROUND_COLOR_STATIC, "全屏背景颜色(&F):", // LOCALIZATION_ID_FULLSCREEN_BACKGROUND_COLOR_STATIC, + "重置窗口大小和位置", // LOCALIZATION_ID_OPTIONS_VIEW_RESET_WINDOW_BUTTON, "左键操作(&L):", // LOCALIZATION_ID_LEFT_CLICK_ACTION_STATIC, "右键操作(&R):", // LOCALIZATION_ID_RIGHT_CLICK_ACTION_STATIC, "鼠标滚轮操作(&M):", // LOCALIZATION_ID_MOUSE_WHEEL_ACTION_STATIC, diff --git a/src/version.h b/src/version.h index ea4550ee..81a054d8 100644 --- a/src/version.h +++ b/src/version.h @@ -1,6 +1,6 @@ #define VERSION_YEAR 2026 #define VERSION_MAJOR 1 #define VERSION_MINOR 0 -#define VERSION_REVISION 0 -#define VERSION_BUILD 15 +#define VERSION_REVISION 1 +#define VERSION_BUILD 0 #define VERSION_TYPE "" diff --git a/src/viv.c b/src/viv.c index deff4526..26db4fc8 100644 --- a/src/viv.c +++ b/src/viv.c @@ -257,7 +257,7 @@ #define _VIV_ASSOCIATION_TIFF 0x00000080 #define _VIV_ASSOCIATION_WEBP 0x00000100 -#define _VIV_ZOOM_MAX 16 +#define _VIV_ZOOM_MAX 64 #define BCM_SETSHIELD 0x0000160C @@ -444,7 +444,7 @@ typedef struct _viv_webp_s typedef struct _viv_name_mapping_s { UINT count; - SHNAMEMAPPING *mappings; + SHNAMEMAPPING mappings[1]; }_viv_name_mapping_t; static void _viv_update_title(void); @@ -608,6 +608,7 @@ static int _viv_compare_id(const WIN32_FIND_DATA *a,const WIN32_FIND_DATA *b); static int _viv_fd_compare_name(const WIN32_FIND_DATA *a,const WIN32_FIND_DATA *b); static int _viv_fd_compare_path_and_name(const WIN32_FIND_DATA *a,const WIN32_FIND_DATA *b); static void _viv_update_1to1_scroll(int x,int y); +static HBITMAP _viv_create_bitmap(HDC hdc, int width, int height); static HBITMAP _viv_orientate_hbitmap(HBITMAP hbitmap,int counterclockwise); static void _viv_send_random_everything_search(void); static void _viv_do_mousewheel_action(int action,int delta,int x,int y); @@ -681,8 +682,6 @@ static int _viv_view_y = 0; // the current image offset in pixels static double _viv_view_ix = 0.0; // the current image offset in percent, used when resizing the window static double _viv_view_iy = 0.0; // the current image offset in percent, used when resizing the window static int _viv_zoom_pos = 0; // the current zoom level -//static float _viv_zoom_presets[_VIV_ZOOM_MAX] = {0.004815f,0.019215f,0.043060f,0.076120f,0.118079f,0.168530f,0.226989f,0.292893f,0.365607f,0.444430f,0.528603f,0.617316f,0.709715f,0.804909f,0.901983f,1.000000f}; // (1 - cos(((float)(x+1) * 1.570796f) / _VIV_ZOOM_MAX)) // this is missing cos((1 * 1.570796f) / _VIV_ZOOM_MAX), which is too small -static float _viv_zoom_presets[_VIV_ZOOM_MAX] = {0.0000,0.0100,0.0225,0.0379,0.0569,0.0806,0.1098,0.1461,0.1909,0.2465,0.3154,0.4007,0.5063,0.6372,0.7993,1.0000}; // 0.01 - 0.2 curve static ULONG_PTR os_GdiplusToken; // gdiplus handle static int _viv_image_wide = 0; // current image width @@ -988,9 +987,7 @@ _viv_default_key_t _viv_default_keys[] = {VIV_ID_EDIT_COPY,CONFIG_KEYFLAG_CTRL | 'C'}, {VIV_ID_EDIT_COPY_FILENAME,CONFIG_KEYFLAG_CTRL | CONFIG_KEYFLAG_SHIFT | 'C'}, {VIV_ID_EDIT_PASTE,CONFIG_KEYFLAG_CTRL | 'V'}, - {VIV_ID_VIEW_PRESET_1,'1'}, - {VIV_ID_VIEW_PRESET_2,'2'}, - {VIV_ID_VIEW_PRESET_3,'3'}, + {VIV_ID_VIEW_1TO1,CONFIG_KEYFLAG_CTRL | CONFIG_KEYFLAG_ALT | '0'}, {VIV_ID_VIEW_FULLSCREEN,CONFIG_KEYFLAG_ALT | VK_RETURN}, {VIV_ID_VIEW_SLIDESHOW,VK_F11}, @@ -5260,6 +5257,7 @@ static int _viv_init(int nCmdShow) // load settings config_load_settings(); + localization_init(); // config_maximized will be overwritten when we show are normal window // so save it now and apply it later. @@ -6975,23 +6973,11 @@ static void _viv_get_render_size(int *prw,int *prh) if (config_keep_aspect_ratio) { - if (rw > _viv_image_wide) - { - max_zoom_wide = rw * 16; - } - else - { - max_zoom_wide = _viv_image_wide * 16; - } + max_zoom_wide = rw * 16; + if (max_zoom_wide < _viv_image_wide * 16) max_zoom_wide = _viv_image_wide * 16; - if (rh > _viv_image_high) - { - max_zoom_high = rh * 16; - } - else - { - max_zoom_high = _viv_image_high * 16; - } + max_zoom_high = rh * 16; + if (max_zoom_high < _viv_image_high * 16) max_zoom_high = _viv_image_high * 16; } else { @@ -7011,8 +6997,9 @@ static void _viv_get_render_size(int *prw,int *prh) */ if (_viv_zoom_pos) { - rw = rw + (int)((max_zoom_wide - rw) * _viv_zoom_presets[_viv_zoom_pos]); - rh = rh + (int)((max_zoom_high - rh) * _viv_zoom_presets[_viv_zoom_pos]); + double zoom_factor = pow((double)max_zoom_wide / (double)rw, (double)_viv_zoom_pos / (double)(_VIV_ZOOM_MAX - 1)); + rw = (int)((double)rw * zoom_factor); + rh = (int)((double)rh * zoom_factor); } *prw = rw; @@ -7212,7 +7199,7 @@ static void _viv_delete(int permanently) fo.hwnd = _viv_hwnd; fo.wFunc = FO_DELETE; fo.pFrom = filename_list; - fo.fFlags = permanently ? 0 : FOF_ALLOWUNDO; + fo.fFlags = permanently ? FOF_WANTNUKEWARNING : FOF_ALLOWUNDO; if (SHFileOperation(&fo) == 0) { @@ -7295,8 +7282,8 @@ static INT_PTR CALLBACK _viv_rename_proc(HWND hwnd,UINT msg,WPARAM wParam,LPARAM fo.pTo = new_filename_list; fo.fFlags = FOF_ALLOWUNDO | FOF_WANTMAPPINGHANDLE; - // returns ERROR_CANCELLED if user cancelled. - if (SHFileOperation(&fo) == 0) + int sh_ret = SHFileOperation(&fo); + if (sh_ret == 0) { if (fo.fAnyOperationsAborted) { @@ -7316,7 +7303,7 @@ static INT_PTR CALLBACK _viv_rename_proc(HWND hwnd,UINT msg,WPARAM wParam,LPARAM mappings = (_viv_name_mapping_t *)fo.hNameMappings; - if (mappings->count == 1) + if (mappings->count >= 1) { // use the resolved name incase there was a rename collision. file_op_new_name = mappings->mappings[0].pszNewPath; @@ -7340,6 +7327,11 @@ static INT_PTR CALLBACK _viv_rename_proc(HWND hwnd,UINT msg,WPARAM wParam,LPARAM SHFreeNameMappings(fo.hNameMappings); } } + else + { + // returned non-zero (user cancelled or clicked No) + dont_end_dialog = 1; + } } if (!dont_end_dialog) @@ -7505,7 +7497,7 @@ static void _viv_set_clipboard_image(void) { HBITMAP mem1_hbitmap; - mem1_hbitmap = CreateCompatibleBitmap(screen_hdc,_viv_image_wide,_viv_image_high); + mem1_hbitmap = _viv_create_bitmap(screen_hdc,_viv_image_wide,_viv_image_high); if (mem1_hbitmap) { HGDIOBJ last_mem1_hbitmap; @@ -7936,8 +7928,26 @@ static INT_PTR CALLBACK _viv_options_general_proc(HWND hwnd,UINT msg,WPARAM wPar case WM_INITDIALOG: { int exti; - os_SetDlgItemText_localization_id(hwnd,IDC_APPDATA,LOCALIZATION_ID_STORE_SETTINGS_APPDATA); + os_SetDlgItemText_localization_id(hwnd,IDC_LANGUAGE_STATIC,LOCALIZATION_ID_LANGUAGE_STATIC); + + { + HWND hLanguage = GetDlgItem(hwnd,IDC_LANGUAGE_COMBOBOX); + int index; + + index = os_ComboBox_AddString_localization_id(hwnd,IDC_LANGUAGE_COMBOBOX,LOCALIZATION_ID_LANGUAGE_AUTO); + ComboBox_SetItemData(hLanguage,index,255); + if (config_language == 255) ComboBox_SetCurSel(hLanguage,index); + + for(int i=0;iscreen_hdc,viv_webp->wide,viv_webp->high); + hbitmap = _viv_create_bitmap(viv_webp->screen_hdc,viv_webp->wide,viv_webp->high); // Set RGB data to the bitmap if (hbitmap) @@ -10612,7 +10666,7 @@ static DWORD WINAPI _viv_load_image_thread_proc(void *param) break; } - hbitmap = CreateCompatibleBitmap(screen_hdc,load_wide,load_high); + hbitmap = _viv_create_bitmap(screen_hdc,load_wide,load_high); if (hbitmap) { UINT image_flags; @@ -11974,7 +12028,7 @@ static void _viv_update_color_button_bitmap(HWND hwnd) screen_hdc = GetDC(0); mem_hdc = CreateCompatibleDC(screen_hdc); - hbitmap = CreateCompatibleBitmap(screen_hdc,wide,high); + hbitmap = _viv_create_bitmap(screen_hdc,wide,high); last_hbitmap = SelectObject(mem_hdc,hbitmap); @@ -13668,6 +13722,22 @@ static void _viv_update_1to1_scroll(int x,int y) // #define PHOTO_ORIENTATION_ROTATE270 6u // #define PHOTO_ORIENTATION_TRANSVERSE 7u // #define PHOTO_ORIENTATION_ROTATE90 8u +static HBITMAP _viv_create_bitmap(HDC hdc, int width, int height) +{ + BITMAPINFO bmi; + void* bits; + + ZeroMemory(&bmi, sizeof(BITMAPINFO)); + bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); + bmi.bmiHeader.biWidth = width; + bmi.bmiHeader.biHeight = -height; + bmi.bmiHeader.biPlanes = 1; + bmi.bmiHeader.biBitCount = 32; + bmi.bmiHeader.biCompression = BI_RGB; + + return CreateDIBSection(hdc, &bmi, DIB_RGB_COLORS, &bits, NULL, 0); +} + static HBITMAP _viv_orientate_hbitmap(HBITMAP hbitmap,int orientation) { BITMAP bitmap; @@ -13706,7 +13776,7 @@ static HBITMAP _viv_orientate_hbitmap(HBITMAP hbitmap,int orientation) break; } - ret_hbitmap = CreateCompatibleBitmap(screen_hdc,ret_wide,ret_high); + ret_hbitmap = _viv_create_bitmap(screen_hdc,ret_wide,ret_high); if (ret_hbitmap) { int ret; @@ -14223,7 +14293,7 @@ static HBITMAP _viv_get_mipmap(HBITMAP hbitmap,int image_wide,int image_high,int if (mem2_hdc) { (*pmip)->mipmap = NULL; - (*pmip)->hbitmap = CreateCompatibleBitmap(screen_hdc,mip_wide,mip_high); + (*pmip)->hbitmap = _viv_create_bitmap(screen_hdc,mip_wide,mip_high); last_hbitmap = SelectObject(mem_hdc,(*pmip)->hbitmap); last2_hbitmap = SelectObject(mem2_hdc,best_hbitmap); @@ -14883,7 +14953,7 @@ static void _viv_stretch_blt(HDC dst_hdc,int dst_x,int dst_y,int dst_wide,int ds { HBITMAP dst_hbitmap; - dst_hbitmap = CreateCompatibleBitmap(dst_hdc,clip_wide,clip_high); + dst_hbitmap = _viv_create_bitmap(dst_hdc,clip_wide,clip_high); if (dst_hbitmap) { HGDIOBJ last_dst_hbitmap; @@ -15321,3 +15391,4 @@ static int _viv_safe_copy_data(const void *base,SIZE_T src_size,const void *src, return 1; } + diff --git a/src/viv.h b/src/viv.h index 16daba6a..37a1e94a 100644 --- a/src/viv.h +++ b/src/viv.h @@ -57,9 +57,25 @@ typedef unsigned __int64 VIV_UINT64; #include #include //#include +#define IDC_STATIC -1 +#define IDC_ASSOCIATIONS_GROUPBOX IDC_STATIC +#define IDC_ADD_KEY_BUTTON IDC_ADD_KEY +#define IDC_EDIT_KEY_BUTTON IDC_EDIT_KEY +#define IDC_REMOVE_KEY_BUTTON IDC_REMOVE_KEY +#define IDC_LEFTCLICKACTION_COMBOBOX IDC_LEFTCLICKACTION +#define IDC_RIGHTCLICKACTION_COMBOBOX IDC_RIGHTCLICKACTION +#define IDC_MOUSEWHEELACTION_COMBOBOX IDC_MOUSEWHEELACTION +#define IDC_SHRINK_BLIT_MODE_COMBOBOX IDC_COMBO1 +#define IDC_MAGNIFY_BLIT_MODE_COMBOBOX IDC_COMBO2 +#define IDC_TITLE_BAR_FORMAT_COMBOBOX IDC_TITLE_BAR_FORMAT +#define IDC_AUTO_SIZE_WINDOW_COMBOBOX IDC_COMBO4 +#define IDC_WINDOWEDBACKGROUNDCOLOR_BUTTON IDC_WINDOWEDBACKGROUNDCOLOR +#define IDC_FULLSCREENBACKGROUNDCOLOR_BUTTON IDC_FULLSCREENBACKGROUNDCOLOR +#define IDC_CUSTOM_RATE_TYPE_COMBOBOX IDC_CUSTOM_RATE_TYPE_COMBO +#define IDC_SEARCH_EVERYTHING_RANDOM_CHECKBOX IDC_SEARCH_EVERYTHING_RANDOM #include "../res/resource.h" //#include -//#include +#include //#include //#include #include // OPENFILENAME diff --git a/test_math.c b/test_math.c new file mode 100644 index 00000000..441c4d4d --- /dev/null +++ b/test_math.c @@ -0,0 +1,6 @@ +#include +#include +int main() { + printf("%f\n", pow(2.0, 3.0)); + return 0; +} diff --git a/vs2026/voidImageViewer.vcxproj b/vs2026/voidImageViewer.vcxproj index a5f40a8e..8d6323f7 100644 --- a/vs2026/voidImageViewer.vcxproj +++ b/vs2026/voidImageViewer.vcxproj @@ -465,7 +465,7 @@ /MP %(AdditionalOptions) Disabled ..\libwebp;%(AdditionalIncludeDirectories) - BZ_NO_STDIO;VERSION_DEBUG;BZ_NO_STDIO;%(PreprocessorDefinitions) + WIN32;_DEBUG;_WINDOWS;BZ_NO_STDIO;VERSION_DEBUG;VERSION_X86;%(PreprocessorDefinitions) false Async MultiThreadedDebug @@ -606,7 +606,7 @@ false true ..\libwebp;%(AdditionalIncludeDirectories) - BZ_NO_STDIO;BZ_NO_STDIO;%(PreprocessorDefinitions) + WIN32;NDEBUG;_WINDOWS;BZ_NO_STDIO;VERSION_X86;%(PreprocessorDefinitions) true MultiThreaded Fast From 074282112c2c2b0ce5054c0df8e1f467b6956a8b Mon Sep 17 00:00:00 2001 From: Syahda Fahreza Date: Fri, 1 May 2026 18:39:32 +0700 Subject: [PATCH 02/15] Restore GitHub Actions workflow with NSIS support --- .github/workflows/build.yml | 40 +++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..d0a4c206 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,40 @@ +name: Build + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + runs-on: windows-latest + + steps: + - uses: actions/checkout@v4 + + - name: Add MSBuild to PATH + uses: microsoft/setup-msbuild@v2 + + - name: Build x64 + run: msbuild vs2026/voidImageViewer.sln /p:Configuration=Release /p:Platform=x64 /p:PlatformToolset=v143 + + - name: Build x86 + run: msbuild vs2026/voidImageViewer.sln /p:Configuration=Release /p:Platform=Win32 /p:PlatformToolset=v143 + + - name: Setup NSIS + run: | + choco install nsis -y + echo "C:\Program Files (x86)\NSIS" >> $GITHUB_PATH + + - name: Build Installer x64 + run: powershell -File nsis/build_installer.ps1 -Arch x64 -VsVersion vs2026 -BuildConfig Release -Lang English + + - name: Upload Artifacts + uses: actions/upload-artifact@v4 + with: + name: voidImageViewer-build + path: | + voidImageViewer/x64/Release/voidImageViewer.exe + voidImageViewer/Release/voidImageViewer.exe + nsis/voidImageViewer-*-setup.exe From f5edbf5601aef7cb054e8cab40ae4d179137a92b Mon Sep 17 00:00:00 2001 From: Syahda Fahreza Date: Fri, 1 May 2026 18:42:02 +0700 Subject: [PATCH 03/15] Fix NSIS path in GitHub Actions --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d0a4c206..e83fc29e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,7 +25,7 @@ jobs: - name: Setup NSIS run: | choco install nsis -y - echo "C:\Program Files (x86)\NSIS" >> $GITHUB_PATH + "C:\Program Files (x86)\NSIS" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - name: Build Installer x64 run: powershell -File nsis/build_installer.ps1 -Arch x64 -VsVersion vs2026 -BuildConfig Release -Lang English From d199e6b8c040ead80f048a7938bf987674eb8604 Mon Sep 17 00:00:00 2001 From: Syahda Fahreza Date: Fri, 1 May 2026 18:46:47 +0700 Subject: [PATCH 04/15] Add x86 installer build to GitHub Actions --- .github/workflows/build.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e83fc29e..b818c75f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,6 +30,9 @@ jobs: - name: Build Installer x64 run: powershell -File nsis/build_installer.ps1 -Arch x64 -VsVersion vs2026 -BuildConfig Release -Lang English + - name: Build Installer x86 + run: powershell -File nsis/build_installer.ps1 -Arch x86 -VsVersion vs2026 -BuildConfig Release -Lang English + - name: Upload Artifacts uses: actions/upload-artifact@v4 with: From fad8e70d94671d776831a5ec85a24112817fde59 Mon Sep 17 00:00:00 2001 From: Syahda Fahreza <87214398+syahdafahreza@users.noreply.github.com> Date: Fri, 1 May 2026 19:18:59 +0700 Subject: [PATCH 05/15] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index a98a9335..bd10c250 100644 --- a/README.md +++ b/README.md @@ -26,21 +26,21 @@ void Image Viewer main window: void Image Viewer General Options: -image +image


void Image Viewer View Options: -image +image


void Image Viewer Controls Options: -image +image


From eeda1779bfb40b6bf8c5693a79c393a884bd2e79 Mon Sep 17 00:00:00 2001 From: Syahda Fahreza Date: Tue, 25 Aug 2026 18:31:12 +0700 Subject: [PATCH 06/15] Add Zoom options and calculation engine Introduce a new Zoom options tab (IDD_ZOOM) with localized UI (en/ID/zh-CN). Add config entries (zoom_calc_method, zoom_step, zoom_fixed_table) with defaults and INI persistence. Implement zoom calculation methods: Relative (% multiplier), Absolute (additive %), and Fixed table (custom list) including parser and _viv_calculate_next_zoom. Preserve pixel under cursor during zoom, add custom-zoom state, toolbar updates, and related UI handling. Bump version to 1.1.0.0 and add changelog to README. --- README.md | 26 +++ nsis/version.nsh | 2 +- res/resource.h | 16 +- res/voidImageViewer.rc | 26 ++- res/voidImageViewer_utf8.rc | 26 ++- src/config.c | 15 ++ src/config.h | 12 +- src/localization.h | 8 + src/localization_en_us.h | 8 + src/localization_id_id.h | 8 + src/localization_zh_cn.h | 8 + src/version.h | 4 +- src/viv.c | 344 ++++++++++++++++++++++++++++++------ src/viv.h | 1 + 14 files changed, 434 insertions(+), 70 deletions(-) diff --git a/README.md b/README.md index bd10c250..4ac632c1 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,32 @@ void Image Viewer Controls Options: +## Changelog + +### Version 1.1.0.0 (Latest) +- **New Feature: Zoom Options Tab & Calculation Engine** + - Added dedicated **Zoom options** tab in Options dialog (`IDD_ZOOM`). + - Added customizable zoom calculation methods: + - **Relative step:** Multiplicative scaling using configured zoom step % (default: 10%). + - **Absolute step:** Additive scaling by fixed percentage point steps. + - **Fixed table:** Step-by-step navigation through a user-defined list of zoom percentage levels (default: `1;2;3;10;14;16;19;21;25;28;33;38;43;50;57;66;76;87;100;115;132;150;174;200;230;260;300;350;400;450;500;600;700;800;900;1000;1200;1400;1600;2000;3000;`). + - Added **Load default table** button to quickly restore default values. +- **Improved Zoom Handling:** Pixel under cursor remains stationary during mouse wheel and shortcut zooms across all zoom methods. +- **Expanded Localization:** Added full UI translations for the Zoom options page in English (`en-US`), Indonesian (`id-ID`), and Simplified Chinese (`zh-CN`). +- **Configuration Persistence:** Zoom settings (`zoom_calc_method`, `zoom_step`, `zoom_fixed_table`) automatically saved to and loaded from `voidImageViewer.ini`. + +| Feature / Behavior | Before (v1.0.1.0) | After (v1.1.0.0) | +| :--- | :--- | :--- | +| **Zoom Calculation Method** | Hardcoded fixed exponential power steps (`_viv_zoom_pos` from 0 to 63) | Choice between **Relative step (%)**, **Absolute step (%)**, or customizable **Fixed table** | +| **Zoom Step Config** | Fixed / non-configurable | Configurable zoom step percentage (default `10%`) | +| **Fixed Table Values** | Not available | Semicolon-delimited custom table support (up to 50 ordered values) with default reset | +| **Settings UI** | 3 tabs (*General*, *View*, *Controls*) | 4 tabs (*General*, *View*, *Controls*, ***Zoom options***) | +| **Localization** | Dialog strings for 3 tabs | Full multilingual translations for all 4 tabs (English, Indonesian, Chinese) | +| **INI Storage** | No zoom calculation preferences saved | Saves `zoom_calc_method`, `zoom_step`, `zoom_fixed_table` to INI | + +


+ See also -------- https://www.voidtools.com/forum/viewtopic.php?t=5623 + diff --git a/nsis/version.nsh b/nsis/version.nsh index 81406c85..211bac86 100644 --- a/nsis/version.nsh +++ b/nsis/version.nsh @@ -1,3 +1,3 @@ -!define VERSION "1.0.1.0" +!define VERSION "1.1.0.0" !define BETAVERSION "" !define VERSIONYEAR "2026" diff --git a/res/resource.h b/res/resource.h index 7aa22dcc..32e21440 100644 --- a/res/resource.h +++ b/res/resource.h @@ -25,6 +25,7 @@ #define IDD_RENAME 127 #define IDD_JUMPTO 128 #define IDD_EVERYTHING 129 +#define IDD_ZOOM 130 #define IDC_TAB1 1003 #define IDC_BMP 1006 #define IDC_GIF 1007 @@ -108,14 +109,25 @@ #define IDC_EDIT_KEYBOARD_SHORTCUT_KEY_STATIC 1070 #define IDC_EDIT_KEYBOARD_SHORTCUT_KEY_CURRENTLY_USED_BY_STATIC 1071 #define IDC_CUSTOM_RATE_STATIC 1072 +#define IDC_TAB4 1073 +#define IDC_ZOOM_STEP_STATIC 1074 +#define IDC_ZOOM_STEP_EDIT 1075 +#define IDC_ZOOM_STEP_PERCENT_STATIC 1076 +#define IDC_ZOOM_STEP_DEFAULT_STATIC 1077 +#define IDC_ZOOM_CALC_METHOD_STATIC 1078 +#define IDC_ZOOM_CALC_RELATIVE_RADIO 1079 +#define IDC_ZOOM_CALC_ABSOLUTE_RADIO 1080 +#define IDC_ZOOM_CALC_FIXED_TABLE_RADIO 1081 +#define IDC_ZOOM_FIXED_TABLE_EDIT 1082 +#define IDC_ZOOM_LOAD_DEFAULT_TABLE_BUTTON 1083 // Next default values for new objects // #ifdef APSTUDIO_INVOKED #ifndef APSTUDIO_READONLY_SYMBOLS #define _APS_NO_MFC 1 -#define _APS_NEXT_RESOURCE_VALUE 130 +#define _APS_NEXT_RESOURCE_VALUE 131 #define _APS_NEXT_COMMAND_VALUE 40048 -#define _APS_NEXT_CONTROL_VALUE 1073 +#define _APS_NEXT_CONTROL_VALUE 1084 #endif #endif diff --git a/res/voidImageViewer.rc b/res/voidImageViewer.rc index b4ae2659..4d974b8e 100644 --- a/res/voidImageViewer.rc +++ b/res/voidImageViewer.rc @@ -64,6 +64,7 @@ BEGIN CONTROL "",IDC_TAB1,"SysTabControl32",WS_TABSTOP,96,6,260,240 CONTROL "",IDC_TAB2,"SysTabControl32",WS_TABSTOP,96,6,260,240 CONTROL "",IDC_TAB3,"SysTabControl32",WS_TABSTOP,96,6,260,240 + CONTROL "",IDC_TAB4,"SysTabControl32",WS_TABSTOP,96,6,260,240 LTEXT "Static",IDC_PAGEPLACEHOLDER,106,26,236,214,NOT WS_VISIBLE END @@ -113,6 +114,23 @@ BEGIN PUSHBUTTON "Remo&ve",IDC_REMOVE_KEY,187,195,50,14 END +IDD_ZOOM DIALOGEX 0, 0, 244, 200 +STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_CLIPSIBLINGS +EXSTYLE WS_EX_CONTROLPARENT +FONT 8, "MS Shell Dlg", 0, 0, 0x0 +BEGIN + LTEXT "Zoom &step:",IDC_ZOOM_STEP_STATIC,0,2,42,12,SS_CENTERIMAGE + EDITTEXT IDC_ZOOM_STEP_EDIT,46,2,24,12,ES_AUTOHSCROLL | ES_NUMBER + LTEXT "%",IDC_ZOOM_STEP_PERCENT_STATIC,73,2,8,12,SS_CENTERIMAGE + LTEXT "(default: 10 %)",IDC_ZOOM_STEP_DEFAULT_STATIC,86,2,100,12,SS_CENTERIMAGE + LTEXT "Zoom calculation method:",IDC_ZOOM_CALC_METHOD_STATIC,0,22,244,10 + CONTROL "&Relative step",IDC_ZOOM_CALC_RELATIVE_RADIO,"Button",BS_AUTORADIOBUTTON | WS_GROUP | WS_TABSTOP,10,36,220,10 + CONTROL "&Absolute step",IDC_ZOOM_CALC_ABSOLUTE_RADIO,"Button",BS_AUTORADIOBUTTON,10,50,220,10 + CONTROL "Use &fixed table: (for experts: max. 50 ordered values from 3 to 4000)",IDC_ZOOM_CALC_FIXED_TABLE_RADIO,"Button",BS_AUTORADIOBUTTON,10,64,234,10 + EDITTEXT IDC_ZOOM_FIXED_TABLE_EDIT,18,78,140,14,ES_AUTOHSCROLL | WS_TABSTOP + PUSHBUTTON "&Load default table",IDC_ZOOM_LOAD_DEFAULT_TABLE_BUTTON,162,78,76,14,WS_TABSTOP +END + IDD_CUSTOM_RATE DIALOGEX 0, 0, 202, 63 STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CLIPSIBLINGS | WS_CLIPCHILDREN | WS_CAPTION | WS_SYSMENU CAPTION "Set Custom Rate" @@ -210,8 +228,8 @@ IDI_BESTFIT ICON "bestfit.ico" // VS_VERSION_INFO VERSIONINFO - FILEVERSION 1,0,1,0 - PRODUCTVERSION 1,0,1,0 + FILEVERSION 1,1,0,0 + PRODUCTVERSION 1,1,0,0 FILEFLAGSMASK 0x17L #ifdef _DEBUG FILEFLAGS 0x1L @@ -227,12 +245,12 @@ BEGIN BLOCK "040904b0" BEGIN VALUE "FileDescription", "voidImageViewer" - VALUE "FileVersion", "1.0.1.0" + VALUE "FileVersion", "1.1.0.0" VALUE "InternalName", "voidImageViewer" VALUE "LegalCopyright", "Copyright © 2026 voidtools" VALUE "OriginalFilename", "voidImageViewer.exe" VALUE "ProductName", "voidImageViewer" - VALUE "ProductVersion", "1.0.1.0" + VALUE "ProductVersion", "1.1.0.0" END END BLOCK "VarFileInfo" diff --git a/res/voidImageViewer_utf8.rc b/res/voidImageViewer_utf8.rc index 198d8ba4..3dafd1d0 100644 --- a/res/voidImageViewer_utf8.rc +++ b/res/voidImageViewer_utf8.rc @@ -64,6 +64,7 @@ BEGIN CONTROL "",IDC_TAB1,"SysTabControl32",WS_TABSTOP,96,6,260,240 CONTROL "",IDC_TAB2,"SysTabControl32",WS_TABSTOP,96,6,260,240 CONTROL "",IDC_TAB3,"SysTabControl32",WS_TABSTOP,96,6,260,240 + CONTROL "",IDC_TAB4,"SysTabControl32",WS_TABSTOP,96,6,260,240 LTEXT "Static",IDC_PAGEPLACEHOLDER,106,26,236,214,NOT WS_VISIBLE END @@ -113,6 +114,23 @@ BEGIN PUSHBUTTON "Remo&ve",IDC_REMOVE_KEY,187,195,50,14 END +IDD_ZOOM DIALOGEX 0, 0, 244, 200 +STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD | WS_CLIPSIBLINGS +EXSTYLE WS_EX_CONTROLPARENT +FONT 8, "MS Shell Dlg", 0, 0, 0x0 +BEGIN + LTEXT "Zoom &step:",IDC_ZOOM_STEP_STATIC,0,2,42,12,SS_CENTERIMAGE + EDITTEXT IDC_ZOOM_STEP_EDIT,46,2,24,12,ES_AUTOHSCROLL | ES_NUMBER + LTEXT "%",IDC_ZOOM_STEP_PERCENT_STATIC,73,2,8,12,SS_CENTERIMAGE + LTEXT "(default: 10 %)",IDC_ZOOM_STEP_DEFAULT_STATIC,86,2,100,12,SS_CENTERIMAGE + LTEXT "Zoom calculation method:",IDC_ZOOM_CALC_METHOD_STATIC,0,22,244,10 + CONTROL "&Relative step",IDC_ZOOM_CALC_RELATIVE_RADIO,"Button",BS_AUTORADIOBUTTON | WS_GROUP | WS_TABSTOP,10,36,220,10 + CONTROL "&Absolute step",IDC_ZOOM_CALC_ABSOLUTE_RADIO,"Button",BS_AUTORADIOBUTTON,10,50,220,10 + CONTROL "Use &fixed table: (for experts: max. 50 ordered values from 3 to 4000)",IDC_ZOOM_CALC_FIXED_TABLE_RADIO,"Button",BS_AUTORADIOBUTTON,10,64,234,10 + EDITTEXT IDC_ZOOM_FIXED_TABLE_EDIT,18,78,140,14,ES_AUTOHSCROLL | WS_TABSTOP + PUSHBUTTON "&Load default table",IDC_ZOOM_LOAD_DEFAULT_TABLE_BUTTON,162,78,76,14,WS_TABSTOP +END + IDD_CUSTOM_RATE DIALOGEX 0, 0, 202, 63 STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CLIPSIBLINGS | WS_CLIPCHILDREN | WS_CAPTION | WS_SYSMENU CAPTION "Set Custom Rate" @@ -210,8 +228,8 @@ IDI_BESTFIT ICON "bestfit.ico" // VS_VERSION_INFO VERSIONINFO - FILEVERSION 1,0,0,15 - PRODUCTVERSION 1,0,0,15 + FILEVERSION 1,1,0,0 + PRODUCTVERSION 1,1,0,0 FILEFLAGSMASK 0x17L #ifdef _DEBUG FILEFLAGS 0x1L @@ -227,12 +245,12 @@ BEGIN BLOCK "040904b0" BEGIN VALUE "FileDescription", "voidImageViewer" - VALUE "FileVersion", "1.0.0.15" + VALUE "FileVersion", "1.1.0.0" VALUE "InternalName", "voidImageViewer" VALUE "LegalCopyright", "Copyright © 2026 voidtools" VALUE "OriginalFilename", "voidImageViewer.exe" VALUE "ProductName", "voidImageViewer" - VALUE "ProductVersion", "1.0.0.15" + VALUE "ProductVersion", "1.1.0.0" END END BLOCK "VarFileInfo" diff --git a/src/config.c b/src/config.c index 30d0ac7e..c7285193 100644 --- a/src/config.c +++ b/src/config.c @@ -91,6 +91,9 @@ BYTE config_pixel_info = 0; BYTE config_orientation = 1; BYTE config_title_bar_format = 1; // 0=full path, 1=filename, 2=none int config_add_command_line_timeout = 500; // in milliseconds +BYTE config_zoom_calc_method = CONFIG_ZOOM_CALC_RELATIVE; +int config_zoom_step = CONFIG_ZOOM_DEFAULT_STEP; +wchar_t config_zoom_fixed_table[STRING_SIZE] = CONFIG_ZOOM_DEFAULT_FIXED_TABLE; static void _config_load_settings_by_location(const wchar_t *path,int is_root) { @@ -163,6 +166,15 @@ static void _config_load_settings_by_location(const wchar_t *path,int is_root) config_toolbar_move_window = ini_get_int(ini,(const utf8_t *)"toolbar_move_window",config_toolbar_move_window); config_title_bar_format = ini_get_int(ini,(const utf8_t *)"title_bar_format",config_title_bar_format); config_add_command_line_timeout = ini_get_int(ini,(const utf8_t *)"add_command_line_timeout",config_add_command_line_timeout); + config_zoom_calc_method = ini_get_int(ini,(const utf8_t *)"zoom_calc_method",config_zoom_calc_method); + config_zoom_step = ini_get_int(ini,(const utf8_t *)"zoom_step",config_zoom_step); + { + const utf8_t *fixed_table = ini_get_string(ini,(const utf8_t *)"zoom_fixed_table"); + if (fixed_table && *fixed_table) + { + string_copy_utf8_string(config_zoom_fixed_table,fixed_table); + } + } if (is_root) { @@ -348,6 +360,9 @@ static void _config_save_settings_by_location(const wchar_t *path,int is_root) _config_write_int(h,"toolbar_move_window",config_toolbar_move_window); _config_write_int(h,"title_bar_format",config_title_bar_format); _config_write_int(h,"add_command_line_timeout",config_add_command_line_timeout); + _config_write_int(h,"zoom_calc_method",config_zoom_calc_method); + _config_write_int(h,"zoom_step",config_zoom_step); + _config_write_string(h,"zoom_fixed_table",config_zoom_fixed_table); // save keys { diff --git a/src/config.h b/src/config.h index 644129dd..953bb92e 100644 --- a/src/config.h +++ b/src/config.h @@ -55,6 +55,14 @@ typedef struct config_key_s #define CONFIG_MAG_FILTER_COLORONCOLOR 0 #define CONFIG_MAG_FILTER_HALFTONE 1 +#define CONFIG_ZOOM_CALC_RELATIVE 0 +#define CONFIG_ZOOM_CALC_ABSOLUTE 1 +#define CONFIG_ZOOM_CALC_FIXED_TABLE 2 + +#define CONFIG_ZOOM_DEFAULT_STEP 10 +#define CONFIG_ZOOM_DEFAULT_FIXED_TABLE L"1;2;3;10;14;16;19;21;25;28;33;38;43;50;57;66;76;87;100;115;132;150;174;200;230;260;300;350;400;450;500;600;700;800;900;1000;1200;1400;1600;2000;3000;" +#define CONFIG_ZOOM_DEFAULT_FIXED_TABLE_A "1;2;3;10;14;16;19;21;25;28;33;38;43;50;57;66;76;87;100;115;132;150;174;200;230;260;300;350;400;450;500;600;700;800;900;1000;1200;1400;1600;2000;3000;" + void config_load_settings(void); void config_save_settings(int appdata); @@ -116,11 +124,13 @@ extern BYTE config_show_caption; extern BYTE config_show_thickframe; extern BYTE config_toolbar_move_window; extern BYTE config_windowed_hide_cursor; -extern BYTE config_auto_zoom; extern BYTE config_pixel_info; extern BYTE config_orientation; extern BYTE config_title_bar_format; extern int config_add_command_line_timeout; +extern BYTE config_zoom_calc_method; +extern int config_zoom_step; +extern wchar_t config_zoom_fixed_table[STRING_SIZE]; #ifdef __cplusplus } diff --git a/src/localization.h b/src/localization.h index 34e3734e..5a93426b 100644 --- a/src/localization.h +++ b/src/localization.h @@ -237,6 +237,7 @@ enum LOCALIZATION_ID_OPTIONS_GENERAL_DIALOG, LOCALIZATION_ID_OPTIONS_VIEW_DIALOG, LOCALIZATION_ID_OPTIONS_CONTROLS_DIALOG, + LOCALIZATION_ID_OPTIONS_ZOOM_DIALOG, LOCALIZATION_ID_OK_BUTTON, LOCALIZATION_ID_CANCEL_BUTTON, LOCALIZATION_ID_STORE_SETTINGS_APPDATA, @@ -305,6 +306,13 @@ enum LOCALIZATION_ID_OPTIONS_TITLE_BAR_FORMAT_FULL_PATH_COMBOBOXITEM, LOCALIZATION_ID_OPTIONS_TITLE_BAR_FORMAT_FILENAME_ONLY_COMBOBOXITEM, LOCALIZATION_ID_OPTIONS_TITLE_BAR_FORMAT_NONE_COMBOBOXITEM, + LOCALIZATION_ID_OPTIONS_ZOOM_STEP_STATIC, + LOCALIZATION_ID_OPTIONS_ZOOM_STEP_DEFAULT_STATIC, + LOCALIZATION_ID_OPTIONS_ZOOM_CALC_METHOD_STATIC, + LOCALIZATION_ID_OPTIONS_ZOOM_CALC_RELATIVE_RADIO, + LOCALIZATION_ID_OPTIONS_ZOOM_CALC_ABSOLUTE_RADIO, + LOCALIZATION_ID_OPTIONS_ZOOM_CALC_FIXED_TABLE_RADIO, + LOCALIZATION_ID_OPTIONS_ZOOM_LOAD_DEFAULT_TABLE_BUTTON, LOCALIZATION_ID_EVERYTHING_NOT_AVAILABLE_MESSAGE, LOCALIZATION_ID_EVERYTHING_ADD_EVERYTHING_SEARCH_CAPTION, LOCALIZATION_ID_EVERYTHING_LOAD_EVERYTHING_SEARCH_CAPTION, diff --git a/src/localization_en_us.h b/src/localization_en_us.h index 9d2b01f6..f03a6e7b 100644 --- a/src/localization_en_us.h +++ b/src/localization_en_us.h @@ -212,6 +212,7 @@ static const utf8_t *_localization_string_array_en_us[LOCALIZATION_ID_COUNT] = "General", // LOCALIZATION_ID_OPTIONS_GENERAL_DIALOG, "View", // LOCALIZATION_ID_OPTIONS_VIEW_DIALOG, "Controls", // LOCALIZATION_ID_OPTIONS_CONTROLS_DIALOG, + "Zoom options", // LOCALIZATION_ID_OPTIONS_ZOOM_DIALOG, "OK", // LOCALIZATION_ID_OK_BUTTON, "Cancel", // LOCALIZATION_ID_CANCEL_BUTTON, "&Store settings in %APPDATA%\\voidImageViewer", // LOCALIZATION_ID_STORE_SETTINGS_APPDATA, @@ -280,6 +281,13 @@ static const utf8_t *_localization_string_array_en_us[LOCALIZATION_ID_COUNT] = "Full Path", // LOCALIZATION_ID_OPTIONS_TITLE_BAR_FORMAT_FULL_PATH_COMBOBOXITEM, "Filename Only", // LOCALIZATION_ID_OPTIONS_TITLE_BAR_FORMAT_FILENAME_ONLY_COMBOBOXITEM, "None", // LOCALIZATION_ID_OPTIONS_TITLE_BAR_FORMAT_NONE_COMBOBOXITEM, + "Zoom &step:", // LOCALIZATION_ID_OPTIONS_ZOOM_STEP_STATIC, + "(default: 10 %)", // LOCALIZATION_ID_OPTIONS_ZOOM_STEP_DEFAULT_STATIC, + "Zoom calculation method:", // LOCALIZATION_ID_OPTIONS_ZOOM_CALC_METHOD_STATIC, + "&Relative step", // LOCALIZATION_ID_OPTIONS_ZOOM_CALC_RELATIVE_RADIO, + "&Absolute step", // LOCALIZATION_ID_OPTIONS_ZOOM_CALC_ABSOLUTE_RADIO, + "Use &fixed table: (for experts: max. 50 ordered values from 3 to 4000)", // LOCALIZATION_ID_OPTIONS_ZOOM_CALC_FIXED_TABLE_RADIO, + "&Load default table", // LOCALIZATION_ID_OPTIONS_ZOOM_LOAD_DEFAULT_TABLE_BUTTON, "Everything not available", // LOCALIZATION_ID_EVERYTHING_NOT_AVAILABLE_MESSAGE, "Add Everything Search", // LOCALIZATION_ID_EVERYTHING_ADD_EVERYTHING_SEARCH_CAPTION, "Load Everything Search", // LOCALIZATION_ID_EVERYTHING_LOAD_EVERYTHING_SEARCH_CAPTION, diff --git a/src/localization_id_id.h b/src/localization_id_id.h index ef5a2fb3..0c1e93e9 100644 --- a/src/localization_id_id.h +++ b/src/localization_id_id.h @@ -212,6 +212,7 @@ static const utf8_t *_localization_string_array_id_id[LOCALIZATION_ID_COUNT] = "Umum", // LOCALIZATION_ID_OPTIONS_GENERAL_DIALOG, "Tampilan", // LOCALIZATION_ID_OPTIONS_VIEW_DIALOG, "Kontrol", // LOCALIZATION_ID_OPTIONS_CONTROLS_DIALOG, + "Opsi zoom", // LOCALIZATION_ID_OPTIONS_ZOOM_DIALOG, "OK", // LOCALIZATION_ID_OK_BUTTON, "Batal", // LOCALIZATION_ID_CANCEL_BUTTON, "&Simpan pengaturan di %APPDATA%\\voidImageViewer", // LOCALIZATION_ID_STORE_SETTINGS_APPDATA, @@ -280,6 +281,13 @@ static const utf8_t *_localization_string_array_id_id[LOCALIZATION_ID_COUNT] = "Path Lengkap", // LOCALIZATION_ID_OPTIONS_TITLE_BAR_FORMAT_FULL_PATH_COMBOBOXITEM, "Hanya Nama Berkas", // LOCALIZATION_ID_OPTIONS_TITLE_BAR_FORMAT_FILENAME_ONLY_COMBOBOXITEM, "Tidak Ada", // LOCALIZATION_ID_OPTIONS_TITLE_BAR_FORMAT_NONE_COMBOBOXITEM, + "Langkah &zoom:", // LOCALIZATION_ID_OPTIONS_ZOOM_STEP_STATIC, + "(standar: 10 %)", // LOCALIZATION_ID_OPTIONS_ZOOM_STEP_DEFAULT_STATIC, + "Metode kalkulasi zoom:", // LOCALIZATION_ID_OPTIONS_ZOOM_CALC_METHOD_STATIC, + "Langkah &relatif", // LOCALIZATION_ID_OPTIONS_ZOOM_CALC_RELATIVE_RADIO, + "Langkah &absolut", // LOCALIZATION_ID_OPTIONS_ZOOM_CALC_ABSOLUTE_RADIO, + "Gunakan tabel &tetap: (khusus ahli: maks. 50 nilai terurut dari 3 ke 4000)", // LOCALIZATION_ID_OPTIONS_ZOOM_CALC_FIXED_TABLE_RADIO, + "Muat tabel &standar", // LOCALIZATION_ID_OPTIONS_ZOOM_LOAD_DEFAULT_TABLE_BUTTON, "Everything tidak tersedia", // LOCALIZATION_ID_EVERYTHING_NOT_AVAILABLE_MESSAGE, "Tambah Pencarian Everything", // LOCALIZATION_ID_EVERYTHING_ADD_EVERYTHING_SEARCH_CAPTION, "Muat Pencarian Everything", // LOCALIZATION_ID_EVERYTHING_LOAD_EVERYTHING_SEARCH_CAPTION, diff --git a/src/localization_zh_cn.h b/src/localization_zh_cn.h index 1f5678cc..a4a46060 100644 --- a/src/localization_zh_cn.h +++ b/src/localization_zh_cn.h @@ -213,6 +213,7 @@ static const utf8_t *_localization_string_array_zh_cn[LOCALIZATION_ID_COUNT] = "常规", // LOCALIZATION_ID_OPTIONS_GENERAL_DIALOG, "视图", // LOCALIZATION_ID_OPTIONS_VIEW_DIALOG, "控件", // LOCALIZATION_ID_OPTIONS_CONTROLS_DIALOG, + "缩放选项", // LOCALIZATION_ID_OPTIONS_ZOOM_DIALOG, "确定", // LOCALIZATION_ID_OK_BUTTON, "取消", // LOCALIZATION_ID_CANCEL_BUTTON, "在 %APPDATA%\\voidImageViewer 中存储设置(&S)", // LOCALIZATION_ID_STORE_SETTINGS_APPDATA, @@ -281,6 +282,13 @@ static const utf8_t *_localization_string_array_zh_cn[LOCALIZATION_ID_COUNT] = "完整路径", // LOCALIZATION_ID_OPTIONS_TITLE_BAR_FORMAT_FULL_PATH_COMBOBOXITEM, "仅文件名", // LOCALIZATION_ID_OPTIONS_TITLE_BAR_FORMAT_FILENAME_ONLY_COMBOBOXITEM, "无", // LOCALIZATION_ID_OPTIONS_TITLE_BAR_FORMAT_NONE_COMBOBOXITEM, + "缩放步长(&S):", // LOCALIZATION_ID_OPTIONS_ZOOM_STEP_STATIC, + "(默认: 10 %)", // LOCALIZATION_ID_OPTIONS_ZOOM_STEP_DEFAULT_STATIC, + "缩放计算方式:", // LOCALIZATION_ID_OPTIONS_ZOOM_CALC_METHOD_STATIC, + "相对步长(&R)", // LOCALIZATION_ID_OPTIONS_ZOOM_CALC_RELATIVE_RADIO, + "绝对步长(&A)", // LOCALIZATION_ID_OPTIONS_ZOOM_CALC_ABSOLUTE_RADIO, + "使用固定表(&F): (专家级: 3 到 4000 之间的最多 50 个有序值)", // LOCALIZATION_ID_OPTIONS_ZOOM_CALC_FIXED_TABLE_RADIO, + "加载默认表(&L)", // LOCALIZATION_ID_OPTIONS_ZOOM_LOAD_DEFAULT_TABLE_BUTTON, "Everything 不可用", // LOCALIZATION_ID_EVERYTHING_NOT_AVAILABLE_MESSAGE, "添加 Everything 搜索", // LOCALIZATION_ID_EVERYTHING_ADD_EVERYTHING_SEARCH_CAPTION, "加载 Everything 搜索", // LOCALIZATION_ID_EVERYTHING_LOAD_EVERYTHING_SEARCH_CAPTION, diff --git a/src/version.h b/src/version.h index 81a054d8..a0fb2e69 100644 --- a/src/version.h +++ b/src/version.h @@ -1,6 +1,6 @@ #define VERSION_YEAR 2026 #define VERSION_MAJOR 1 -#define VERSION_MINOR 0 -#define VERSION_REVISION 1 +#define VERSION_MINOR 1 +#define VERSION_REVISION 0 #define VERSION_BUILD 0 #define VERSION_TYPE "" diff --git a/src/viv.c b/src/viv.c index 26db4fc8..74229554 100644 --- a/src/viv.c +++ b/src/viv.c @@ -496,6 +496,9 @@ static INT_PTR CALLBACK _viv_options_proc(HWND hwndDlg,UINT uMsg,WPARAM wParam,L static INT_PTR CALLBACK _viv_options_general_proc(HWND hwnd,UINT msg,WPARAM wParam,LPARAM lParam); static INT_PTR CALLBACK _viv_options_view_proc(HWND hwnd,UINT msg,WPARAM wParam,LPARAM lParam); static INT_PTR CALLBACK _viv_options_controls_proc(HWND hwnd,UINT msg,WPARAM wParam,LPARAM lParam); +static INT_PTR CALLBACK _viv_options_zoom_proc(HWND hwnd,UINT msg,WPARAM wParam,LPARAM lParam); +static double _viv_calculate_next_zoom(double current_percent,int zoom_in); +static int _viv_parse_fixed_table(const wchar_t *table_str,double *values,int max_values); static void _viv_install_association_by_extension(const char *association,const char *description,const char *icon_location); static void _viv_uninstall_association_by_extension(const char *association); static int _viv_is_association(const char *association); @@ -675,6 +678,10 @@ static BYTE _viv_animation_play = 1; // play or pause animations static BYTE _viv_1to1 = 0; // temporarily show the image with 100% scaling static BYTE _viv_have_old_zoom = 0; // restore this zoom level after leaving 1:1 mode. static int _viv_old_zoom_pos = 0; // restore this zoom level after leaving 1:1 mode. +static BYTE _viv_is_custom_zoom = 0; // custom zoom level active +static double _viv_custom_zoom_percent = 100.0; // custom zoom in percent +static BYTE _viv_old_is_custom_zoom = 0; +static double _viv_old_custom_zoom_percent = 100.0; WIN32_FIND_DATA *_viv_current_fd = 0; // the current image find data including the full path and filename. WIN32_FIND_DATA *_viv_preload_fd = 0; static int _viv_view_x = 0; // the current image offset in pixels @@ -732,12 +739,12 @@ static volatile int _viv_load_image_terminate = 0; static _viv_reply_t *_viv_reply_start = 0; static _viv_reply_t *_viv_reply_last = 0; static wchar_t *_viv_status_temp_text = 0; -static localization_id_t _viv_options_page_localization_id_array[] = {LOCALIZATION_ID_OPTIONS_GENERAL_DIALOG,LOCALIZATION_ID_OPTIONS_VIEW_DIALOG,LOCALIZATION_ID_OPTIONS_CONTROLS_DIALOG}; -static int _viv_options_tab_ids[] = {IDC_TAB1,IDC_TAB2,IDC_TAB3}; -static int _viv_options_dialog_ids[] = {IDD_GENERAL,IDD_VIEW,IDD_CONTROLS}; +static localization_id_t _viv_options_page_localization_id_array[] = {LOCALIZATION_ID_OPTIONS_GENERAL_DIALOG,LOCALIZATION_ID_OPTIONS_VIEW_DIALOG,LOCALIZATION_ID_OPTIONS_CONTROLS_DIALOG,LOCALIZATION_ID_OPTIONS_ZOOM_DIALOG}; +static int _viv_options_tab_ids[] = {IDC_TAB1,IDC_TAB2,IDC_TAB3,IDC_TAB4}; +static int _viv_options_dialog_ids[] = {IDD_GENERAL,IDD_VIEW,IDD_CONTROLS,IDD_ZOOM}; #define _VIV_OPTIONS_PAGE_COUNT (sizeof(_viv_options_dialog_ids) / sizeof(int)) -static int _viv_options_page_ids[] = {VIV_ID_OPTIONS_GENERAL,VIV_ID_OPTIONS_VIEW,VIV_ID_OPTIONS_CONTROLS}; -static DLGPROC _viv_options_page_procs[] = {_viv_options_general_proc,_viv_options_view_proc,_viv_options_controls_proc}; +static int _viv_options_page_ids[] = {VIV_ID_OPTIONS_GENERAL,VIV_ID_OPTIONS_VIEW,VIV_ID_OPTIONS_CONTROLS,VIV_ID_OPTIONS_ZOOM}; +static DLGPROC _viv_options_page_procs[] = {_viv_options_general_proc,_viv_options_view_proc,_viv_options_controls_proc,_viv_options_zoom_proc}; static HFONT _viv_about_hfont = 0; static wchar_t *_viv_last_open_file = 0; static wchar_t *_viv_last_open_folder = 0; @@ -1277,12 +1284,16 @@ static void _viv_clear(void) _viv_frame_count = 0; _viv_frame_loaded_count = 0; _viv_zoom_pos = 0; + _viv_is_custom_zoom = 0; + _viv_custom_zoom_percent = 100.0; _viv_view_x = 0; _viv_view_y = 0; _viv_view_ix = 0.0; _viv_view_iy = 0.0; _viv_1to1 = 0; _viv_have_old_zoom = 0; + _viv_old_is_custom_zoom = 0; + _viv_old_custom_zoom_percent = 100.0; _viv_image_wide = 0; _viv_image_high = 0; _viv_animation_play = 1; @@ -1672,9 +1683,12 @@ static void _viv_command_with_is_key_repeat(int command_id,int is_key_repeat) case VIV_ID_VIEW_ZOOM_RESET: _viv_1to1 = 0; + _viv_is_custom_zoom = 0; + _viv_custom_zoom_percent = 100.0; _viv_zoom_pos = 0; _viv_view_set(_viv_view_x,_viv_view_y,1); InvalidateRect(_viv_hwnd,0,FALSE); + _viv_toolbar_update_buttons(); break; case VIV_ID_HELP_HELP: @@ -6879,6 +6893,19 @@ static void _viv_get_render_size(int *prw,int *prh) return; } + if (_viv_is_custom_zoom) + { + rw = (int)((double)_viv_image_wide * _viv_custom_zoom_percent / 100.0 + 0.5); + rh = (int)((double)_viv_image_high * _viv_custom_zoom_percent / 100.0 + 0.5); + if (rw <= 0) rw = 1; + if (rh <= 0) rh = 1; + + *prw = rw; + *prh = rh; + + return; + } + if (_viv_is_fullscreen) { fill_window = config_fullscreen_fill_window; @@ -8499,6 +8526,199 @@ static INT_PTR CALLBACK _viv_options_view_proc(HWND hwnd,UINT msg,WPARAM wParam, return FALSE; } +static int _viv_parse_fixed_table(const wchar_t *table_str,double *values,int max_values) +{ + const wchar_t *p; + int count = 0; + + if (!table_str || !*table_str) + { + table_str = CONFIG_ZOOM_DEFAULT_FIXED_TABLE; + } + + p = table_str; + while (*p && (count < max_values)) + { + while (*p && ((*p == L' ') || (*p == L'\t') || (*p == L';') || (*p == L','))) + { + p++; + } + if (!*p) break; + + if (((*p >= L'0') && (*p <= L'9')) || (*p == L'.')) + { + double val = 0.0; + double frac = 0.0; + double div = 10.0; + + while ((*p >= L'0') && (*p <= L'9')) + { + val = (val * 10.0) + (*p - L'0'); + p++; + } + if (*p == L'.') + { + p++; + while ((*p >= L'0') && (*p <= L'9')) + { + frac += (*p - L'0') / div; + div *= 10.0; + p++; + } + } + val += frac; + if (val > 0.0) + { + values[count++] = val; + } + } + else + { + p++; + } + } + + { + int i, j; + for (i = 0; i < count - 1; i++) + { + for (j = i + 1; j < count; j++) + { + if (values[j] < values[i]) + { + double tmp = values[i]; + values[i] = values[j]; + values[j] = tmp; + } + } + } + } + + return count; +} + +static double _viv_calculate_next_zoom(double current_percent,int zoom_in) +{ + double new_percent = current_percent; + int step = config_zoom_step > 0 ? config_zoom_step : CONFIG_ZOOM_DEFAULT_STEP; + + if (config_zoom_calc_method == CONFIG_ZOOM_CALC_RELATIVE) + { + if (zoom_in) + { + new_percent = current_percent * (1.0 + ((double)step / 100.0)); + } + else + { + new_percent = current_percent / (1.0 + ((double)step / 100.0)); + } + } + else if (config_zoom_calc_method == CONFIG_ZOOM_CALC_ABSOLUTE) + { + if (zoom_in) + { + new_percent = current_percent + (double)step; + } + else + { + new_percent = current_percent - (double)step; + } + } + else if (config_zoom_calc_method == CONFIG_ZOOM_CALC_FIXED_TABLE) + { + double table_values[64]; + int table_count = _viv_parse_fixed_table(config_zoom_fixed_table, table_values, 64); + if (table_count <= 0) + { + table_count = _viv_parse_fixed_table(CONFIG_ZOOM_DEFAULT_FIXED_TABLE, table_values, 64); + } + + if (table_count > 0) + { + int i; + if (zoom_in) + { + new_percent = table_values[table_count - 1]; + for (i = 0; i < table_count; i++) + { + if (table_values[i] > current_percent + 0.001) + { + new_percent = table_values[i]; + break; + } + } + } + else + { + new_percent = table_values[0]; + for (i = table_count - 1; i >= 0; i--) + { + if (table_values[i] < current_percent - 0.001) + { + new_percent = table_values[i]; + break; + } + } + } + } + } + + if (new_percent < 0.1) new_percent = 0.1; + if (new_percent > 10000.0) new_percent = 10000.0; + + return new_percent; +} + +static INT_PTR CALLBACK _viv_options_zoom_proc(HWND hwnd,UINT msg,WPARAM wParam,LPARAM lParam) +{ + switch(msg) + { + case WM_INITDIALOG: + { + wchar_t wbuf[STRING_SIZE]; + + os_SetDlgItemText_localization_id(hwnd,IDC_ZOOM_STEP_STATIC,LOCALIZATION_ID_OPTIONS_ZOOM_STEP_STATIC); + os_SetDlgItemText_localization_id(hwnd,IDC_ZOOM_STEP_DEFAULT_STATIC,LOCALIZATION_ID_OPTIONS_ZOOM_STEP_DEFAULT_STATIC); + os_SetDlgItemText_localization_id(hwnd,IDC_ZOOM_CALC_METHOD_STATIC,LOCALIZATION_ID_OPTIONS_ZOOM_CALC_METHOD_STATIC); + os_SetDlgItemText_localization_id(hwnd,IDC_ZOOM_CALC_RELATIVE_RADIO,LOCALIZATION_ID_OPTIONS_ZOOM_CALC_RELATIVE_RADIO); + os_SetDlgItemText_localization_id(hwnd,IDC_ZOOM_CALC_ABSOLUTE_RADIO,LOCALIZATION_ID_OPTIONS_ZOOM_CALC_ABSOLUTE_RADIO); + os_SetDlgItemText_localization_id(hwnd,IDC_ZOOM_CALC_FIXED_TABLE_RADIO,LOCALIZATION_ID_OPTIONS_ZOOM_CALC_FIXED_TABLE_RADIO); + os_SetDlgItemText_localization_id(hwnd,IDC_ZOOM_LOAD_DEFAULT_TABLE_BUTTON,LOCALIZATION_ID_OPTIONS_ZOOM_LOAD_DEFAULT_TABLE_BUTTON); + + string_format_number(wbuf,config_zoom_step); + SetDlgItemText(hwnd,IDC_ZOOM_STEP_EDIT,wbuf); + + if (config_zoom_calc_method == CONFIG_ZOOM_CALC_RELATIVE) + { + CheckRadioButton(hwnd,IDC_ZOOM_CALC_RELATIVE_RADIO,IDC_ZOOM_CALC_FIXED_TABLE_RADIO,IDC_ZOOM_CALC_RELATIVE_RADIO); + } + else if (config_zoom_calc_method == CONFIG_ZOOM_CALC_ABSOLUTE) + { + CheckRadioButton(hwnd,IDC_ZOOM_CALC_RELATIVE_RADIO,IDC_ZOOM_CALC_FIXED_TABLE_RADIO,IDC_ZOOM_CALC_ABSOLUTE_RADIO); + } + else + { + CheckRadioButton(hwnd,IDC_ZOOM_CALC_RELATIVE_RADIO,IDC_ZOOM_CALC_FIXED_TABLE_RADIO,IDC_ZOOM_CALC_FIXED_TABLE_RADIO); + } + + SetDlgItemText(hwnd,IDC_ZOOM_FIXED_TABLE_EDIT,config_zoom_fixed_table); + + return FALSE; + } + + case WM_COMMAND: + switch(LOWORD(wParam)) + { + case IDC_ZOOM_LOAD_DEFAULT_TABLE_BUTTON: + SetDlgItemText(hwnd,IDC_ZOOM_FIXED_TABLE_EDIT,CONFIG_ZOOM_DEFAULT_FIXED_TABLE); + break; + } + break; + } + + return FALSE; +} + static void _viv_options_treeview_changed(HWND hwnd) { TV_ITEM tvi; @@ -8828,6 +9048,38 @@ static INT_PTR CALLBACK _viv_options_proc(HWND hwnd,UINT msg,WPARAM wParam,LPARA config_loop_animations_once = IsDlgButtonChecked(view_page,IDC_LOOP_ANIMATIONS_ONCE_STATIC) == BST_CHECKED ? 1 : 0; config_preload_next = IsDlgButtonChecked(view_page,IDC_PRELOAD_NEXT_IMAGE_STATIC) == BST_CHECKED ? 1 : 0; config_cache_last = IsDlgButtonChecked(view_page,IDC_CACHE_LAST_IMAGE_STATIC) == BST_CHECKED ? 1 : 0; + + { + HWND zoom_page; + zoom_page = GetDlgItem(hwnd,VIV_ID_OPTIONS_ZOOM); + if (zoom_page) + { + wchar_t wbuf[STRING_SIZE]; + int step; + + GetDlgItemText(zoom_page,IDC_ZOOM_STEP_EDIT,wbuf,STRING_SIZE); + step = string_to_int(wbuf); + if (step > 0) + { + config_zoom_step = step; + } + + if (IsDlgButtonChecked(zoom_page,IDC_ZOOM_CALC_RELATIVE_RADIO) == BST_CHECKED) + { + config_zoom_calc_method = CONFIG_ZOOM_CALC_RELATIVE; + } + else if (IsDlgButtonChecked(zoom_page,IDC_ZOOM_CALC_ABSOLUTE_RADIO) == BST_CHECKED) + { + config_zoom_calc_method = CONFIG_ZOOM_CALC_ABSOLUTE; + } + else if (IsDlgButtonChecked(zoom_page,IDC_ZOOM_CALC_FIXED_TABLE_RADIO) == BST_CHECKED) + { + config_zoom_calc_method = CONFIG_ZOOM_CALC_FIXED_TABLE; + } + + GetDlgItemText(zoom_page,IDC_ZOOM_FIXED_TABLE_EDIT,config_zoom_fixed_table,STRING_SIZE); + } + } // copy keys. _viv_key_list_copy(_viv_key_list,(_viv_key_list_t *)GetWindowLongPtr(GetDlgItem(controls_page,IDC_COMMANDS_LIST),GWLP_USERDATA)); @@ -9375,9 +9627,12 @@ static void _viv_view_1to1(void) if (_viv_have_old_zoom) { _viv_1to1 = 0; + _viv_is_custom_zoom = _viv_old_is_custom_zoom; + _viv_custom_zoom_percent = _viv_old_custom_zoom_percent; _viv_zoom_pos = _viv_old_zoom_pos; _viv_view_set(_viv_view_x,_viv_view_y,1); InvalidateRect(_viv_hwnd,0,FALSE); + _viv_toolbar_update_buttons(); return; } @@ -9385,11 +9640,15 @@ static void _viv_view_1to1(void) _viv_have_old_zoom = 1; _viv_old_zoom_pos = _viv_zoom_pos; + _viv_old_is_custom_zoom = _viv_is_custom_zoom; + _viv_old_custom_zoom_percent = _viv_custom_zoom_percent; _viv_1to1 = 1; + _viv_is_custom_zoom = 0; _viv_zoom_pos = 0; _viv_view_set(_viv_view_x,_viv_view_y,1); InvalidateRect(_viv_hwnd,0,FALSE); + _viv_toolbar_update_buttons(); } static void _viv_playlist_add_current_if_empty(void) @@ -11746,7 +12005,7 @@ static void _viv_toolbar_update_buttons(void) tbbinfo.cbSize = sizeof(TBBUTTONINFO); tbbinfo.dwMask = TBIF_STATE; - tbbinfo.fsState = ((_viv_zoom_pos == 0) && (!_viv_1to1)) ? (0) : (TBSTATE_ENABLED); + tbbinfo.fsState = ((!_viv_is_custom_zoom) && (_viv_zoom_pos == 0) && (!_viv_1to1)) ? (0) : (TBSTATE_ENABLED); SendMessage(_viv_toolbar_hwnd,TB_SETBUTTONINFO,VIV_ID_VIEW_BESTFIT,(LPARAM)&tbbinfo); } @@ -14078,70 +14337,42 @@ static void _viv_do_mousewheel_action(int action,int delta,int x,int y) if (_viv_1to1) { _viv_1to1 = 0; - - if (delta > 0) + } + + { + double current_percent; + double new_percent; + + if (_viv_image_wide > 0) { - for(_viv_zoom_pos = 0;_viv_zoom_pos<_VIV_ZOOM_MAX;_viv_zoom_pos++) - { - _viv_get_render_size(&rw,&rh); - - if (rw > old_rw) - { - break; - } - } + current_percent = ((double)old_rw / (double)_viv_image_wide) * 100.0; } else { - for(_viv_zoom_pos = _VIV_ZOOM_MAX-1;_viv_zoom_pos>=0;_viv_zoom_pos--) - { - _viv_get_render_size(&rw,&rh); - - if (rw < old_rw) - { - break; - } - } + current_percent = 100.0; } - } - else - { + if (delta > 0) { - _viv_zoom_pos++; + new_percent = _viv_calculate_next_zoom(current_percent, 1); } - else + else if (delta < 0) { - _viv_zoom_pos--; + new_percent = _viv_calculate_next_zoom(current_percent, 0); } - } - - if (_viv_zoom_pos < 0) _viv_zoom_pos = 0; - if (_viv_zoom_pos > _VIV_ZOOM_MAX-1) _viv_zoom_pos = _VIV_ZOOM_MAX-1; - - if (_viv_zoom_pos != old_zoom_pos) - { - _viv_get_render_size(&rw,&rh); - /* - if (_viv_zoom_pos == 1) + else { - if ((rw < _viv_image_wide) || (rw < _viv_image_wide)) - { - rw = _viv_image_wide; - rh = _viv_image_high; - } + new_percent = current_percent; } - */ - // - // new_cursor_x = - // rx = (wide / 2) - (rw / 2) - //debug_printf("%d %d\n",(wide / 2) - (rw / 2),(high / 2) - (rh / 2)); - //debug_printf("%d %d\n",old_cursor_px,(old_cursor_px * 100) / old_rw); - - //debug_printf("old %d %d new %d %d\n",old_cursor_px,old_cursor_py,(wide / 2) - (rw / 2) - cursor_x + ((old_cursor_px * rw) / old_rw),(high / 2) - (rh / 2) - cursor_y + ((old_cursor_py * rh) / old_rh)) ; - //debug_printf("wide / 2 = %d, rw/2=%d, old_cursor_px * rw=%d\n",wide/2,rw/2,old_cursor_px * rw) ; + _viv_is_custom_zoom = 1; + _viv_custom_zoom_percent = new_percent; + } + + _viv_get_render_size(&rw,&rh); + if ((rw != old_rw) || (rh != old_rh)) + { if (old_rw) { new_cursor_x = ((__int64)old_cursor_px * (__int64)rw) / (__int64)old_rw; @@ -14163,6 +14394,7 @@ static void _viv_do_mousewheel_action(int action,int delta,int x,int y) _viv_view_set((wide / 2) - (rw / 2) - cursor_x + new_cursor_x,(high / 2) - (rh / 2) - cursor_y + new_cursor_y,1); InvalidateRect(_viv_hwnd,0,FALSE); + _viv_toolbar_update_buttons(); } } else diff --git a/src/viv.h b/src/viv.h index 37a1e94a..e914218a 100644 --- a/src/viv.h +++ b/src/viv.h @@ -216,6 +216,7 @@ enum VIV_ID_OPTIONS_GENERAL, VIV_ID_OPTIONS_VIEW, VIV_ID_OPTIONS_CONTROLS, + VIV_ID_OPTIONS_ZOOM, VIV_ID_FILE_OPEN_FILE, VIV_ID_FILE_OPEN_FOLDER, From 92ea5436ec4d5cde39dfa2e46fbac564785f5159 Mon Sep 17 00:00:00 2001 From: Syahda Fahreza Date: Sat, 29 Aug 2026 21:58:02 +0700 Subject: [PATCH 07/15] Add Compare Images mode and version 1.2.0.0 Introduce a new Compare Images side-by-side comparison window with synchronized/individual zoom & pan, per-pane mini toolbars (browse, zoom, 1:1, fit, rotate, select), and a top control bar (tips, Auto-Fit, Smooth, Filename overlay, Close). Implement full UI, painting, input handling, image loading, mipmap usage and overlays; register new menu item and default hotkey Ctrl+Alt+C. Add multilingual strings (en-US, id-ID, zh-CN), bump product/version to 1.2.0.0 in resources and installer, and update README/Changes.txt. --- Changes.txt | 6 + README.md | 22 +- nsis/version.nsh | 3 +- res/voidImageViewer.rc | 8 +- res/voidImageViewer_utf8.rc | 8 +- src/localization.h | 16 + src/localization_en_us.h | 16 + src/localization_id_id.h | 16 + src/localization_zh_cn.h | 16 + src/version.h | 3 +- src/viv.c | 1341 +++++++++++++++++++++++++++++++++++ src/viv.h | 1 + 12 files changed, 1445 insertions(+), 11 deletions(-) diff --git a/Changes.txt b/Changes.txt index 62d6cdba..a8967ae7 100644 --- a/Changes.txt +++ b/Changes.txt @@ -1,3 +1,9 @@ +Saturday, 29 August 2026: Version 1.2.0.0 + added Compare Images side-by-side comparison mode with synchronized/individual zoom and pan. + added mini toolbars per pane for file browsing, zooming, 1:1, fit, and rotation. + added top control bar with tips, Auto-Fit, Smooth, Filename overlay, and Close. + added full multilingual localization for English, Indonesian, and Simplified Chinese. + Monday, 12 January 2026: Version 1.0.0.15 fixed an issue with scrolling large images. improved position offset when using numpad keys. diff --git a/README.md b/README.md index 4ac632c1..eb41966d 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,27 @@ void Image Viewer Controls Options: ## Changelog -### Version 1.1.0.0 (Latest) +### Version 1.2.0.0 (Latest) +- **New Feature: Compare Images (Side-by-Side Image Comparison)** + - Added multi-pane **Compare Images** window accessible via `View` -> `Compare Images...` or shortcut (`Ctrl+Alt+C`). + - **Synchronized Navigation & Zoom:** + - Mouse wheel zooms all compared images simultaneously using the exact same zoom engine (`_viv_calculate_next_zoom`). + - Left-click drag pans all compared images synchronously. + - **Individual Pane Mode:** Hold `Ctrl` key or use **Right Mouse Button** to zoom or pan only the hovered image pane. + - **Per-Pane Mini Toolbar:** + - Previous (`<`) and Next (`>`) buttons to browse folder/playlist images per pane. + - Zoom In (`+`), Zoom Out (`-`), Zoom % display, `1:1` (Actual size), and `Fit` (Best Fit) buttons. + - Rotate clockwise button (`90°`). + - Change/Select image file button (`X`). + - **Top Control Bar:** + - Helpful shortcut and mouse tips. + - **Auto-Fit** toggle to automatically scale images to fit pane viewports. + - **Smooth** toggle (Halftone / High-quality interpolation). + - **Filename** toggle with bright green overlay text (`filename`, `dimensions`, `file size`). + - **Close** button (or `ESC` key) to quickly return to main viewer. + - **Full Multilingual Localization:** Added translations for all compare UI elements in English (`en-US`), Indonesian (`id-ID`), and Simplified Chinese (`zh-CN`). + +### Version 1.1.0.0 - **New Feature: Zoom Options Tab & Calculation Engine** - Added dedicated **Zoom options** tab in Options dialog (`IDD_ZOOM`). - Added customizable zoom calculation methods: diff --git a/nsis/version.nsh b/nsis/version.nsh index 211bac86..bd29c9bd 100644 --- a/nsis/version.nsh +++ b/nsis/version.nsh @@ -1,3 +1,4 @@ -!define VERSION "1.1.0.0" +!define VERSION "1.2.0.0" !define BETAVERSION "" !define VERSIONYEAR "2026" + diff --git a/res/voidImageViewer.rc b/res/voidImageViewer.rc index 4d974b8e..bd47a318 100644 --- a/res/voidImageViewer.rc +++ b/res/voidImageViewer.rc @@ -228,8 +228,8 @@ IDI_BESTFIT ICON "bestfit.ico" // VS_VERSION_INFO VERSIONINFO - FILEVERSION 1,1,0,0 - PRODUCTVERSION 1,1,0,0 + FILEVERSION 1,2,0,0 + PRODUCTVERSION 1,2,0,0 FILEFLAGSMASK 0x17L #ifdef _DEBUG FILEFLAGS 0x1L @@ -245,12 +245,12 @@ BEGIN BLOCK "040904b0" BEGIN VALUE "FileDescription", "voidImageViewer" - VALUE "FileVersion", "1.1.0.0" + VALUE "FileVersion", "1.2.0.0" VALUE "InternalName", "voidImageViewer" VALUE "LegalCopyright", "Copyright © 2026 voidtools" VALUE "OriginalFilename", "voidImageViewer.exe" VALUE "ProductName", "voidImageViewer" - VALUE "ProductVersion", "1.1.0.0" + VALUE "ProductVersion", "1.2.0.0" END END BLOCK "VarFileInfo" diff --git a/res/voidImageViewer_utf8.rc b/res/voidImageViewer_utf8.rc index 3dafd1d0..e604b875 100644 --- a/res/voidImageViewer_utf8.rc +++ b/res/voidImageViewer_utf8.rc @@ -228,8 +228,8 @@ IDI_BESTFIT ICON "bestfit.ico" // VS_VERSION_INFO VERSIONINFO - FILEVERSION 1,1,0,0 - PRODUCTVERSION 1,1,0,0 + FILEVERSION 1,2,0,0 + PRODUCTVERSION 1,2,0,0 FILEFLAGSMASK 0x17L #ifdef _DEBUG FILEFLAGS 0x1L @@ -245,12 +245,12 @@ BEGIN BLOCK "040904b0" BEGIN VALUE "FileDescription", "voidImageViewer" - VALUE "FileVersion", "1.1.0.0" + VALUE "FileVersion", "1.2.0.0" VALUE "InternalName", "voidImageViewer" VALUE "LegalCopyright", "Copyright © 2026 voidtools" VALUE "OriginalFilename", "voidImageViewer.exe" VALUE "ProductName", "voidImageViewer" - VALUE "ProductVersion", "1.1.0.0" + VALUE "ProductVersion", "1.2.0.0" END END BLOCK "VarFileInfo" diff --git a/src/localization.h b/src/localization.h index 5a93426b..8dd97ffb 100644 --- a/src/localization.h +++ b/src/localization.h @@ -331,6 +331,22 @@ enum LOCALIZATION_ID_ASSOCIATION_DESCRIPTION_TIFF, LOCALIZATION_ID_ASSOCIATION_DESCRIPTION_WEBP, + // Compare Images + LOCALIZATION_ID_COMPARE_IMAGES, + LOCALIZATION_ID_COMPARE_TIPS, + LOCALIZATION_ID_COMPARE_AUTO_FIT, + LOCALIZATION_ID_COMPARE_SMOOTH, + LOCALIZATION_ID_COMPARE_FILENAME, + LOCALIZATION_ID_COMPARE_CLOSE, + LOCALIZATION_ID_COMPARE_PREV, + LOCALIZATION_ID_COMPARE_NEXT, + LOCALIZATION_ID_COMPARE_ZOOM_IN, + LOCALIZATION_ID_COMPARE_ZOOM_OUT, + LOCALIZATION_ID_COMPARE_ACTUAL_SIZE, + LOCALIZATION_ID_COMPARE_BEST_FIT, + LOCALIZATION_ID_COMPARE_ROTATE, + LOCALIZATION_ID_COMPARE_SELECT_IMAGE, + LOCALIZATION_ID_COUNT, }; diff --git a/src/localization_en_us.h b/src/localization_en_us.h index f03a6e7b..a38e1000 100644 --- a/src/localization_en_us.h +++ b/src/localization_en_us.h @@ -305,4 +305,20 @@ static const utf8_t *_localization_string_array_en_us[LOCALIZATION_ID_COUNT] = "TIFF Image", "TIFF Image", "WebP Image", + + // Compare Images + "Compare Images...", // LOCALIZATION_ID_COMPARE_IMAGES, + "Tips: Drag images with LEFT mouse button; Scroll mouse WHEEL to zoom in/out; Press Ctrl key or Right mouse button to work with individual images.", // LOCALIZATION_ID_COMPARE_TIPS, + "Auto-Fit", // LOCALIZATION_ID_COMPARE_AUTO_FIT, + "Smooth", // LOCALIZATION_ID_COMPARE_SMOOTH, + "Filename", // LOCALIZATION_ID_COMPARE_FILENAME, + "Close", // LOCALIZATION_ID_COMPARE_CLOSE, + "Previous", // LOCALIZATION_ID_COMPARE_PREV, + "Next", // LOCALIZATION_ID_COMPARE_NEXT, + "Zoom In", // LOCALIZATION_ID_COMPARE_ZOOM_IN, + "Zoom Out", // LOCALIZATION_ID_COMPARE_ZOOM_OUT, + "1:1", // LOCALIZATION_ID_COMPARE_ACTUAL_SIZE, + "Fit", // LOCALIZATION_ID_COMPARE_BEST_FIT, + "Rotate", // LOCALIZATION_ID_COMPARE_ROTATE, + "Select Image", // LOCALIZATION_ID_COMPARE_SELECT_IMAGE, }; diff --git a/src/localization_id_id.h b/src/localization_id_id.h index 0c1e93e9..f49fae18 100644 --- a/src/localization_id_id.h +++ b/src/localization_id_id.h @@ -305,4 +305,20 @@ static const utf8_t *_localization_string_array_id_id[LOCALIZATION_ID_COUNT] = "Gambar TIFF", "Gambar TIFF", "Gambar WebP", + + // Compare Images + "Bandingkan Gambar...", // LOCALIZATION_ID_COMPARE_IMAGES, + "Tips: Seret gambar dengan tombol mouse KIRI; Gulir RODA mouse untuk zoom; Tekan tombol Ctrl atau tombol mouse Kanan untuk memproses gambar individual.", // LOCALIZATION_ID_COMPARE_TIPS, + "Pas Otomatis", // LOCALIZATION_ID_COMPARE_AUTO_FIT, + "Halus", // LOCALIZATION_ID_COMPARE_SMOOTH, + "Nama Berkas", // LOCALIZATION_ID_COMPARE_FILENAME, + "Tutup", // LOCALIZATION_ID_COMPARE_CLOSE, + "Sebelumnya", // LOCALIZATION_ID_COMPARE_PREV, + "Berikutnya", // LOCALIZATION_ID_COMPARE_NEXT, + "Perbesar", // LOCALIZATION_ID_COMPARE_ZOOM_IN, + "Perkecil", // LOCALIZATION_ID_COMPARE_ZOOM_OUT, + "1:1", // LOCALIZATION_ID_COMPARE_ACTUAL_SIZE, + "Pas", // LOCALIZATION_ID_COMPARE_BEST_FIT, + "Putar", // LOCALIZATION_ID_COMPARE_ROTATE, + "Pilih Gambar", // LOCALIZATION_ID_COMPARE_SELECT_IMAGE, }; diff --git a/src/localization_zh_cn.h b/src/localization_zh_cn.h index a4a46060..b08e18c6 100644 --- a/src/localization_zh_cn.h +++ b/src/localization_zh_cn.h @@ -306,4 +306,20 @@ static const utf8_t *_localization_string_array_zh_cn[LOCALIZATION_ID_COUNT] = "TIFF Image", "TIFF Image", "WebP Image", + + // Compare Images + "比较图像...", // LOCALIZATION_ID_COMPARE_IMAGES, + "提示: 按住鼠标左键拖动图像; 滚动鼠标滚轮缩放; 按住 Ctrl 键或鼠标右键可单独操作当前图像。", // LOCALIZATION_ID_COMPARE_TIPS, + "自动适合", // LOCALIZATION_ID_COMPARE_AUTO_FIT, + "平滑", // LOCALIZATION_ID_COMPARE_SMOOTH, + "文件名", // LOCALIZATION_ID_COMPARE_FILENAME, + "关闭", // LOCALIZATION_ID_COMPARE_CLOSE, + "上一个", // LOCALIZATION_ID_COMPARE_PREV, + "下一个", // LOCALIZATION_ID_COMPARE_NEXT, + "放大", // LOCALIZATION_ID_COMPARE_ZOOM_IN, + "缩小", // LOCALIZATION_ID_COMPARE_ZOOM_OUT, + "1:1", // LOCALIZATION_ID_COMPARE_ACTUAL_SIZE, + "适合", // LOCALIZATION_ID_COMPARE_BEST_FIT, + "旋转", // LOCALIZATION_ID_COMPARE_ROTATE, + "选择图像", // LOCALIZATION_ID_COMPARE_SELECT_IMAGE, }; diff --git a/src/version.h b/src/version.h index a0fb2e69..6024029c 100644 --- a/src/version.h +++ b/src/version.h @@ -1,6 +1,7 @@ #define VERSION_YEAR 2026 #define VERSION_MAJOR 1 -#define VERSION_MINOR 1 +#define VERSION_MINOR 2 #define VERSION_REVISION 0 #define VERSION_BUILD 0 #define VERSION_TYPE "" + diff --git a/src/viv.c b/src/viv.c index 74229554..ddc6800d 100644 --- a/src/viv.c +++ b/src/viv.c @@ -594,6 +594,8 @@ static INT_PTR CALLBACK _viv_rename_proc(HWND hwnd,UINT msg,WPARAM wParam,LPARAM static void _viv_shuffle_playlist(void); static void _viv_show_jumpto(void); static LRESULT CALLBACK _viv_jumpto_proc(HWND hwnd,UINT msg,WPARAM wParam,LPARAM lParam); +static void _viv_compare_show(void); +static LRESULT CALLBACK _viv_compare_proc(HWND hwnd,UINT msg,WPARAM wParam,LPARAM lParam); static void _viv_jumpto_on_size(HWND hwnd); static void _viv_jumpto_on_search(HWND hwnd); static void _viv_jumpto_open_sel(HWND hwnd); @@ -855,6 +857,7 @@ static _viv_command_t _viv_commands[] = {LOCALIZATION_ID_INVALID,MF_SEPARATOR,_VIV_MENU_VIEW,0}, {LOCALIZATION_ID_FULLSCREEN,MF_STRING,_VIV_MENU_VIEW,VIV_ID_VIEW_FULLSCREEN}, {LOCALIZATION_ID_SLIDESHOW,MF_STRING,_VIV_MENU_VIEW,VIV_ID_VIEW_SLIDESHOW}, + {LOCALIZATION_ID_COMPARE_IMAGES,MF_STRING,_VIV_MENU_VIEW,VIV_ID_VIEW_COMPARE_IMAGES}, {LOCALIZATION_ID_VIEW_WINDOW_SIZE,MF_POPUP,_VIV_MENU_VIEW,_VIV_MENU_VIEW_WINDOW_SIZE}, {LOCALIZATION_ID_VIEW_WINDOW_SIZE_50_PERCENT,MF_STRING,_VIV_MENU_VIEW_WINDOW_SIZE,VIV_ID_VIEW_WINDOW_SIZE_50}, {LOCALIZATION_ID_VIEW_WINDOW_SIZE_100_PERCENT,MF_STRING,_VIV_MENU_VIEW_WINDOW_SIZE,VIV_ID_VIEW_WINDOW_SIZE_100}, @@ -998,6 +1001,7 @@ _viv_default_key_t _viv_default_keys[] = {VIV_ID_VIEW_1TO1,CONFIG_KEYFLAG_CTRL | CONFIG_KEYFLAG_ALT | '0'}, {VIV_ID_VIEW_FULLSCREEN,CONFIG_KEYFLAG_ALT | VK_RETURN}, {VIV_ID_VIEW_SLIDESHOW,VK_F11}, + {VIV_ID_VIEW_COMPARE_IMAGES,CONFIG_KEYFLAG_CTRL | CONFIG_KEYFLAG_ALT | 'C'}, {VIV_ID_VIEW_WINDOW_SIZE_50,CONFIG_KEYFLAG_ALT | '1'}, {VIV_ID_VIEW_WINDOW_SIZE_100,CONFIG_KEYFLAG_ALT | '2'}, {VIV_ID_VIEW_WINDOW_SIZE_200,CONFIG_KEYFLAG_ALT | '3'}, @@ -2084,6 +2088,12 @@ static void _viv_command_with_is_key_repeat(int command_id,int is_key_repeat) break; + case VIV_ID_VIEW_COMPARE_IMAGES: + + _viv_compare_show(); + + break; + case VIV_ID_VIEW_WINDOW_SIZE_50: case VIV_ID_VIEW_WINDOW_SIZE_100: case VIV_ID_VIEW_WINDOW_SIZE_200: @@ -13592,6 +13602,1337 @@ static void _viv_show_jumpto(void) } } +// +// Compare Images Implementation +// + +#define _VIV_COMPARE_PANE_COUNT 2 + +enum { + _VIV_COMPARE_BTN_NONE = 0, + _VIV_COMPARE_BTN_PREV, + _VIV_COMPARE_BTN_NEXT, + _VIV_COMPARE_BTN_ZOOM_IN, + _VIV_COMPARE_BTN_ZOOM_OUT, + _VIV_COMPARE_BTN_ZOOM_TEXT, + _VIV_COMPARE_BTN_1TO1, + _VIV_COMPARE_BTN_FIT, + _VIV_COMPARE_BTN_ROTATE, + _VIV_COMPARE_BTN_SELECT, + + _VIV_COMPARE_TOP_AUTOFIT = 101, + _VIV_COMPARE_TOP_SMOOTH = 102, + _VIV_COMPARE_TOP_FILENAME = 103, + _VIV_COMPARE_TOP_CLOSE = 104 +}; + +typedef struct _viv_compare_pane_s +{ + wchar_t filename[STRING_SIZE]; + VIV_UINT64 filesize; + int image_wide; + int image_high; + HBITMAP hbitmap; + _viv_mipmap_t *mipmap; + int orientation; + + double zoom_percent; + int is_custom_zoom; + int is_1to1; + int is_autofit; + int view_x; + int view_y; + + RECT pane_rect; + RECT toolbar_rect; + RECT viewport_rect; + + RECT btn_prev_rect; + RECT btn_next_rect; + RECT btn_zoom_in_rect; + RECT btn_zoom_out_rect; + RECT zoom_text_rect; + RECT btn_1to1_rect; + RECT btn_fit_rect; + RECT btn_rotate_rect; + RECT btn_select_rect; + + _viv_playlist_t *playlist_item; +} _viv_compare_pane_t; + +typedef struct _viv_compare_state_s +{ + HWND hwnd; + int pane_count; + _viv_compare_pane_t panes[_VIV_COMPARE_PANE_COUNT]; + + int opt_autofit; + int opt_smooth; + int opt_filename; + + RECT topbar_rect; + RECT tips_rect; + RECT autofit_rect; + RECT smooth_rect; + RECT filename_rect; + RECT close_btn_rect; + + int is_dragging; + int drag_last_x; + int drag_last_y; + int drag_pane_index; + int drag_is_individual; + + int hovered_pane; + int hovered_btn; + int hovered_top; + + HFONT font_ui; + HFONT font_bold; + HFONT font_overlay; +} _viv_compare_state_t; + +static _viv_compare_state_t *_viv_compare_state = NULL; + +static void _viv_compare_free_pane_image(_viv_compare_pane_t *pane) +{ + if (pane->mipmap) + { + _viv_mipmap_free(pane->mipmap); + pane->mipmap = NULL; + } + if (pane->hbitmap) + { + DeleteObject(pane->hbitmap); + pane->hbitmap = NULL; + } + pane->image_wide = 0; + pane->image_high = 0; + pane->filesize = 0; +} + +static BOOL _viv_compare_load_image(const wchar_t *filename, _viv_compare_pane_t *pane) +{ + HANDLE h; + DWORD size; + HANDLE global_handle; + char *buf; + IStream *stream = NULL; + void *image = NULL; + int load_ret = 1; + WIN32_FIND_DATA fd; + + _viv_compare_free_pane_image(pane); + + if (!filename || !*filename) + { + pane->filename[0] = 0; + return FALSE; + } + + string_copy(pane->filename, filename); + + if (os_GetFileAttributesExW && os_GetFileAttributesExW(filename, GetFileExInfoStandard, &fd)) + { + pane->filesize = ((VIV_UINT64)fd.nFileSizeHigh << 32) | fd.nFileSizeLow; + } + else + { + pane->filesize = 0; + } + + h = CreateFileW(filename, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE | ((os_major_version >= 5) ? FILE_SHARE_DELETE : 0), 0, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, 0); + if (h == INVALID_HANDLE_VALUE) + { + return FALSE; + } + + size = GetFileSize(h, 0); + if (pane->filesize == 0) + { + pane->filesize = size; + } + + global_handle = GlobalAlloc(GMEM_MOVEABLE, size); + if (!global_handle) + { + CloseHandle(h); + return FALSE; + } + + buf = (char *)GlobalLock(global_handle); + if (buf) + { + DWORD numread; + DWORD totreadsize = size; + char *d = buf; + + while (totreadsize) + { + DWORD readsize = 1 * 1024 * 1024; + if (readsize > totreadsize) readsize = totreadsize; + if (ReadFile(h, d, readsize, &numread, 0)) + { + if (!numread) break; + d += numread; + totreadsize -= numread; + } + else + { + break; + } + } + GlobalUnlock(global_handle); + + if (totreadsize == 0) + { + if (SUCCEEDED(CreateStreamOnHGlobal(global_handle, TRUE, &stream))) + { + global_handle = 0; + } + } + } + + if (global_handle) + { + GlobalFree(global_handle); + } + CloseHandle(h); + + if (!stream) + { + return FALSE; + } + + if (config_orientation) + { + pane->orientation = os_get_orientation(filename); + } + else + { + pane->orientation = 0; + } + + if (os_GdipLoadImageFromStream) + { + if (config_icm && os_GdipLoadImageFromStreamICM) + { + load_ret = os_GdipLoadImageFromStreamICM(stream, &image); + } + else + { + load_ret = os_GdipLoadImageFromStream(stream, &image); + } + } + + if (load_ret == 0 && image) + { + UINT raw_wide = 0, raw_high = 0; + if (os_GdipGetImageWidth(image, &raw_wide) == 0 && os_GdipGetImageHeight(image, &raw_high) == 0) + { + HDC screen_hdc = GetDC(0); + if (screen_hdc) + { + HDC mem_hdc = CreateCompatibleDC(screen_hdc); + if (mem_hdc) + { + HBITMAP hbitmap = _viv_create_bitmap(screen_hdc, raw_wide, raw_high); + if (hbitmap) + { + HGDIOBJ last_hbitmap = SelectObject(mem_hdc, hbitmap); + void *g = NULL; + if (os_GdipCreateFromHDC(mem_hdc, &g) == 0 && g) + { + os_GdipSetCompositingMode(g, 1); + os_GdipSetCompositingQuality(g, 1); + os_GdipSetInterpolationMode(g, 5); + os_GdipSetPixelOffsetMode(g, 3); + os_GdipSetSmoothingMode(g, 3); + os_GdipDrawImageRectI(g, image, 0, 0, raw_wide, raw_high); + os_GdipDeleteGraphics(g); + + SelectObject(mem_hdc, last_hbitmap); + + if (pane->orientation) + { + HBITMAP oriented = _viv_orientate_hbitmap(hbitmap, pane->orientation); + if (oriented) + { + DeleteObject(hbitmap); + hbitmap = oriented; + + switch (pane->orientation) + { + case 5: + case 6: + case 7: + case 8: + { + UINT tmp = raw_wide; + raw_wide = raw_high; + raw_high = tmp; + break; + } + } + } + } + + pane->hbitmap = hbitmap; + pane->image_wide = raw_wide; + pane->image_high = raw_high; + pane->zoom_percent = 100.0; + pane->is_autofit = 1; + pane->view_x = 0; + pane->view_y = 0; + } + else + { + SelectObject(mem_hdc, last_hbitmap); + DeleteObject(hbitmap); + } + } + DeleteDC(mem_hdc); + } + ReleaseDC(0, screen_hdc); + } + } + os_GdipDisposeImage(image); + } + + IStream_Release(stream); + return (pane->hbitmap != NULL); +} + +static void _viv_compare_format_filesize(wchar_t *buf, VIV_UINT64 size) +{ + if (size >= 1024 * 1024 * 1024) + { + double gb = (double)size / (1024.0 * 1024.0 * 1024.0); + string_printf(buf, L"%.2f GB", gb); + } + else if (size >= 1024 * 1024) + { + double mb = (double)size / (1024.0 * 1024.0); + string_printf(buf, L"%.2f MB", mb); + } + else if (size >= 1024) + { + double kb = (double)size / 1024.0; + string_printf(buf, L"%.1f KB", kb); + } + else + { + string_printf(buf, L"%I64u Bytes", size); + } +} + +static void _viv_compare_update_layout(_viv_compare_state_t *cs, int wide, int high) +{ + int top_h = 32; + int toolbar_h = 28; + int pane_w, i; + + if (wide < 100) wide = 100; + if (high < 100) high = 100; + + cs->topbar_rect = (RECT){0, 0, wide, top_h}; + cs->tips_rect = (RECT){10, 6, wide - 420, top_h - 2}; + cs->autofit_rect = (RECT){wide - 410, 6, wide - 330, top_h - 6}; + cs->smooth_rect = (RECT){wide - 320, 6, wide - 240, top_h - 6}; + cs->filename_rect = (RECT){wide - 230, 6, wide - 130, top_h - 6}; + cs->close_btn_rect = (RECT){wide - 110, 4, wide - 10, top_h - 4}; + + pane_w = wide / cs->pane_count; + for (i = 0; i < cs->pane_count; i++) + { + int p_left = i * pane_w; + int p_right = (i == cs->pane_count - 1) ? wide : (p_left + pane_w); + _viv_compare_pane_t *pane = &cs->panes[i]; + + pane->pane_rect = (RECT){p_left, top_h, p_right, high}; + pane->toolbar_rect = (RECT){p_left, top_h, p_right, top_h + toolbar_h}; + pane->viewport_rect = (RECT){p_left, top_h + toolbar_h, p_right, high}; + + int by = top_h + 3; + int bh = toolbar_h - 6; + int bx = p_left + 6; + + pane->btn_prev_rect = (RECT){bx, by, bx + 22, by + bh}; bx += 24; + pane->btn_next_rect = (RECT){bx, by, bx + 22, by + bh}; bx += 28; + pane->btn_zoom_in_rect = (RECT){bx, by, bx + 22, by + bh}; bx += 24; + pane->btn_zoom_out_rect = (RECT){bx, by, bx + 22, by + bh}; bx += 24; + pane->zoom_text_rect = (RECT){bx, by, bx + 55, by + bh}; bx += 59; + pane->btn_1to1_rect = (RECT){bx, by, bx + 32, by + bh}; bx += 36; + pane->btn_fit_rect = (RECT){bx, by, bx + 32, by + bh}; bx += 36; + pane->btn_rotate_rect = (RECT){bx, by, bx + 32, by + bh}; bx += 36; + pane->btn_select_rect = (RECT){p_right - 30, by, p_right - 6, by + bh}; + } +} + +static int _viv_compare_hit_test(_viv_compare_state_t *cs, int x, int y, int *out_pane, int *out_btn) +{ + int i; + *out_pane = -1; + *out_btn = _VIV_COMPARE_BTN_NONE; + + if (PtInRect(&cs->close_btn_rect, (POINT){x, y})) return _VIV_COMPARE_TOP_CLOSE; + if (PtInRect(&cs->autofit_rect, (POINT){x, y})) return _VIV_COMPARE_TOP_AUTOFIT; + if (PtInRect(&cs->smooth_rect, (POINT){x, y})) return _VIV_COMPARE_TOP_SMOOTH; + if (PtInRect(&cs->filename_rect, (POINT){x, y})) return _VIV_COMPARE_TOP_FILENAME; + + for (i = 0; i < cs->pane_count; i++) + { + _viv_compare_pane_t *pane = &cs->panes[i]; + if (PtInRect(&pane->pane_rect, (POINT){x, y})) + { + *out_pane = i; + if (PtInRect(&pane->btn_prev_rect, (POINT){x, y})) { *out_btn = _VIV_COMPARE_BTN_PREV; return 0; } + if (PtInRect(&pane->btn_next_rect, (POINT){x, y})) { *out_btn = _VIV_COMPARE_BTN_NEXT; return 0; } + if (PtInRect(&pane->btn_zoom_in_rect, (POINT){x, y})) { *out_btn = _VIV_COMPARE_BTN_ZOOM_IN; return 0; } + if (PtInRect(&pane->btn_zoom_out_rect, (POINT){x, y})) { *out_btn = _VIV_COMPARE_BTN_ZOOM_OUT; return 0; } + if (PtInRect(&pane->zoom_text_rect, (POINT){x, y})) { *out_btn = _VIV_COMPARE_BTN_ZOOM_TEXT; return 0; } + if (PtInRect(&pane->btn_1to1_rect, (POINT){x, y})) { *out_btn = _VIV_COMPARE_BTN_1TO1; return 0; } + if (PtInRect(&pane->btn_fit_rect, (POINT){x, y})) { *out_btn = _VIV_COMPARE_BTN_FIT; return 0; } + if (PtInRect(&pane->btn_rotate_rect, (POINT){x, y})) { *out_btn = _VIV_COMPARE_BTN_ROTATE; return 0; } + if (PtInRect(&pane->btn_select_rect, (POINT){x, y})) { *out_btn = _VIV_COMPARE_BTN_SELECT; return 0; } + return 0; + } + } + return 0; +} + +static void _viv_compare_zoom_pane(_viv_compare_pane_t *pane, int zoom_in, int center_x, int center_y) +{ + int vw, vh, old_rw, old_rh, rx, ry, old_cursor_px, old_cursor_py; + int new_rw, new_rh; + __int64 new_cursor_px, new_cursor_py; + + if (!pane->hbitmap || pane->image_wide <= 0 || pane->image_high <= 0) + return; + + vw = pane->viewport_rect.right - pane->viewport_rect.left; + vh = pane->viewport_rect.bottom - pane->viewport_rect.top; + if (vw <= 0 || vh <= 0) return; + + old_rw = (int)((pane->image_wide * pane->zoom_percent) / 100.0 + 0.5); + old_rh = (int)((pane->image_high * pane->zoom_percent) / 100.0 + 0.5); + if (old_rw < 1) old_rw = 1; + if (old_rh < 1) old_rh = 1; + + rx = pane->viewport_rect.left + (vw / 2) - (old_rw / 2) - pane->view_x; + ry = pane->viewport_rect.top + (vh / 2) - (old_rh / 2) - pane->view_y; + + old_cursor_px = center_x - rx; + old_cursor_py = center_y - ry; + + pane->zoom_percent = _viv_calculate_next_zoom(pane->zoom_percent, zoom_in); + pane->is_autofit = 0; + pane->is_custom_zoom = 1; + pane->is_1to1 = 0; + + new_rw = (int)((pane->image_wide * pane->zoom_percent) / 100.0 + 0.5); + new_rh = (int)((pane->image_high * pane->zoom_percent) / 100.0 + 0.5); + if (new_rw < 1) new_rw = 1; + if (new_rh < 1) new_rh = 1; + + new_cursor_px = old_rw ? ((__int64)old_cursor_px * new_rw) / old_rw : 0; + new_cursor_py = old_rh ? ((__int64)old_cursor_py * new_rh) / old_rh : 0; + + pane->view_x = (vw / 2) - (new_rw / 2) - (center_x - pane->viewport_rect.left) + (int)new_cursor_px; + pane->view_y = (vh / 2) - (new_rh / 2) - (center_y - pane->viewport_rect.top) + (int)new_cursor_py; +} + +static void _viv_compare_pick_image_for_pane(HWND hwnd, _viv_compare_pane_t *pane) +{ + OPENFILENAMEW ofn; + wchar_t filename[STRING_SIZE]; + wchar_t filter[STRING_SIZE]; + int i; + + filename[0] = 0; + ZeroMemory(&ofn, sizeof(OPENFILENAMEW)); + ofn.lStructSize = sizeof(OPENFILENAMEW); + ofn.hwndOwner = hwnd; + + string_copy_utf8_string(filter, localization_get_string(LOCALIZATION_ID_OPEN_ALL_IMAGE_FILES)); + i = string_get_length(filter); + filter[i++] = 0; + string_copy(filter + i, L"*.bmp;*.gif;*.ico;*.jpg;*.jpeg;*.png;*.tif;*.tiff;*.webp"); + i += string_get_length(filter + i); + filter[i++] = 0; + string_copy_utf8_string(filter + i, localization_get_string(LOCALIZATION_ID_OPEN_ALL_FILES)); + i += string_get_length(filter + i); + filter[i++] = 0; + string_copy(filter + i, L"*.*"); + i += string_get_length(filter + i); + filter[i++] = 0; + filter[i++] = 0; + + ofn.lpstrFilter = filter; + ofn.lpstrFile = filename; + ofn.nMaxFile = STRING_SIZE; + ofn.Flags = OFN_FILEMUSTEXIST | OFN_HIDEREADONLY; + + if (GetOpenFileNameW(&ofn)) + { + _viv_compare_load_image(filename, pane); + pane->playlist_item = NULL; + InvalidateRect(hwnd, NULL, FALSE); + } +} + +static void _viv_compare_draw_button(HDC hdc, const RECT *rect, const wchar_t *text, int is_hovered, HFONT font) +{ + HGDIOBJ old_font = SelectObject(hdc, font); + HBRUSH br = CreateSolidBrush(is_hovered ? RGB(215, 230, 250) : RGB(242, 242, 242)); + HPEN pen = CreatePen(PS_SOLID, 1, is_hovered ? RGB(100, 150, 220) : RGB(190, 190, 190)); + HGDIOBJ old_br = SelectObject(hdc, br); + HGDIOBJ old_pen = SelectObject(hdc, pen); + + RoundRect(hdc, rect->left, rect->top, rect->right, rect->bottom, 4, 4); + + SetBkMode(hdc, TRANSPARENT); + SetTextColor(hdc, RGB(20, 20, 20)); + DrawTextW(hdc, text, -1, (LPRECT)rect, DT_CENTER | DT_VCENTER | DT_SINGLELINE); + + SelectObject(hdc, old_pen); + SelectObject(hdc, old_br); + SelectObject(hdc, old_font); + DeleteObject(pen); + DeleteObject(br); +} + +static void _viv_compare_draw_checkbox(HDC hdc, const RECT *rect, const wchar_t *text, int is_checked, int is_hovered, HFONT font) +{ + HGDIOBJ old_font = SelectObject(hdc, font); + RECT box_rect = {rect->left, rect->top + (rect->bottom - rect->top - 14) / 2, rect->left + 14, rect->top + (rect->bottom - rect->top - 14) / 2 + 14}; + RECT text_rect = {rect->left + 18, rect->top, rect->right, rect->bottom}; + + HBRUSH br = CreateSolidBrush(RGB(255, 255, 255)); + HPEN pen = CreatePen(PS_SOLID, 1, is_hovered ? RGB(50, 120, 220) : RGB(140, 140, 140)); + HGDIOBJ old_br = SelectObject(hdc, br); + HGDIOBJ old_pen = SelectObject(hdc, pen); + + Rectangle(hdc, box_rect.left, box_rect.top, box_rect.right, box_rect.bottom); + + if (is_checked) + { + HPEN check_pen = CreatePen(PS_SOLID, 2, RGB(20, 120, 20)); + HGDIOBJ old_cpen = SelectObject(hdc, check_pen); + MoveToEx(hdc, box_rect.left + 3, box_rect.top + 7, NULL); + LineTo(hdc, box_rect.left + 6, box_rect.top + 10); + LineTo(hdc, box_rect.left + 11, box_rect.top + 3); + SelectObject(hdc, old_cpen); + DeleteObject(check_pen); + } + + SetBkMode(hdc, TRANSPARENT); + SetTextColor(hdc, RGB(30, 30, 30)); + DrawTextW(hdc, text, -1, &text_rect, DT_LEFT | DT_VCENTER | DT_SINGLELINE); + + SelectObject(hdc, old_pen); + SelectObject(hdc, old_br); + SelectObject(hdc, old_font); + DeleteObject(pen); + DeleteObject(br); +} + +static void _viv_compare_paint(HWND hwnd, _viv_compare_state_t *cs) +{ + PAINTSTRUCT ps; + HDC hdc = BeginPaint(hwnd, &ps); + RECT rc; + int wide, high, i; + HDC mem_hdc; + HBITMAP mem_hbitmap; + HGDIOBJ old_mem_bitmap; + + GetClientRect(hwnd, &rc); + wide = rc.right - rc.left; + high = rc.bottom - rc.top; + + if (wide <= 0 || high <= 0) + { + EndPaint(hwnd, &ps); + return; + } + + mem_hdc = CreateCompatibleDC(hdc); + mem_hbitmap = CreateCompatibleBitmap(hdc, wide, high); + old_mem_bitmap = SelectObject(mem_hdc, mem_hbitmap); + + _viv_compare_update_layout(cs, wide, high); + + // 1. Draw Top Bar Background + { + HBRUSH top_br = CreateSolidBrush(RGB(242, 242, 245)); + HPEN top_pen = CreatePen(PS_SOLID, 1, RGB(185, 185, 195)); + HGDIOBJ old_br = SelectObject(mem_hdc, top_br); + HGDIOBJ old_pen = SelectObject(mem_hdc, top_pen); + + FillRect(mem_hdc, &cs->topbar_rect, top_br); + MoveToEx(mem_hdc, 0, cs->topbar_rect.bottom - 1, NULL); + LineTo(mem_hdc, wide, cs->topbar_rect.bottom - 1); + + SelectObject(mem_hdc, old_pen); + SelectObject(mem_hdc, old_br); + DeleteObject(top_pen); + DeleteObject(top_br); + + // Tips Text + { + wchar_t tips_wbuf[STRING_SIZE]; + string_copy_utf8_string(tips_wbuf, localization_get_string(LOCALIZATION_ID_COMPARE_TIPS)); + HGDIOBJ old_font = SelectObject(mem_hdc, cs->font_ui); + SetBkMode(mem_hdc, TRANSPARENT); + SetTextColor(mem_hdc, RGB(60, 60, 60)); + DrawTextW(mem_hdc, tips_wbuf, -1, &cs->tips_rect, DT_LEFT | DT_VCENTER | DT_SINGLELINE | DT_END_ELLIPSIS); + SelectObject(mem_hdc, old_font); + } + + // Checkboxes + { + wchar_t label[STRING_SIZE]; + string_copy_utf8_string(label, localization_get_string(LOCALIZATION_ID_COMPARE_AUTO_FIT)); + _viv_compare_draw_checkbox(mem_hdc, &cs->autofit_rect, label, cs->opt_autofit, cs->hovered_top == _VIV_COMPARE_TOP_AUTOFIT, cs->font_ui); + + string_copy_utf8_string(label, localization_get_string(LOCALIZATION_ID_COMPARE_SMOOTH)); + _viv_compare_draw_checkbox(mem_hdc, &cs->smooth_rect, label, cs->opt_smooth, cs->hovered_top == _VIV_COMPARE_TOP_SMOOTH, cs->font_ui); + + string_copy_utf8_string(label, localization_get_string(LOCALIZATION_ID_COMPARE_FILENAME)); + _viv_compare_draw_checkbox(mem_hdc, &cs->filename_rect, label, cs->opt_filename, cs->hovered_top == _VIV_COMPARE_TOP_FILENAME, cs->font_ui); + } + + // Close Button + { + wchar_t close_label[STRING_SIZE]; + string_copy_utf8_string(close_label, localization_get_string(LOCALIZATION_ID_COMPARE_CLOSE)); + _viv_compare_draw_button(mem_hdc, &cs->close_btn_rect, close_label, cs->hovered_top == _VIV_COMPARE_TOP_CLOSE, cs->font_bold); + } + } + + // 2. Draw Panes + for (i = 0; i < cs->pane_count; i++) + { + _viv_compare_pane_t *pane = &cs->panes[i]; + int vw = pane->viewport_rect.right - pane->viewport_rect.left; + int vh = pane->viewport_rect.bottom - pane->viewport_rect.top; + + // Toolbar + { + HBRUSH tb_br = CreateSolidBrush(RGB(236, 238, 242)); + HPEN tb_pen = CreatePen(PS_SOLID, 1, RGB(195, 198, 205)); + HGDIOBJ old_br = SelectObject(mem_hdc, tb_br); + HGDIOBJ old_pen = SelectObject(mem_hdc, tb_pen); + + FillRect(mem_hdc, &pane->toolbar_rect, tb_br); + MoveToEx(mem_hdc, pane->toolbar_rect.left, pane->toolbar_rect.bottom - 1, NULL); + LineTo(mem_hdc, pane->toolbar_rect.right, pane->toolbar_rect.bottom - 1); + + SelectObject(mem_hdc, old_pen); + SelectObject(mem_hdc, old_br); + DeleteObject(tb_pen); + DeleteObject(tb_br); + + // Buttons + _viv_compare_draw_button(mem_hdc, &pane->btn_prev_rect, L"<", (cs->hovered_pane == i && cs->hovered_btn == _VIV_COMPARE_BTN_PREV), cs->font_bold); + _viv_compare_draw_button(mem_hdc, &pane->btn_next_rect, L">", (cs->hovered_pane == i && cs->hovered_btn == _VIV_COMPARE_BTN_NEXT), cs->font_bold); + _viv_compare_draw_button(mem_hdc, &pane->btn_zoom_in_rect, L"+", (cs->hovered_pane == i && cs->hovered_btn == _VIV_COMPARE_BTN_ZOOM_IN), cs->font_bold); + _viv_compare_draw_button(mem_hdc, &pane->btn_zoom_out_rect, L"-", (cs->hovered_pane == i && cs->hovered_btn == _VIV_COMPARE_BTN_ZOOM_OUT), cs->font_bold); + + // Zoom text + { + wchar_t ztext[64]; + string_printf(ztext, L"%.0f%%", pane->zoom_percent); + _viv_compare_draw_button(mem_hdc, &pane->zoom_text_rect, ztext, (cs->hovered_pane == i && cs->hovered_btn == _VIV_COMPARE_BTN_ZOOM_TEXT), cs->font_ui); + } + + _viv_compare_draw_button(mem_hdc, &pane->btn_1to1_rect, L"1:1", (cs->hovered_pane == i && cs->hovered_btn == _VIV_COMPARE_BTN_1TO1), cs->font_ui); + _viv_compare_draw_button(mem_hdc, &pane->btn_fit_rect, L"Fit", (cs->hovered_pane == i && cs->hovered_btn == _VIV_COMPARE_BTN_FIT), cs->font_ui); + _viv_compare_draw_button(mem_hdc, &pane->btn_rotate_rect, L"90°", (cs->hovered_pane == i && cs->hovered_btn == _VIV_COMPARE_BTN_ROTATE), cs->font_ui); + _viv_compare_draw_button(mem_hdc, &pane->btn_select_rect, L"...", (cs->hovered_pane == i && cs->hovered_btn == _VIV_COMPARE_BTN_SELECT), cs->font_bold); + } + + // Viewport + { + HBRUSH vp_br = CreateSolidBrush(RGB(75, 75, 80)); + FillRect(mem_hdc, &pane->viewport_rect, vp_br); + DeleteObject(vp_br); + + if (pane->hbitmap && pane->image_wide > 0 && pane->image_high > 0) + { + int rw, rh, rx, ry; + HRGN clip_rgn; + + if (pane->is_autofit && vw > 0 && vh > 0) + { + double sx = (double)vw / (double)pane->image_wide; + double sy = (double)vh / (double)pane->image_high; + double sc = sx < sy ? sx : sy; + if (sc > 1.0) sc = 1.0; + pane->zoom_percent = sc * 100.0; + } + + rw = (int)((pane->image_wide * pane->zoom_percent) / 100.0 + 0.5); + rh = (int)((pane->image_high * pane->zoom_percent) / 100.0 + 0.5); + if (rw < 1) rw = 1; + if (rh < 1) rh = 1; + + rx = pane->viewport_rect.left + (vw / 2) - (rw / 2) - pane->view_x; + ry = pane->viewport_rect.top + (vh / 2) - (rh / 2) - pane->view_y; + + clip_rgn = CreateRectRgn(pane->viewport_rect.left, pane->viewport_rect.top, pane->viewport_rect.right, pane->viewport_rect.bottom); + SelectClipRgn(mem_hdc, clip_rgn); + + { + HDC img_dc = CreateCompatibleDC(mem_hdc); + int mip_w, mip_h; + HBITMAP mip = _viv_get_mipmap(pane->hbitmap, pane->image_wide, pane->image_high, rw, rh, &mip_w, &mip_h, &pane->mipmap); + if (mip) + { + HGDIOBJ old_img = SelectObject(img_dc, mip); + int last_mode = 0; + if (cs->opt_smooth) + { + last_mode = SetStretchBltMode(mem_hdc, HALFTONE); + SetBrushOrgEx(mem_hdc, -rx, -ry, NULL); + } + else + { + last_mode = SetStretchBltMode(mem_hdc, COLORONCOLOR); + } + + if (rw == mip_w && rh == mip_h) + { + BitBlt(mem_hdc, rx, ry, rw, rh, img_dc, 0, 0, SRCCOPY); + } + else + { + StretchBlt(mem_hdc, rx, ry, rw, rh, img_dc, 0, 0, mip_w, mip_h, SRCCOPY); + } + + SetStretchBltMode(mem_hdc, last_mode); + SelectObject(img_dc, old_img); + } + DeleteDC(img_dc); + } + + // Green Text Overlay + if (cs->opt_filename && *pane->filename) + { + wchar_t filename_part[STRING_SIZE]; + wchar_t size_part[64]; + wchar_t overlay_str[STRING_SIZE * 2]; + RECT text_rc; + + string_get_filename_part(filename_part, pane->filename); + _viv_compare_format_filesize(size_part, pane->filesize); + string_printf(overlay_str, L"%s , %d x %d , %s", filename_part, pane->image_wide, pane->image_high, size_part); + + text_rc = (RECT){pane->viewport_rect.left + 10, pane->viewport_rect.top + 10, pane->viewport_rect.right - 10, pane->viewport_rect.top + 40}; + + HGDIOBJ old_font = SelectObject(mem_hdc, cs->font_overlay); + SetBkMode(mem_hdc, TRANSPARENT); + + // Text Shadow + SetTextColor(mem_hdc, RGB(0, 0, 0)); + OffsetRect(&text_rc, 1, 1); + DrawTextW(mem_hdc, overlay_str, -1, &text_rc, DT_LEFT | DT_TOP | DT_SINGLELINE | DT_END_ELLIPSIS); + + // Green Text + SetTextColor(mem_hdc, RGB(0, 235, 0)); + OffsetRect(&text_rc, -1, -1); + DrawTextW(mem_hdc, overlay_str, -1, &text_rc, DT_LEFT | DT_TOP | DT_SINGLELINE | DT_END_ELLIPSIS); + + SelectObject(mem_hdc, old_font); + } + + SelectClipRgn(mem_hdc, NULL); + DeleteObject(clip_rgn); + } + else + { + HGDIOBJ old_font = SelectObject(mem_hdc, cs->font_ui); + RECT center_rc = pane->viewport_rect; + SetBkMode(mem_hdc, TRANSPARENT); + SetTextColor(mem_hdc, RGB(180, 180, 180)); + DrawTextW(mem_hdc, L"[No Image - Click ... to select file]", -1, ¢er_rc, DT_CENTER | DT_VCENTER | DT_SINGLELINE); + SelectObject(mem_hdc, old_font); + } + } + + // Splitter line between panes + if (i < cs->pane_count - 1) + { + HPEN split_pen = CreatePen(PS_SOLID, 1, RGB(40, 40, 45)); + HGDIOBJ old_spen = SelectObject(mem_hdc, split_pen); + MoveToEx(mem_hdc, pane->pane_rect.right, cs->topbar_rect.bottom, NULL); + LineTo(mem_hdc, pane->pane_rect.right, high); + SelectObject(mem_hdc, old_spen); + DeleteObject(split_pen); + } + } + + BitBlt(hdc, 0, 0, wide, high, mem_hdc, 0, 0, SRCCOPY); + + SelectObject(mem_hdc, old_mem_bitmap); + DeleteObject(mem_hbitmap); + DeleteDC(mem_hdc); + + EndPaint(hwnd, &ps); +} + +static LRESULT CALLBACK _viv_compare_proc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) +{ + _viv_compare_state_t *cs = _viv_compare_state; + + switch (msg) + { + case WM_CREATE: + return 0; + + case WM_ERASEBKGND: + return 1; + + case WM_PAINT: + if (cs) + { + _viv_compare_paint(hwnd, cs); + } + return 0; + + case WM_SIZE: + if (cs) + { + int wide = LOWORD(lParam); + int high = HIWORD(lParam); + _viv_compare_update_layout(cs, wide, high); + InvalidateRect(hwnd, NULL, FALSE); + } + return 0; + + case WM_MOUSEMOVE: + if (cs) + { + int x = GET_X_LPARAM(lParam); + int y = GET_Y_LPARAM(lParam); + + if (cs->is_dragging) + { + int dx = x - cs->drag_last_x; + int dy = y - cs->drag_last_y; + cs->drag_last_x = x; + cs->drag_last_y = y; + + if (cs->drag_is_individual && cs->drag_pane_index >= 0 && cs->drag_pane_index < cs->pane_count) + { + cs->panes[cs->drag_pane_index].view_x -= dx; + cs->panes[cs->drag_pane_index].view_y -= dy; + } + else + { + int i; + for (i = 0; i < cs->pane_count; i++) + { + cs->panes[i].view_x -= dx; + cs->panes[i].view_y -= dy; + } + } + InvalidateRect(hwnd, NULL, FALSE); + } + else + { + int pane_idx, btn_idx; + int top_res = _viv_compare_hit_test(cs, x, y, &pane_idx, &btn_idx); + + if (top_res != cs->hovered_top || pane_idx != cs->hovered_pane || btn_idx != cs->hovered_btn) + { + cs->hovered_top = top_res; + cs->hovered_pane = pane_idx; + cs->hovered_btn = btn_idx; + InvalidateRect(hwnd, NULL, FALSE); + } + } + } + return 0; + + case WM_LBUTTONDOWN: + if (cs) + { + int x = GET_X_LPARAM(lParam); + int y = GET_Y_LPARAM(lParam); + int pane_idx, btn_idx; + int top_res = _viv_compare_hit_test(cs, x, y, &pane_idx, &btn_idx); + + if (top_res == _VIV_COMPARE_TOP_CLOSE) + { + DestroyWindow(hwnd); + return 0; + } + else if (top_res == _VIV_COMPARE_TOP_AUTOFIT) + { + int i; + cs->opt_autofit = !cs->opt_autofit; + for (i = 0; i < cs->pane_count; i++) + { + cs->panes[i].is_autofit = cs->opt_autofit; + cs->panes[i].view_x = 0; + cs->panes[i].view_y = 0; + } + InvalidateRect(hwnd, NULL, FALSE); + return 0; + } + else if (top_res == _VIV_COMPARE_TOP_SMOOTH) + { + cs->opt_smooth = !cs->opt_smooth; + InvalidateRect(hwnd, NULL, FALSE); + return 0; + } + else if (top_res == _VIV_COMPARE_TOP_FILENAME) + { + cs->opt_filename = !cs->opt_filename; + InvalidateRect(hwnd, NULL, FALSE); + return 0; + } + + if (pane_idx >= 0 && pane_idx < cs->pane_count) + { + _viv_compare_pane_t *p = &cs->panes[pane_idx]; + if (btn_idx == _VIV_COMPARE_BTN_PREV) + { + if (p->playlist_item && p->playlist_item->prev) + { + p->playlist_item = p->playlist_item->prev; + _viv_compare_load_image(p->playlist_item->fd.cFileName, p); + InvalidateRect(hwnd, NULL, FALSE); + } + return 0; + } + else if (btn_idx == _VIV_COMPARE_BTN_NEXT) + { + if (p->playlist_item && p->playlist_item->next) + { + p->playlist_item = p->playlist_item->next; + _viv_compare_load_image(p->playlist_item->fd.cFileName, p); + InvalidateRect(hwnd, NULL, FALSE); + } + return 0; + } + else if (btn_idx == _VIV_COMPARE_BTN_ZOOM_IN) + { + int is_indiv = (GetKeyState(VK_CONTROL) < 0); + if (is_indiv) + { + int cx = (p->viewport_rect.left + p->viewport_rect.right) / 2; + int cy = (p->viewport_rect.top + p->viewport_rect.bottom) / 2; + _viv_compare_zoom_pane(p, 1, cx, cy); + } + else + { + int k; + for (k = 0; k < cs->pane_count; k++) + { + int cx = (cs->panes[k].viewport_rect.left + cs->panes[k].viewport_rect.right) / 2; + int cy = (cs->panes[k].viewport_rect.top + cs->panes[k].viewport_rect.bottom) / 2; + _viv_compare_zoom_pane(&cs->panes[k], 1, cx, cy); + } + } + InvalidateRect(hwnd, NULL, FALSE); + return 0; + } + else if (btn_idx == _VIV_COMPARE_BTN_ZOOM_OUT) + { + int is_indiv = (GetKeyState(VK_CONTROL) < 0); + if (is_indiv) + { + int cx = (p->viewport_rect.left + p->viewport_rect.right) / 2; + int cy = (p->viewport_rect.top + p->viewport_rect.bottom) / 2; + _viv_compare_zoom_pane(p, 0, cx, cy); + } + else + { + int k; + for (k = 0; k < cs->pane_count; k++) + { + int cx = (cs->panes[k].viewport_rect.left + cs->panes[k].viewport_rect.right) / 2; + int cy = (cs->panes[k].viewport_rect.top + cs->panes[k].viewport_rect.bottom) / 2; + _viv_compare_zoom_pane(&cs->panes[k], 0, cx, cy); + } + } + InvalidateRect(hwnd, NULL, FALSE); + return 0; + } + else if (btn_idx == _VIV_COMPARE_BTN_1TO1) + { + int is_indiv = (GetKeyState(VK_CONTROL) < 0); + if (is_indiv) + { + p->zoom_percent = 100.0; + p->is_1to1 = 1; + p->is_autofit = 0; + p->view_x = 0; + p->view_y = 0; + } + else + { + int k; + for (k = 0; k < cs->pane_count; k++) + { + cs->panes[k].zoom_percent = 100.0; + cs->panes[k].is_1to1 = 1; + cs->panes[k].is_autofit = 0; + cs->panes[k].view_x = 0; + cs->panes[k].view_y = 0; + } + } + InvalidateRect(hwnd, NULL, FALSE); + return 0; + } + else if (btn_idx == _VIV_COMPARE_BTN_FIT) + { + int is_indiv = (GetKeyState(VK_CONTROL) < 0); + if (is_indiv) + { + p->is_autofit = 1; + p->view_x = 0; + p->view_y = 0; + } + else + { + int k; + for (k = 0; k < cs->pane_count; k++) + { + cs->panes[k].is_autofit = 1; + cs->panes[k].view_x = 0; + cs->panes[k].view_y = 0; + } + } + InvalidateRect(hwnd, NULL, FALSE); + return 0; + } + else if (btn_idx == _VIV_COMPARE_BTN_ROTATE) + { + if (p->hbitmap) + { + HBITMAP rot = _viv_orientate_hbitmap(p->hbitmap, 6); // 90 deg cw + if (rot) + { + int tmp; + DeleteObject(p->hbitmap); + p->hbitmap = rot; + if (p->mipmap) + { + _viv_mipmap_free(p->mipmap); + p->mipmap = NULL; + } + tmp = p->image_wide; + p->image_wide = p->image_high; + p->image_high = tmp; + InvalidateRect(hwnd, NULL, FALSE); + } + } + return 0; + } + else if (btn_idx == _VIV_COMPARE_BTN_SELECT) + { + _viv_compare_pick_image_for_pane(hwnd, p); + return 0; + } + + // Click inside viewport: Start Dragging + if (PtInRect(&p->viewport_rect, (POINT){x, y})) + { + cs->is_dragging = 1; + cs->drag_last_x = x; + cs->drag_last_y = y; + cs->drag_pane_index = pane_idx; + cs->drag_is_individual = (GetKeyState(VK_CONTROL) < 0); + SetCapture(hwnd); + } + } + } + return 0; + + case WM_LBUTTONUP: + case WM_RBUTTONUP: + if (cs && cs->is_dragging) + { + cs->is_dragging = 0; + ReleaseCapture(); + } + return 0; + + case WM_RBUTTONDOWN: + if (cs) + { + int x = GET_X_LPARAM(lParam); + int y = GET_Y_LPARAM(lParam); + int pane_idx, btn_idx; + _viv_compare_hit_test(cs, x, y, &pane_idx, &btn_idx); + + if (pane_idx >= 0 && pane_idx < cs->pane_count) + { + if (PtInRect(&cs->panes[pane_idx].viewport_rect, (POINT){x, y})) + { + cs->is_dragging = 1; + cs->drag_last_x = x; + cs->drag_last_y = y; + cs->drag_pane_index = pane_idx; + cs->drag_is_individual = 1; // Right-drag is always individual + SetCapture(hwnd); + } + } + } + return 0; + + case WM_MOUSEWHEEL: + if (cs) + { + POINT pt; + int delta = GET_WHEEL_DELTA_WPARAM(wParam); + int zoom_in = (delta > 0); + int is_indiv; + int pane_idx, btn_idx; + + pt.x = GET_X_LPARAM(lParam); + pt.y = GET_Y_LPARAM(lParam); + ScreenToClient(hwnd, &pt); + + _viv_compare_hit_test(cs, pt.x, pt.y, &pane_idx, &btn_idx); + is_indiv = (GetKeyState(VK_CONTROL) < 0) || (GetKeyState(VK_RBUTTON) < 0); + + if (is_indiv && pane_idx >= 0 && pane_idx < cs->pane_count) + { + _viv_compare_zoom_pane(&cs->panes[pane_idx], zoom_in, pt.x, pt.y); + } + else + { + int k; + for (k = 0; k < cs->pane_count; k++) + { + if (k == pane_idx) + { + _viv_compare_zoom_pane(&cs->panes[k], zoom_in, pt.x, pt.y); + } + else + { + int cx = (cs->panes[k].viewport_rect.left + cs->panes[k].viewport_rect.right) / 2; + int cy = (cs->panes[k].viewport_rect.top + cs->panes[k].viewport_rect.bottom) / 2; + _viv_compare_zoom_pane(&cs->panes[k], zoom_in, cx, cy); + } + } + } + InvalidateRect(hwnd, NULL, FALSE); + } + return 0; + + case WM_KEYDOWN: + if (cs) + { + switch (wParam) + { + case VK_ESCAPE: + DestroyWindow(hwnd); + return 0; + + case VK_LEFT: + case VK_PRIOR: + { + int idx = (cs->hovered_pane >= 0 && cs->hovered_pane < cs->pane_count) ? cs->hovered_pane : 0; + _viv_compare_pane_t *p = &cs->panes[idx]; + if (p->playlist_item && p->playlist_item->prev) + { + p->playlist_item = p->playlist_item->prev; + _viv_compare_load_image(p->playlist_item->fd.cFileName, p); + InvalidateRect(hwnd, NULL, FALSE); + } + return 0; + } + + case VK_RIGHT: + case VK_NEXT: + case VK_SPACE: + { + int idx = (cs->hovered_pane >= 0 && cs->hovered_pane < cs->pane_count) ? cs->hovered_pane : 0; + _viv_compare_pane_t *p = &cs->panes[idx]; + if (p->playlist_item && p->playlist_item->next) + { + p->playlist_item = p->playlist_item->next; + _viv_compare_load_image(p->playlist_item->fd.cFileName, p); + InvalidateRect(hwnd, NULL, FALSE); + } + return 0; + } + + case VK_ADD: + case VK_OEM_PLUS: + { + int k; + for (k = 0; k < cs->pane_count; k++) + { + int cx = (cs->panes[k].viewport_rect.left + cs->panes[k].viewport_rect.right) / 2; + int cy = (cs->panes[k].viewport_rect.top + cs->panes[k].viewport_rect.bottom) / 2; + _viv_compare_zoom_pane(&cs->panes[k], 1, cx, cy); + } + InvalidateRect(hwnd, NULL, FALSE); + return 0; + } + + case VK_SUBTRACT: + case VK_OEM_MINUS: + { + int k; + for (k = 0; k < cs->pane_count; k++) + { + int cx = (cs->panes[k].viewport_rect.left + cs->panes[k].viewport_rect.right) / 2; + int cy = (cs->panes[k].viewport_rect.top + cs->panes[k].viewport_rect.bottom) / 2; + _viv_compare_zoom_pane(&cs->panes[k], 0, cx, cy); + } + InvalidateRect(hwnd, NULL, FALSE); + return 0; + } + + case '1': + { + int k; + for (k = 0; k < cs->pane_count; k++) + { + cs->panes[k].zoom_percent = 100.0; + cs->panes[k].is_1to1 = 1; + cs->panes[k].is_autofit = 0; + cs->panes[k].view_x = 0; + cs->panes[k].view_y = 0; + } + InvalidateRect(hwnd, NULL, FALSE); + return 0; + } + + case 'F': + { + int k; + for (k = 0; k < cs->pane_count; k++) + { + cs->panes[k].is_autofit = 1; + cs->panes[k].view_x = 0; + cs->panes[k].view_y = 0; + } + InvalidateRect(hwnd, NULL, FALSE); + return 0; + } + } + } + break; + + case WM_DESTROY: + if (cs) + { + int i; + for (i = 0; i < cs->pane_count; i++) + { + _viv_compare_free_pane_image(&cs->panes[i]); + } + if (cs->font_ui) DeleteObject(cs->font_ui); + if (cs->font_bold) DeleteObject(cs->font_bold); + if (cs->font_overlay) DeleteObject(cs->font_overlay); + + mem_free(cs); + _viv_compare_state = NULL; + } + EnableWindow(_viv_hwnd, TRUE); + SetForegroundWindow(_viv_hwnd); + return 0; + } + + return DefWindowProcW(hwnd, msg, wParam, lParam); +} + +static void _viv_compare_show(void) +{ + _viv_compare_state_t *cs; + HWND hwnd; + RECT rc; + static int class_registered = 0; + + if (_viv_compare_state && _viv_compare_state->hwnd) + { + SetForegroundWindow(_viv_compare_state->hwnd); + return; + } + + if (!class_registered) + { + os_RegisterClassEx( + CS_DBLCLKS | CS_VREDRAW | CS_HREDRAW, + _viv_compare_proc, + (HICON)LoadImage(os_hinstance, MAKEINTRESOURCE(IDI_ICON1), IMAGE_ICON, GetSystemMetrics(SM_CXICON), GetSystemMetrics(SM_CYICON), 0), + LoadCursor(NULL, IDC_ARROW), + (HBRUSH)(COLOR_BTNFACE + 1), + "_VIV_COMPARE", + (HICON)LoadImage(os_hinstance, MAKEINTRESOURCE(IDI_ICON1), IMAGE_ICON, GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON), 0)); + class_registered = 1; + } + + cs = (_viv_compare_state_t *)mem_alloc(sizeof(_viv_compare_state_t)); + ZeroMemory(cs, sizeof(_viv_compare_state_t)); + _viv_compare_state = cs; + + cs->pane_count = _VIV_COMPARE_PANE_COUNT; + cs->opt_autofit = 1; + cs->opt_smooth = (config_shrink_blit_mode == CONFIG_SHRINK_BLIT_MODE_HALFTONE); + cs->opt_filename = 1; + + cs->font_ui = CreateFontW(-12, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH | FF_DONTCARE, L"Segoe UI"); + if (!cs->font_ui) cs->font_ui = (HFONT)GetStockObject(DEFAULT_GUI_FONT); + + cs->font_bold = CreateFontW(-12, 0, 0, 0, FW_BOLD, FALSE, FALSE, FALSE, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH | FF_DONTCARE, L"Segoe UI"); + if (!cs->font_bold) cs->font_bold = (HFONT)GetStockObject(DEFAULT_GUI_FONT); + + cs->font_overlay = CreateFontW(-14, 0, 0, 0, FW_BOLD, FALSE, FALSE, FALSE, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH | FF_DONTCARE, L"Segoe UI"); + if (!cs->font_overlay) cs->font_overlay = cs->font_bold; + + // Setup initial images + if (*_viv_current_fd->cFileName) + { + _viv_playlist_t *cur_item = _viv_playlist_from_fd(_viv_current_fd); + cs->panes[0].playlist_item = cur_item; + _viv_compare_load_image(_viv_current_fd->cFileName, &cs->panes[0]); + + if (cur_item && cur_item->next) + { + cs->panes[1].playlist_item = cur_item->next; + _viv_compare_load_image(cur_item->next->fd.cFileName, &cs->panes[1]); + } + else if (cur_item && cur_item->prev) + { + cs->panes[1].playlist_item = cur_item->prev; + _viv_compare_load_image(cur_item->prev->fd.cFileName, &cs->panes[1]); + } + else + { + cs->panes[1].playlist_item = cur_item; + _viv_compare_load_image(_viv_current_fd->cFileName, &cs->panes[1]); + } + } + + GetWindowRect(_viv_hwnd, &rc); + + hwnd = os_CreateWindowEx( + WS_EX_APPWINDOW, + "_VIV_COMPARE", + localization_get_string(LOCALIZATION_ID_COMPARE_IMAGES), + WS_OVERLAPPEDWINDOW | WS_VISIBLE | WS_CLIPCHILDREN, + rc.left, rc.top, rc.right - rc.left, rc.bottom - rc.top, + _viv_hwnd, 0, os_hinstance, NULL); + + cs->hwnd = hwnd; + + if (hwnd) + { + ShowWindow(hwnd, SW_MAXIMIZE); + EnableWindow(_viv_hwnd, FALSE); + } +} + static void _viv_nav_item_free_all(void) { if (_viv_nav_items) diff --git a/src/viv.h b/src/viv.h index e914218a..e1662d12 100644 --- a/src/viv.h +++ b/src/viv.h @@ -113,6 +113,7 @@ enum VIV_ID_VIEW_BESTFIT, VIV_ID_VIEW_FULLSCREEN, VIV_ID_VIEW_SLIDESHOW, + VIV_ID_VIEW_COMPARE_IMAGES, VIV_ID_VIEW_ONTOP_ALWAYS, VIV_ID_VIEW_ONTOP_WHILE_PLAYING_OR_ANIMATING, VIV_ID_VIEW_ONTOP_NEVER, From e23894a2386c5cef3711af157ae4e2377873de11 Mon Sep 17 00:00:00 2001 From: Syahda Fahreza Date: Sat, 29 Aug 2026 22:06:05 +0700 Subject: [PATCH 08/15] Use 16-bit localization IDs and fix formatting Increase localization_id_t to unsigned short and set LOCALIZATION_ID_INVALID to 65535 to allow larger localization ID range (src/localization.h). In src/viv.c, correct several formatting and type issues in _viv_compare_format_filesize and _viv_compare_paint: switch format specifiers and casts for filesize and bytes output, render zoom percent as integer, obtain filename_part as a const wchar_t* from string_get_filename_part, and adjust overlay string formatting. These changes address type/overflow and incorrect printf format usage. --- src/localization.h | 4 ++-- src/viv.c | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/localization.h b/src/localization.h index 8dd97ffb..5acdebf0 100644 --- a/src/localization.h +++ b/src/localization.h @@ -33,9 +33,9 @@ enum LOCALIZATION_LANGUAGE_COUNT, }; -typedef BYTE localization_id_t; +typedef unsigned short localization_id_t; -#define LOCALIZATION_ID_INVALID 255 +#define LOCALIZATION_ID_INVALID 65535 // Get localized string for a command name // Returns UTF-8 encoded string diff --git a/src/viv.c b/src/viv.c index ddc6800d..22965a99 100644 --- a/src/viv.c +++ b/src/viv.c @@ -13908,21 +13908,21 @@ static void _viv_compare_format_filesize(wchar_t *buf, VIV_UINT64 size) if (size >= 1024 * 1024 * 1024) { double gb = (double)size / (1024.0 * 1024.0 * 1024.0); - string_printf(buf, L"%.2f GB", gb); + string_printf(buf, "%f GB", gb); } else if (size >= 1024 * 1024) { double mb = (double)size / (1024.0 * 1024.0); - string_printf(buf, L"%.2f MB", mb); + string_printf(buf, "%f MB", mb); } else if (size >= 1024) { double kb = (double)size / 1024.0; - string_printf(buf, L"%.1f KB", kb); + string_printf(buf, "%f KB", kb); } else { - string_printf(buf, L"%I64u Bytes", size); + string_printf(buf, "%u Bytes", (DWORD)size); } } @@ -14243,7 +14243,7 @@ static void _viv_compare_paint(HWND hwnd, _viv_compare_state_t *cs) // Zoom text { wchar_t ztext[64]; - string_printf(ztext, L"%.0f%%", pane->zoom_percent); + string_printf(ztext, "%d%%", (int)(pane->zoom_percent + 0.5)); _viv_compare_draw_button(mem_hdc, &pane->zoom_text_rect, ztext, (cs->hovered_pane == i && cs->hovered_btn == _VIV_COMPARE_BTN_ZOOM_TEXT), cs->font_ui); } @@ -14320,14 +14320,14 @@ static void _viv_compare_paint(HWND hwnd, _viv_compare_state_t *cs) // Green Text Overlay if (cs->opt_filename && *pane->filename) { - wchar_t filename_part[STRING_SIZE]; + const wchar_t *filename_part; wchar_t size_part[64]; wchar_t overlay_str[STRING_SIZE * 2]; RECT text_rc; - string_get_filename_part(filename_part, pane->filename); + filename_part = string_get_filename_part(pane->filename); _viv_compare_format_filesize(size_part, pane->filesize); - string_printf(overlay_str, L"%s , %d x %d , %s", filename_part, pane->image_wide, pane->image_high, size_part); + string_printf(overlay_str, "%S , %d x %d , %S", filename_part, pane->image_wide, pane->image_high, size_part); text_rc = (RECT){pane->viewport_rect.left + 10, pane->viewport_rect.top + 10, pane->viewport_rect.right - 10, pane->viewport_rect.top + 40}; From b07ebaac45562686e4075067d637075f5d7f9a2d Mon Sep 17 00:00:00 2001 From: Syahda Fahreza Date: Sat, 29 Aug 2026 22:15:26 +0700 Subject: [PATCH 09/15] Simplify compare zoom and start empty This change makes compare mode zoom all panes around their viewport centers so the zoom behavior stays consistent across panes. It also removes the automatic initial image pairing in compare view, leaving panes empty until the user selects images manually. --- src/viv.c | 37 ++++--------------------------------- 1 file changed, 4 insertions(+), 33 deletions(-) diff --git a/src/viv.c b/src/viv.c index 22965a99..97830e2a 100644 --- a/src/viv.c +++ b/src/viv.c @@ -14710,16 +14710,9 @@ static LRESULT CALLBACK _viv_compare_proc(HWND hwnd, UINT msg, WPARAM wParam, LP int k; for (k = 0; k < cs->pane_count; k++) { - if (k == pane_idx) - { - _viv_compare_zoom_pane(&cs->panes[k], zoom_in, pt.x, pt.y); - } - else - { - int cx = (cs->panes[k].viewport_rect.left + cs->panes[k].viewport_rect.right) / 2; - int cy = (cs->panes[k].viewport_rect.top + cs->panes[k].viewport_rect.bottom) / 2; - _viv_compare_zoom_pane(&cs->panes[k], zoom_in, cx, cy); - } + int cx = (cs->panes[k].viewport_rect.left + cs->panes[k].viewport_rect.right) / 2; + int cy = (cs->panes[k].viewport_rect.top + cs->panes[k].viewport_rect.bottom) / 2; + _viv_compare_zoom_pane(&cs->panes[k], zoom_in, cx, cy); } } InvalidateRect(hwnd, NULL, FALSE); @@ -14890,29 +14883,7 @@ static void _viv_compare_show(void) cs->font_overlay = CreateFontW(-14, 0, 0, 0, FW_BOLD, FALSE, FALSE, FALSE, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH | FF_DONTCARE, L"Segoe UI"); if (!cs->font_overlay) cs->font_overlay = cs->font_bold; - // Setup initial images - if (*_viv_current_fd->cFileName) - { - _viv_playlist_t *cur_item = _viv_playlist_from_fd(_viv_current_fd); - cs->panes[0].playlist_item = cur_item; - _viv_compare_load_image(_viv_current_fd->cFileName, &cs->panes[0]); - - if (cur_item && cur_item->next) - { - cs->panes[1].playlist_item = cur_item->next; - _viv_compare_load_image(cur_item->next->fd.cFileName, &cs->panes[1]); - } - else if (cur_item && cur_item->prev) - { - cs->panes[1].playlist_item = cur_item->prev; - _viv_compare_load_image(cur_item->prev->fd.cFileName, &cs->panes[1]); - } - else - { - cs->panes[1].playlist_item = cur_item; - _viv_compare_load_image(_viv_current_fd->cFileName, &cs->panes[1]); - } - } + // Start with empty panes - user will select images manually via browse button GetWindowRect(_viv_hwnd, &rc); From 80541e63927c3b899ace85bc7efef0fe7332dd66 Mon Sep 17 00:00:00 2001 From: Syahda Fahreza Date: Sat, 29 Aug 2026 22:28:13 +0700 Subject: [PATCH 10/15] Add 'Use Current Image' option to compare pane Introduce a 'Use Current Image' option in the image comparison pane. Adds a new localization id and strings (en,id,zh). Adds UI state (use_current) and rect, a new compare button id, layout and hit-test handling, and painting support via _viv_compare_draw_checkbox_ex. Enforces only one pane can use the current image at a time, toggles loading/clearing the current playlist image into a pane, and updates related event handling and drawing logic. --- src/localization.h | 1 + src/localization_en_us.h | 1 + src/localization_id_id.h | 1 + src/localization_zh_cn.h | 1 + src/viv.c | 99 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 103 insertions(+) diff --git a/src/localization.h b/src/localization.h index 5acdebf0..a633c7c1 100644 --- a/src/localization.h +++ b/src/localization.h @@ -346,6 +346,7 @@ enum LOCALIZATION_ID_COMPARE_BEST_FIT, LOCALIZATION_ID_COMPARE_ROTATE, LOCALIZATION_ID_COMPARE_SELECT_IMAGE, + LOCALIZATION_ID_COMPARE_USE_CURRENT, LOCALIZATION_ID_COUNT, }; diff --git a/src/localization_en_us.h b/src/localization_en_us.h index a38e1000..6c4bd679 100644 --- a/src/localization_en_us.h +++ b/src/localization_en_us.h @@ -321,4 +321,5 @@ static const utf8_t *_localization_string_array_en_us[LOCALIZATION_ID_COUNT] = "Fit", // LOCALIZATION_ID_COMPARE_BEST_FIT, "Rotate", // LOCALIZATION_ID_COMPARE_ROTATE, "Select Image", // LOCALIZATION_ID_COMPARE_SELECT_IMAGE, + "Use Current Image", // LOCALIZATION_ID_COMPARE_USE_CURRENT, }; diff --git a/src/localization_id_id.h b/src/localization_id_id.h index f49fae18..18efdb83 100644 --- a/src/localization_id_id.h +++ b/src/localization_id_id.h @@ -321,4 +321,5 @@ static const utf8_t *_localization_string_array_id_id[LOCALIZATION_ID_COUNT] = "Pas", // LOCALIZATION_ID_COMPARE_BEST_FIT, "Putar", // LOCALIZATION_ID_COMPARE_ROTATE, "Pilih Gambar", // LOCALIZATION_ID_COMPARE_SELECT_IMAGE, + "Gunakan Gambar Saat Ini", // LOCALIZATION_ID_COMPARE_USE_CURRENT, }; diff --git a/src/localization_zh_cn.h b/src/localization_zh_cn.h index b08e18c6..fbc0d98c 100644 --- a/src/localization_zh_cn.h +++ b/src/localization_zh_cn.h @@ -322,4 +322,5 @@ static const utf8_t *_localization_string_array_zh_cn[LOCALIZATION_ID_COUNT] = "适合", // LOCALIZATION_ID_COMPARE_BEST_FIT, "旋转", // LOCALIZATION_ID_COMPARE_ROTATE, "选择图像", // LOCALIZATION_ID_COMPARE_SELECT_IMAGE, + "使用当前图像", // LOCALIZATION_ID_COMPARE_USE_CURRENT, }; diff --git a/src/viv.c b/src/viv.c index 97830e2a..a59d82a9 100644 --- a/src/viv.c +++ b/src/viv.c @@ -13619,6 +13619,7 @@ enum { _VIV_COMPARE_BTN_FIT, _VIV_COMPARE_BTN_ROTATE, _VIV_COMPARE_BTN_SELECT, + _VIV_COMPARE_BTN_USE_CURRENT, _VIV_COMPARE_TOP_AUTOFIT = 101, _VIV_COMPARE_TOP_SMOOTH = 102, @@ -13656,6 +13657,9 @@ typedef struct _viv_compare_pane_s RECT btn_fit_rect; RECT btn_rotate_rect; RECT btn_select_rect; + RECT use_current_rect; + + int use_current; _viv_playlist_t *playlist_item; } _viv_compare_pane_t; @@ -13966,6 +13970,7 @@ static void _viv_compare_update_layout(_viv_compare_state_t *cs, int wide, int h pane->btn_fit_rect = (RECT){bx, by, bx + 32, by + bh}; bx += 36; pane->btn_rotate_rect = (RECT){bx, by, bx + 32, by + bh}; bx += 36; pane->btn_select_rect = (RECT){p_right - 30, by, p_right - 6, by + bh}; + pane->use_current_rect = (RECT){p_right - 215, by, p_right - 36, by + bh}; } } @@ -13994,6 +13999,7 @@ static int _viv_compare_hit_test(_viv_compare_state_t *cs, int x, int y, int *ou if (PtInRect(&pane->btn_1to1_rect, (POINT){x, y})) { *out_btn = _VIV_COMPARE_BTN_1TO1; return 0; } if (PtInRect(&pane->btn_fit_rect, (POINT){x, y})) { *out_btn = _VIV_COMPARE_BTN_FIT; return 0; } if (PtInRect(&pane->btn_rotate_rect, (POINT){x, y})) { *out_btn = _VIV_COMPARE_BTN_ROTATE; return 0; } + if (PtInRect(&pane->use_current_rect, (POINT){x, y})) { *out_btn = _VIV_COMPARE_BTN_USE_CURRENT; return 0; } if (PtInRect(&pane->btn_select_rect, (POINT){x, y})) { *out_btn = _VIV_COMPARE_BTN_SELECT; return 0; } return 0; } @@ -14075,6 +14081,7 @@ static void _viv_compare_pick_image_for_pane(HWND hwnd, _viv_compare_pane_t *pan if (GetOpenFileNameW(&ofn)) { + pane->use_current = 0; _viv_compare_load_image(filename, pane); pane->playlist_item = NULL; InvalidateRect(hwnd, NULL, FALSE); @@ -14137,6 +14144,44 @@ static void _viv_compare_draw_checkbox(HDC hdc, const RECT *rect, const wchar_t DeleteObject(br); } +static void _viv_compare_draw_checkbox_ex(HDC hdc, const RECT *rect, const wchar_t *text, int is_checked, int is_hovered, int is_disabled, HFONT font) +{ + HGDIOBJ old_font = SelectObject(hdc, font); + RECT box_rect = {rect->left, rect->top + (rect->bottom - rect->top - 14) / 2, rect->left + 14, rect->top + (rect->bottom - rect->top - 14) / 2 + 14}; + RECT text_rect = {rect->left + 18, rect->top, rect->right, rect->bottom}; + COLORREF box_bg = is_disabled ? RGB(230, 230, 230) : RGB(255, 255, 255); + COLORREF border_col = is_disabled ? RGB(180, 180, 180) : (is_hovered ? RGB(50, 120, 220) : RGB(140, 140, 140)); + COLORREF text_col = is_disabled ? RGB(150, 150, 150) : RGB(30, 30, 30); + + HBRUSH br = CreateSolidBrush(box_bg); + HPEN pen = CreatePen(PS_SOLID, 1, border_col); + HGDIOBJ old_br = SelectObject(hdc, br); + HGDIOBJ old_pen = SelectObject(hdc, pen); + + Rectangle(hdc, box_rect.left, box_rect.top, box_rect.right, box_rect.bottom); + + if (is_checked && !is_disabled) + { + HPEN check_pen = CreatePen(PS_SOLID, 2, RGB(20, 120, 20)); + HGDIOBJ old_cpen = SelectObject(hdc, check_pen); + MoveToEx(hdc, box_rect.left + 3, box_rect.top + 7, NULL); + LineTo(hdc, box_rect.left + 6, box_rect.top + 10); + LineTo(hdc, box_rect.left + 11, box_rect.top + 3); + SelectObject(hdc, old_cpen); + DeleteObject(check_pen); + } + + SetBkMode(hdc, TRANSPARENT); + SetTextColor(hdc, text_col); + DrawTextW(hdc, text, -1, &text_rect, DT_LEFT | DT_VCENTER | DT_SINGLELINE); + + SelectObject(hdc, old_pen); + SelectObject(hdc, old_br); + SelectObject(hdc, old_font); + DeleteObject(pen); + DeleteObject(br); +} + static void _viv_compare_paint(HWND hwnd, _viv_compare_state_t *cs) { PAINTSTRUCT ps; @@ -14250,6 +14295,28 @@ static void _viv_compare_paint(HWND hwnd, _viv_compare_state_t *cs) _viv_compare_draw_button(mem_hdc, &pane->btn_1to1_rect, L"1:1", (cs->hovered_pane == i && cs->hovered_btn == _VIV_COMPARE_BTN_1TO1), cs->font_ui); _viv_compare_draw_button(mem_hdc, &pane->btn_fit_rect, L"Fit", (cs->hovered_pane == i && cs->hovered_btn == _VIV_COMPARE_BTN_FIT), cs->font_ui); _viv_compare_draw_button(mem_hdc, &pane->btn_rotate_rect, L"90°", (cs->hovered_pane == i && cs->hovered_btn == _VIV_COMPARE_BTN_ROTATE), cs->font_ui); + + // Use Current Image checkbox — greyed out if another pane already uses current + { + int other_uses_current = 0; + int k; + wchar_t uc_label[STRING_SIZE]; + for (k = 0; k < cs->pane_count; k++) + { + if (k != i && cs->panes[k].use_current) + { + other_uses_current = 1; + break; + } + } + string_copy_utf8_string(uc_label, localization_get_string(LOCALIZATION_ID_COMPARE_USE_CURRENT)); + _viv_compare_draw_checkbox_ex(mem_hdc, &pane->use_current_rect, uc_label, + pane->use_current, + (!other_uses_current) && (cs->hovered_pane == i && cs->hovered_btn == _VIV_COMPARE_BTN_USE_CURRENT), + other_uses_current, /* disabled */ + cs->font_ui); + } + _viv_compare_draw_button(mem_hdc, &pane->btn_select_rect, L"...", (cs->hovered_pane == i && cs->hovered_btn == _VIV_COMPARE_BTN_SELECT), cs->font_bold); } @@ -14633,6 +14700,38 @@ static LRESULT CALLBACK _viv_compare_proc(HWND hwnd, UINT msg, WPARAM wParam, LP } return 0; } + else if (btn_idx == _VIV_COMPARE_BTN_USE_CURRENT) + { + // Only allow if no other pane already uses current + int other_uses = 0; + int k; + for (k = 0; k < cs->pane_count; k++) + { + if (k != pane_idx && cs->panes[k].use_current) + { + other_uses = 1; + break; + } + } + if (!other_uses) + { + p->use_current = !p->use_current; + if (p->use_current && *_viv_current_fd->cFileName) + { + _viv_playlist_t *cur_item = _viv_playlist_from_fd(_viv_current_fd); + p->playlist_item = cur_item; + _viv_compare_load_image(_viv_current_fd->cFileName, p); + } + else if (!p->use_current) + { + _viv_compare_free_pane_image(p); + p->playlist_item = NULL; + p->filename[0] = 0; + } + InvalidateRect(hwnd, NULL, FALSE); + } + return 0; + } else if (btn_idx == _VIV_COMPARE_BTN_SELECT) { _viv_compare_pick_image_for_pane(hwnd, p); From 7568c2e683ad5732c8e4253a00854bf40eb2b3a6 Mon Sep 17 00:00:00 2001 From: Syahda Fahreza Date: Sat, 29 Aug 2026 22:36:43 +0700 Subject: [PATCH 11/15] Add WebP support to compare pane Add WebP decoding fallback and alpha/orientation handling for the compare pane. Introduces _viv_compare_webp_ctx_t, _viv_compare_webp_info_proc and _viv_compare_webp_frame_proc to decode WebP frames, convert RGBA->BGR, composite alpha against the configured window background, create an HBITMAP and set pane properties. Update GDI+ compositing to use SourceOver when images have alpha (based on image flags) and tighten the orientation check to >1. If GDI+ image loading fails, call webp_load(stream, ...) as a fallback. --- src/viv.c | 173 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 171 insertions(+), 2 deletions(-) diff --git a/src/viv.c b/src/viv.c index a59d82a9..4c880618 100644 --- a/src/viv.c +++ b/src/viv.c @@ -13715,6 +13715,138 @@ static void _viv_compare_free_pane_image(_viv_compare_pane_t *pane) pane->filesize = 0; } +typedef struct { + _viv_compare_pane_t *pane; + DWORD wide; + DWORD high; + int has_alpha; + int orientation; +} _viv_compare_webp_ctx_t; + +static int _viv_compare_webp_info_proc(void *user_data, DWORD frame_count, DWORD wide, DWORD high, int has_alpha) +{ + _viv_compare_webp_ctx_t *ctx = (_viv_compare_webp_ctx_t *)user_data; + ctx->wide = wide; + ctx->high = high; + ctx->has_alpha = has_alpha; + return 1; +} + +static int _viv_compare_webp_frame_proc(void *user_data, BYTE *pixels, int delay) +{ + _viv_compare_webp_ctx_t *ctx = (_viv_compare_webp_ctx_t *)user_data; + _viv_compare_pane_t *pane = ctx->pane; + HDC screen_hdc; + + if (pane->hbitmap) + { + return 0; + } + + if (ctx->has_alpha) + { + BYTE *p = pixels; + DWORD run = ctx->wide * ctx->high; + while (run--) + { + int a = p[3]; + if (a != 255) + { + int r = p[0], g = p[1], b = p[2]; + p[0] = config_windowed_background_color_r + ((r - config_windowed_background_color_r) * a) / 255; + p[1] = config_windowed_background_color_g + ((g - config_windowed_background_color_g) * a) / 255; + p[2] = config_windowed_background_color_b + ((b - config_windowed_background_color_b) * a) / 255; + p[3] = 255; + } + p += 4; + } + } + + // Convert RGBA to BGR + { + const BYTE *p = pixels; + BYTE *d = pixels; + DWORD high_run = ctx->high; + + while (high_run) + { + BYTE *wd = d; + DWORD wide_run = ctx->wide; + while (wide_run) + { + int r = p[0], g = p[1], b = p[2]; + p += 4; + wd[0] = b; + wd[1] = g; + wd[2] = r; + wd += 3; + wide_run--; + } + d += (((ctx->wide * 3) + 3) / 4) * 4; + high_run--; + } + } + + screen_hdc = GetDC(NULL); + if (screen_hdc) + { + HDC mem_hdc = CreateCompatibleDC(screen_hdc); + if (mem_hdc) + { + HBITMAP hbitmap = _viv_create_bitmap(screen_hdc, ctx->wide, ctx->high); + if (hbitmap) + { + BITMAPINFO bmi; + ZeroMemory(&bmi, sizeof(BITMAPINFO)); + bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); + bmi.bmiHeader.biWidth = ctx->wide; + bmi.bmiHeader.biHeight = -(int)ctx->high; + bmi.bmiHeader.biPlanes = 1; + bmi.bmiHeader.biBitCount = 24; + bmi.bmiHeader.biCompression = BI_RGB; + + SetDIBits(mem_hdc, hbitmap, 0, ctx->high, pixels, &bmi, DIB_RGB_COLORS); + + if (ctx->orientation > 1) + { + HBITMAP oriented = _viv_orientate_hbitmap(hbitmap, ctx->orientation); + if (oriented) + { + DeleteObject(hbitmap); + hbitmap = oriented; + + switch (ctx->orientation) + { + case 5: + case 6: + case 7: + case 8: + { + DWORD tmp = ctx->wide; + ctx->wide = ctx->high; + ctx->high = tmp; + break; + } + } + } + } + + pane->hbitmap = hbitmap; + pane->image_wide = ctx->wide; + pane->image_high = ctx->high; + pane->zoom_percent = 100.0; + pane->is_autofit = 1; + pane->view_x = 0; + pane->view_y = 0; + } + DeleteDC(mem_hdc); + } + ReleaseDC(NULL, screen_hdc); + } + + return 1; +} + static BOOL _viv_compare_load_image(const wchar_t *filename, _viv_compare_pane_t *pane) { HANDLE h; @@ -13847,7 +13979,32 @@ static BOOL _viv_compare_load_image(const wchar_t *filename, _viv_compare_pane_t void *g = NULL; if (os_GdipCreateFromHDC(mem_hdc, &g) == 0 && g) { - os_GdipSetCompositingMode(g, 1); + UINT image_flags = 0; + + if (os_GdipImageSelectActiveFrame) + { + os_GdipImageSelectActiveFrame(image, &_viv_FrameDimensionTime, 0); + } + + if (os_GdipGetImageFlags) + { + os_GdipGetImageFlags(image, &image_flags); + } + + // If has alpha, fill background with window background color and use SourceOver + if (image_flags & 2) + { + RECT rc = {0, 0, (int)raw_wide, (int)raw_high}; + HBRUSH bg_br = CreateSolidBrush(RGB(config_windowed_background_color_r, config_windowed_background_color_g, config_windowed_background_color_b)); + FillRect(mem_hdc, &rc, bg_br); + DeleteObject(bg_br); + os_GdipSetCompositingMode(g, 0); // CompositingModeSourceOver + } + else + { + os_GdipSetCompositingMode(g, 1); // CompositingModeSourceCopy + } + os_GdipSetCompositingQuality(g, 1); os_GdipSetInterpolationMode(g, 5); os_GdipSetPixelOffsetMode(g, 3); @@ -13857,7 +14014,7 @@ static BOOL _viv_compare_load_image(const wchar_t *filename, _viv_compare_pane_t SelectObject(mem_hdc, last_hbitmap); - if (pane->orientation) + if (pane->orientation > 1) { HBITMAP oriented = _viv_orientate_hbitmap(hbitmap, pane->orientation); if (oriented) @@ -13902,6 +14059,18 @@ static BOOL _viv_compare_load_image(const wchar_t *filename, _viv_compare_pane_t } os_GdipDisposeImage(image); } + else + { + // Fallback to WebP loader + _viv_compare_webp_ctx_t ctx; + ctx.pane = pane; + ctx.wide = 0; + ctx.high = 0; + ctx.has_alpha = 0; + ctx.orientation = pane->orientation; + + webp_load(stream, &ctx, _viv_compare_webp_info_proc, _viv_compare_webp_frame_proc); + } IStream_Release(stream); return (pane->hbitmap != NULL); From 092a5dcc13f5b72a875e83c0dc057c1e40b44b05 Mon Sep 17 00:00:00 2001 From: Syahda Fahreza Date: Sat, 29 Aug 2026 22:48:57 +0700 Subject: [PATCH 12/15] Enable drag-and-drop for compare panes Add drag-and-drop support for the compare window and a localized placeholder string. Introduce LOCALIZATION_ID_COMPARE_NO_IMAGE_DROP and provide English/Indonesian/Chinese translations. Use the localized text when no image is present. Enable file drops via WS_EX_ACCEPTFILES/DragAcceptFiles and implement WM_DROPFILES handling to accept one or two dropped files, load them into the appropriate compare panes, manage use_current/playlist state, and call DragFinish. Minor UI wiring in _viv_compare_proc and _viv_compare_show to accept and process dropped image files. --- src/localization.h | 1 + src/localization_en_us.h | 1 + src/localization_id_id.h | 1 + src/localization_zh_cn.h | 1 + src/viv.c | 82 +++++++++++++++++++++++++++++++++++++++- 5 files changed, 84 insertions(+), 2 deletions(-) diff --git a/src/localization.h b/src/localization.h index a633c7c1..bdbd6025 100644 --- a/src/localization.h +++ b/src/localization.h @@ -347,6 +347,7 @@ enum LOCALIZATION_ID_COMPARE_ROTATE, LOCALIZATION_ID_COMPARE_SELECT_IMAGE, LOCALIZATION_ID_COMPARE_USE_CURRENT, + LOCALIZATION_ID_COMPARE_NO_IMAGE_DROP, LOCALIZATION_ID_COUNT, }; diff --git a/src/localization_en_us.h b/src/localization_en_us.h index 6c4bd679..b31fbc18 100644 --- a/src/localization_en_us.h +++ b/src/localization_en_us.h @@ -322,4 +322,5 @@ static const utf8_t *_localization_string_array_en_us[LOCALIZATION_ID_COUNT] = "Rotate", // LOCALIZATION_ID_COMPARE_ROTATE, "Select Image", // LOCALIZATION_ID_COMPARE_SELECT_IMAGE, "Use Current Image", // LOCALIZATION_ID_COMPARE_USE_CURRENT, + "[No Image - Drop image here or click ... / Use Current]", // LOCALIZATION_ID_COMPARE_NO_IMAGE_DROP, }; diff --git a/src/localization_id_id.h b/src/localization_id_id.h index 18efdb83..6844be66 100644 --- a/src/localization_id_id.h +++ b/src/localization_id_id.h @@ -322,4 +322,5 @@ static const utf8_t *_localization_string_array_id_id[LOCALIZATION_ID_COUNT] = "Putar", // LOCALIZATION_ID_COMPARE_ROTATE, "Pilih Gambar", // LOCALIZATION_ID_COMPARE_SELECT_IMAGE, "Gunakan Gambar Saat Ini", // LOCALIZATION_ID_COMPARE_USE_CURRENT, + "[Tidak Ada Gambar - Tarik & lepas gambar ke sini atau klik ... / Gunakan Saat Ini]", // LOCALIZATION_ID_COMPARE_NO_IMAGE_DROP, }; diff --git a/src/localization_zh_cn.h b/src/localization_zh_cn.h index fbc0d98c..690a8f4f 100644 --- a/src/localization_zh_cn.h +++ b/src/localization_zh_cn.h @@ -323,4 +323,5 @@ static const utf8_t *_localization_string_array_zh_cn[LOCALIZATION_ID_COUNT] = "旋转", // LOCALIZATION_ID_COMPARE_ROTATE, "选择图像", // LOCALIZATION_ID_COMPARE_SELECT_IMAGE, "使用当前图像", // LOCALIZATION_ID_COMPARE_USE_CURRENT, + "[无图像 - 拖放图像至此处或点击 ... / 使用当前图像]", // LOCALIZATION_ID_COMPARE_NO_IMAGE_DROP, }; diff --git a/src/viv.c b/src/viv.c index 4c880618..468d4765 100644 --- a/src/viv.c +++ b/src/viv.c @@ -14588,11 +14588,13 @@ static void _viv_compare_paint(HWND hwnd, _viv_compare_state_t *cs) } else { + wchar_t no_img_str[STRING_SIZE]; HGDIOBJ old_font = SelectObject(mem_hdc, cs->font_ui); RECT center_rc = pane->viewport_rect; + string_copy_utf8_string(no_img_str, localization_get_string(LOCALIZATION_ID_COMPARE_NO_IMAGE_DROP)); SetBkMode(mem_hdc, TRANSPARENT); SetTextColor(mem_hdc, RGB(180, 180, 180)); - DrawTextW(mem_hdc, L"[No Image - Click ... to select file]", -1, ¢er_rc, DT_CENTER | DT_VCENTER | DT_SINGLELINE); + DrawTextW(mem_hdc, no_img_str, -1, ¢er_rc, DT_CENTER | DT_VCENTER | DT_SINGLELINE); SelectObject(mem_hdc, old_font); } } @@ -14625,6 +14627,7 @@ static LRESULT CALLBACK _viv_compare_proc(HWND hwnd, UINT msg, WPARAM wParam, LP switch (msg) { case WM_CREATE: + DragAcceptFiles(hwnd, TRUE); return 0; case WM_ERASEBKGND: @@ -14637,6 +14640,80 @@ static LRESULT CALLBACK _viv_compare_proc(HWND hwnd, UINT msg, WPARAM wParam, LP } return 0; + case WM_DROPFILES: + if (cs) + { + HDROP hDrop = (HDROP)wParam; + UINT count = DragQueryFileW(hDrop, 0xFFFFFFFF, NULL, 0); + POINT pt; + int target_pane = 0; + + if (DragQueryPoint(hDrop, &pt)) + { + int i; + for (i = 0; i < cs->pane_count; i++) + { + if (PtInRect(&cs->panes[i].pane_rect, pt)) + { + target_pane = i; + break; + } + } + } + + if (count >= 2) + { + wchar_t filename0[STRING_SIZE]; + wchar_t filename1[STRING_SIZE]; + + DragQueryFileW(hDrop, 0, filename0, STRING_SIZE); + DragQueryFileW(hDrop, 1, filename1, STRING_SIZE); + + cs->panes[0].use_current = 0; + cs->panes[0].playlist_item = NULL; + _viv_compare_load_image(filename0, &cs->panes[0]); + + cs->panes[1].use_current = 0; + cs->panes[1].playlist_item = NULL; + _viv_compare_load_image(filename1, &cs->panes[1]); + + InvalidateRect(hwnd, NULL, FALSE); + } + else if (count == 1) + { + wchar_t filename[STRING_SIZE]; + DragQueryFileW(hDrop, 0, filename, STRING_SIZE); + + if (target_pane >= 0 && target_pane < cs->pane_count) + { + int other_pane = (target_pane == 0) ? 1 : 0; + + // Reset use_current on target pane + cs->panes[target_pane].use_current = 0; + cs->panes[target_pane].playlist_item = NULL; + + // If the other pane was using current image and this dropped file is that same file, + // reset use_current on the other pane so they don't conflict + if (other_pane >= 0 && other_pane < cs->pane_count) + { + if (cs->panes[other_pane].use_current && *_viv_current_fd->cFileName) + { + if (lstrcmpiW(filename, _viv_current_fd->cFileName) == 0) + { + cs->panes[other_pane].use_current = 0; + } + } + } + + _viv_compare_load_image(filename, &cs->panes[target_pane]); + InvalidateRect(hwnd, NULL, FALSE); + } + } + + DragFinish(hDrop); + } + return 0; + case WM_SIZE: if (cs) { @@ -15156,7 +15233,7 @@ static void _viv_compare_show(void) GetWindowRect(_viv_hwnd, &rc); hwnd = os_CreateWindowEx( - WS_EX_APPWINDOW, + WS_EX_APPWINDOW | WS_EX_ACCEPTFILES, "_VIV_COMPARE", localization_get_string(LOCALIZATION_ID_COMPARE_IMAGES), WS_OVERLAPPEDWINDOW | WS_VISIBLE | WS_CLIPCHILDREN, @@ -15167,6 +15244,7 @@ static void _viv_compare_show(void) if (hwnd) { + DragAcceptFiles(hwnd, TRUE); ShowWindow(hwnd, SW_MAXIMIZE); EnableWindow(_viv_hwnd, FALSE); } From 533d978d12bf2de90bafec0bcec7edd120968edb Mon Sep 17 00:00:00 2001 From: Syahda Fahreza Date: Sat, 29 Aug 2026 23:00:08 +0700 Subject: [PATCH 13/15] Add side-by-side compare enhancements This commit expands the Compare Images window with drag-and-drop loading, per-pane "Use Current Image" toggles, synchronized zoom/pan controls, per-pane toolbars, and improved navigation options. It also adds WebP fallback decoding and proper alpha blending for transparent PNG/ICO/GIF images, and completes English, Indonesian, and Simplified Chinese localization for the new UI. --- Changes.txt | 3 +++ README.md | 15 ++++++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/Changes.txt b/Changes.txt index a8967ae7..4b9dd883 100644 --- a/Changes.txt +++ b/Changes.txt @@ -1,7 +1,10 @@ Saturday, 29 August 2026: Version 1.2.0.0 added Compare Images side-by-side comparison mode with synchronized/individual zoom and pan. + added drag and drop support directly onto left/right compare panes. + added "Use Current Image" toggle per pane with mutual exclusion. added mini toolbars per pane for file browsing, zooming, 1:1, fit, and rotation. added top control bar with tips, Auto-Fit, Smooth, Filename overlay, and Close. + added WebP fallback decoder and alpha blending over window background for transparent PNG/ICO/GIF images. added full multilingual localization for English, Indonesian, and Simplified Chinese. Monday, 12 January 2026: Version 1.0.0.15 diff --git a/README.md b/README.md index eb41966d..238a53e6 100644 --- a/README.md +++ b/README.md @@ -50,21 +50,30 @@ void Image Viewer Controls Options: ### Version 1.2.0.0 (Latest) - **New Feature: Compare Images (Side-by-Side Image Comparison)** - Added multi-pane **Compare Images** window accessible via `View` -> `Compare Images...` or shortcut (`Ctrl+Alt+C`). - - **Synchronized Navigation & Zoom:** - - Mouse wheel zooms all compared images simultaneously using the exact same zoom engine (`_viv_calculate_next_zoom`). + - **Drag & Drop Support:** + - Directly drag & drop image files from File Explorer to either the left or right pane to compare. + - Dropping 2 files simultaneously automatically loads them into the left and right panes. + - **"Use Current Image" Toggle:** + - Convenient checkbox on each pane to instantly load the image currently open in the main viewer. + - Automatically disables on the other pane when active to avoid duplicate/conflicting states. + - **Synchronized Navigation & Precise Zoom:** + - Mouse wheel zooms all compared images simultaneously using the exact same zoom engine (`_viv_calculate_next_zoom`) with synchronized viewport centers to eliminate drift. - Left-click drag pans all compared images synchronously. - **Individual Pane Mode:** Hold `Ctrl` key or use **Right Mouse Button** to zoom or pan only the hovered image pane. - **Per-Pane Mini Toolbar:** - Previous (`<`) and Next (`>`) buttons to browse folder/playlist images per pane. - Zoom In (`+`), Zoom Out (`-`), Zoom % display, `1:1` (Actual size), and `Fit` (Best Fit) buttons. - Rotate clockwise button (`90°`). - - Change/Select image file button (`X`). + - Change/Select image file button (`...`). - **Top Control Bar:** - Helpful shortcut and mouse tips. - **Auto-Fit** toggle to automatically scale images to fit pane viewports. - **Smooth** toggle (Halftone / High-quality interpolation). - **Filename** toggle with bright green overlay text (`filename`, `dimensions`, `file size`). - **Close** button (or `ESC` key) to quickly return to main viewer. + - **Full Format Support & Alpha Transparency:** + - Native support for PNG (with alpha transparency), GIF, animated GIF, BMP, ICO, TIFF, JPEG, and WebP (via `libwebp` fallback decoder). + - Clean alpha blending over the window background color, preventing black background issues on transparent images. - **Full Multilingual Localization:** Added translations for all compare UI elements in English (`en-US`), Indonesian (`id-ID`), and Simplified Chinese (`zh-CN`). ### Version 1.1.0.0 From 493b71bf94dbcd0f83db4999156f5dc591bb4851 Mon Sep 17 00:00:00 2001 From: Syahda Fahreza Date: Sun, 30 Aug 2026 21:57:04 +0700 Subject: [PATCH 14/15] Bump version and add Smooth view option Bump product/version to 1.2.1.0 (nsis/version.nsh, res/voidImageViewer*.rc, src/version.h). Add a new LOCALIZATION_ID_SMOOTH and translations (src/localization.h, localization_en_us.h, localization_id_id.h, localization_zh_cn.h). Introduce VIV_ID_VIEW_SMOOTH and wire it into menus and context handling (src/viv.h, src/viv.c): menu entry, context menu id, toggle command that switches shrink blit mode and magnification filter, and menu check state. This provides a user-toggleable smoothing option for image scaling. --- nsis/version.nsh | 2 +- res/voidImageViewer.rc | 8 ++++---- res/voidImageViewer_utf8.rc | 8 ++++---- src/localization.h | 2 ++ src/localization_en_us.h | 1 + src/localization_id_id.h | 1 + src/localization_zh_cn.h | 1 + src/version.h | 2 +- src/viv.c | 9 +++++++++ src/viv.h | 1 + 10 files changed, 25 insertions(+), 10 deletions(-) diff --git a/nsis/version.nsh b/nsis/version.nsh index bd29c9bd..75f01347 100644 --- a/nsis/version.nsh +++ b/nsis/version.nsh @@ -1,4 +1,4 @@ -!define VERSION "1.2.0.0" +!define VERSION "1.2.1.0" !define BETAVERSION "" !define VERSIONYEAR "2026" diff --git a/res/voidImageViewer.rc b/res/voidImageViewer.rc index bd47a318..1c6b57ee 100644 --- a/res/voidImageViewer.rc +++ b/res/voidImageViewer.rc @@ -228,8 +228,8 @@ IDI_BESTFIT ICON "bestfit.ico" // VS_VERSION_INFO VERSIONINFO - FILEVERSION 1,2,0,0 - PRODUCTVERSION 1,2,0,0 + FILEVERSION 1,2,1,0 + PRODUCTVERSION 1,2,1,0 FILEFLAGSMASK 0x17L #ifdef _DEBUG FILEFLAGS 0x1L @@ -245,12 +245,12 @@ BEGIN BLOCK "040904b0" BEGIN VALUE "FileDescription", "voidImageViewer" - VALUE "FileVersion", "1.2.0.0" + VALUE "FileVersion", "1.2.1.0" VALUE "InternalName", "voidImageViewer" VALUE "LegalCopyright", "Copyright © 2026 voidtools" VALUE "OriginalFilename", "voidImageViewer.exe" VALUE "ProductName", "voidImageViewer" - VALUE "ProductVersion", "1.2.0.0" + VALUE "ProductVersion", "1.2.1.0" END END BLOCK "VarFileInfo" diff --git a/res/voidImageViewer_utf8.rc b/res/voidImageViewer_utf8.rc index e604b875..b7657fca 100644 --- a/res/voidImageViewer_utf8.rc +++ b/res/voidImageViewer_utf8.rc @@ -228,8 +228,8 @@ IDI_BESTFIT ICON "bestfit.ico" // VS_VERSION_INFO VERSIONINFO - FILEVERSION 1,2,0,0 - PRODUCTVERSION 1,2,0,0 + FILEVERSION 1,2,1,0 + PRODUCTVERSION 1,2,1,0 FILEFLAGSMASK 0x17L #ifdef _DEBUG FILEFLAGS 0x1L @@ -245,12 +245,12 @@ BEGIN BLOCK "040904b0" BEGIN VALUE "FileDescription", "voidImageViewer" - VALUE "FileVersion", "1.2.0.0" + VALUE "FileVersion", "1.2.1.0" VALUE "InternalName", "voidImageViewer" VALUE "LegalCopyright", "Copyright © 2026 voidtools" VALUE "OriginalFilename", "voidImageViewer.exe" VALUE "ProductName", "voidImageViewer" - VALUE "ProductVersion", "1.2.0.0" + VALUE "ProductVersion", "1.2.1.0" END END BLOCK "VarFileInfo" diff --git a/src/localization.h b/src/localization.h index bdbd6025..72c2c512 100644 --- a/src/localization.h +++ b/src/localization.h @@ -349,6 +349,8 @@ enum LOCALIZATION_ID_COMPARE_USE_CURRENT, LOCALIZATION_ID_COMPARE_NO_IMAGE_DROP, + LOCALIZATION_ID_SMOOTH, + LOCALIZATION_ID_COUNT, }; diff --git a/src/localization_en_us.h b/src/localization_en_us.h index b31fbc18..fa7d0cbf 100644 --- a/src/localization_en_us.h +++ b/src/localization_en_us.h @@ -323,4 +323,5 @@ static const utf8_t *_localization_string_array_en_us[LOCALIZATION_ID_COUNT] = "Select Image", // LOCALIZATION_ID_COMPARE_SELECT_IMAGE, "Use Current Image", // LOCALIZATION_ID_COMPARE_USE_CURRENT, "[No Image - Drop image here or click ... / Use Current]", // LOCALIZATION_ID_COMPARE_NO_IMAGE_DROP, + "&Smooth", // LOCALIZATION_ID_SMOOTH, }; diff --git a/src/localization_id_id.h b/src/localization_id_id.h index 6844be66..e3dded6f 100644 --- a/src/localization_id_id.h +++ b/src/localization_id_id.h @@ -323,4 +323,5 @@ static const utf8_t *_localization_string_array_id_id[LOCALIZATION_ID_COUNT] = "Pilih Gambar", // LOCALIZATION_ID_COMPARE_SELECT_IMAGE, "Gunakan Gambar Saat Ini", // LOCALIZATION_ID_COMPARE_USE_CURRENT, "[Tidak Ada Gambar - Tarik & lepas gambar ke sini atau klik ... / Gunakan Saat Ini]", // LOCALIZATION_ID_COMPARE_NO_IMAGE_DROP, + "&Halus", // LOCALIZATION_ID_SMOOTH, }; diff --git a/src/localization_zh_cn.h b/src/localization_zh_cn.h index 690a8f4f..7f1e5531 100644 --- a/src/localization_zh_cn.h +++ b/src/localization_zh_cn.h @@ -324,4 +324,5 @@ static const utf8_t *_localization_string_array_zh_cn[LOCALIZATION_ID_COUNT] = "选择图像", // LOCALIZATION_ID_COMPARE_SELECT_IMAGE, "使用当前图像", // LOCALIZATION_ID_COMPARE_USE_CURRENT, "[无图像 - 拖放图像至此处或点击 ... / 使用当前图像]", // LOCALIZATION_ID_COMPARE_NO_IMAGE_DROP, + "平滑(&S)", // LOCALIZATION_ID_SMOOTH, }; diff --git a/src/version.h b/src/version.h index 6024029c..c96d09cf 100644 --- a/src/version.h +++ b/src/version.h @@ -1,7 +1,7 @@ #define VERSION_YEAR 2026 #define VERSION_MAJOR 1 #define VERSION_MINOR 2 -#define VERSION_REVISION 0 +#define VERSION_REVISION 1 #define VERSION_BUILD 0 #define VERSION_TYPE "" diff --git a/src/viv.c b/src/viv.c index 468d4765..e70970ad 100644 --- a/src/viv.c +++ b/src/viv.c @@ -868,6 +868,7 @@ static _viv_command_t _viv_commands[] = {LOCALIZATION_ID_ALLOW_SHRINKING,MF_STRING,_VIV_MENU_VIEW,VIV_ID_VIEW_ALLOW_SHRINKING}, {LOCALIZATION_ID_KEEP_ASPECT_RATIO,MF_STRING,_VIV_MENU_VIEW,VIV_ID_VIEW_KEEP_ASPECT_RATIO}, {LOCALIZATION_ID_FILL_WINDOW,MF_STRING,_VIV_MENU_VIEW,VIV_ID_VIEW_FILL_WINDOW}, + {LOCALIZATION_ID_SMOOTH,MF_STRING,_VIV_MENU_VIEW,VIV_ID_VIEW_SMOOTH}, {LOCALIZATION_ID_ONE_TO_ONE,MF_STRING,_VIV_MENU_VIEW,VIV_ID_VIEW_1TO1}, {LOCALIZATION_ID_BEST_FIT,MF_STRING|MF_OWNERDRAW,_VIV_MENU_VIEW,VIV_ID_VIEW_BESTFIT}, {LOCALIZATION_ID_INVALID,MF_SEPARATOR,_VIV_MENU_VIEW,0}, @@ -1094,6 +1095,7 @@ WORD _viv_context_menu_items[] = VIV_ID_VIEW_ALLOW_SHRINKING, VIV_ID_VIEW_KEEP_ASPECT_RATIO, VIV_ID_VIEW_FILL_WINDOW, + VIV_ID_VIEW_SMOOTH, VIV_ID_VIEW_1TO1, // VIV_ID_VIEW_SLIDESHOW, 0, @@ -2066,6 +2068,12 @@ static void _viv_command_with_is_key_repeat(int command_id,int is_key_repeat) InvalidateRect(_viv_hwnd,0,FALSE); break; + case VIV_ID_VIEW_SMOOTH: + config_shrink_blit_mode = (config_shrink_blit_mode == CONFIG_SHRINK_BLIT_MODE_HALFTONE) ? CONFIG_SHRINK_BLIT_MODE_COLORONCOLOR : CONFIG_SHRINK_BLIT_MODE_HALFTONE; + config_mag_filter = (config_shrink_blit_mode == CONFIG_SHRINK_BLIT_MODE_HALFTONE) ? CONFIG_MAG_FILTER_HALFTONE : CONFIG_MAG_FILTER_COLORONCOLOR; + InvalidateRect(_viv_hwnd,0,FALSE); + break; + case VIV_ID_VIEW_FULLSCREEN: _viv_toggle_fullscreen(); @@ -7152,6 +7160,7 @@ static void _viv_check_menus(HMENU hmenu) CheckMenuItem(hmenu,VIV_ID_VIEW_ALLOW_SHRINKING,config_allow_shrinking ? MF_CHECKED : MF_UNCHECKED); CheckMenuItem(hmenu,VIV_ID_VIEW_KEEP_ASPECT_RATIO,config_keep_aspect_ratio ? MF_CHECKED : MF_UNCHECKED); CheckMenuItem(hmenu,VIV_ID_VIEW_FILL_WINDOW,fill_window ? MF_CHECKED : MF_UNCHECKED); + CheckMenuItem(hmenu,VIV_ID_VIEW_SMOOTH,(config_shrink_blit_mode == CONFIG_SHRINK_BLIT_MODE_HALFTONE) ? MF_CHECKED : MF_UNCHECKED); CheckMenuItem(hmenu,VIV_ID_VIEW_1TO1,((rw == _viv_image_wide) && (rh == _viv_image_high)) ? MF_CHECKED : MF_UNCHECKED); CheckMenuItem(hmenu,VIV_ID_VIEW_FULLSCREEN,_viv_is_fullscreen ? MF_CHECKED : MF_UNCHECKED); CheckMenuItem(hmenu,VIV_ID_VIEW_SLIDESHOW,is_slideshow ? MF_CHECKED : MF_UNCHECKED); diff --git a/src/viv.h b/src/viv.h index e1662d12..89801043 100644 --- a/src/viv.h +++ b/src/viv.h @@ -109,6 +109,7 @@ enum VIV_ID_VIEW_ALLOW_SHRINKING, VIV_ID_VIEW_KEEP_ASPECT_RATIO, VIV_ID_VIEW_FILL_WINDOW, + VIV_ID_VIEW_SMOOTH, VIV_ID_VIEW_1TO1, VIV_ID_VIEW_BESTFIT, VIV_ID_VIEW_FULLSCREEN, From c7562c33fe7e477fd7d3fe03b37da943d309879b Mon Sep 17 00:00:00 2001 From: Syahda Fahreza Date: Sun, 30 Aug 2026 21:59:40 +0700 Subject: [PATCH 15/15] Add smooth toggle and compare mode This release adds a Smooth (Halftone/Bilinear) toggle to the main viewer's View menu and context menu, with configuration persistence and compare-mode integration. It also introduces a side-by-side Compare Images mode with synchronized or independent zoom/pan, drag-and-drop support, and per-pane 'Use Current Image' toggles. --- Changes.txt | 3 ++- README.md | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/Changes.txt b/Changes.txt index 4b9dd883..654adff5 100644 --- a/Changes.txt +++ b/Changes.txt @@ -1,4 +1,5 @@ -Saturday, 29 August 2026: Version 1.2.0.0 +Saturday, 29 August 2026: Version 1.2.1.0 + added Smooth (Halftone) toggle to main viewer View menu and context menu. added Compare Images side-by-side comparison mode with synchronized/individual zoom and pan. added drag and drop support directly onto left/right compare panes. added "Use Current Image" toggle per pane with mutual exclusion. diff --git a/README.md b/README.md index 238a53e6..c1f2f2b5 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,10 @@ void Image Viewer Controls Options: ## Changelog -### Version 1.2.0.0 (Latest) +### Version 1.2.1.0 (Latest) +- **New Feature: Smooth Toggle in Main Viewer** + - Added **Smooth** (Halftone / Bilinear interpolation) toggle directly in the `View` menu and right-click context menu. + - Linked with configuration persistence (`shrink_blit_mode` / `mag_filter`), and seamlessly integrated with Compare Images initial smoothing state. - **New Feature: Compare Images (Side-by-Side Image Comparison)** - Added multi-pane **Compare Images** window accessible via `View` -> `Compare Images...` or shortcut (`Ctrl+Alt+C`). - **Drag & Drop Support:**