7zip/CPP/7zip/UI/FileManager/LangUtils.cpp
Fernando Nillsson Cidade 5a4388f2b8 v2026.3.21.0
**Arquivos-Chave**
[CompressDialog.cpp](c:/Users/ferna/DEV/CPP/7zip/CPP/7zip/UI/GUI/CompressDialog.cpp),
[CompressDialog.h](c:/Users/ferna/DEV/CPP/7zip/CPP/7zip/UI/GUI/CompressDialog.h),
[CompressDialog.rc](c:/Users/ferna/DEV/CPP/7zip/CPP/7zip/UI/GUI/CompressDialog.rc),
[UpdateGUI.cpp](c:/Users/ferna/DEV/CPP/7zip/CPP/7zip/UI/GUI/UpdateGUI.cpp),
[Update.h](c:/Users/ferna/DEV/CPP/7zip/CPP/7zip/UI/Common/Update.h),
[LangUtils.cpp](c:/Users/ferna/DEV/CPP/7zip/CPP/7zip/UI/FileManager/LangUtils.cpp),
[Build.mak](c:/Users/ferna/DEV/CPP/7zip/CPP/Build.mak),
[makefile](c:/Users/ferna/DEV/CPP/7zip/CPP/7zip/Bundles/Fm/makefile),
[7zipUninstall.c](c:/Users/ferna/DEV/CPP/7zip/C/Util/7zipUninstall/7zipUninstall.c),
[7zipInstall.c](c:/Users/ferna/DEV/CPP/7zip/C/Util/7zipInstall/7zipInstall.c)

**English**
Suggested title: `Support multiple output archives, per-item packaging flow, and packaging/uninstall hardening`

This PR extends the fork’s compression flow to support multiple output archives and, when the operation starts from multiple selected items, to execute per-item packaging with isolated destinations. The change is carried from the UI down to the options/execution layer, with safe initialization of the new state so the legacy behavior remains intact when the feature is not used.

- The data model was extended to carry multiple destinations and item→archive mappings through `ArcPaths`, `SeparateItemMode`, `SeparateItemPaths`, `SeparateItemArchivePaths`, `ItemPaths`, `ItemOutputItemPaths`, and `ItemArcPaths`.
- The compression dialog was reworked to accept up to 5 output paths, with an output-count selector, extra combo boxes, dynamic layout resizing, and per-item groups when multiple items are selected.
- Archive name and extension handling was hardened: when the format or SFX state changes, the code removes the previous extension before appending the new one, preventing outputs such as `.7z.zip` or `.exe.exe`, while preserving `KeepName` formats and hash-based handlers.
- Dialog finalization now validates empty and duplicate paths, normalizes final output paths, and blocks unsafe combinations with `Delete after compression`, especially when the same source item could feed more than one output.
- The update pipeline no longer assumes a single `CUpdateArchiveCommand`. `UpdateGUI` now accepts multiple targets, recomputes `WorkingDir` per archive path, and, in separate-item mode, calls `UpdateArchive()` once per item/destination so failures stay isolated and the item-to-output mapping remains explicit.
- Input items are now collected from the `censor` with explicit deduplication, keeping the actual UI selection aligned with the execution layer.
- Language lookup and packaging were adapted for redistributed layouts: `LangUtils.cpp` now searches parent directories for `Lang`, while the File Manager bundle makefile copies language files into the relevant output folders.
- The makefiles were hardened with explicit dependencies for the new resources, and `Build.mak` stops passing `$**` to `rc`, using only the target `.rc` file as the actual resource compiler input.
- The uninstaller was strengthened for real Windows shell scenarios: it removes stale shell-extension CLSIDs first, tries to unload `7-zip.dll` and `7-zip32.dll` from `explorer.exe`, clears `read-only` attributes, removes `.tmp*` variants, schedules locked files for deletion after reboot, notifies shell association changes, and self-elevates with `runas`.
- The installer now writes fork-specific uninstall metadata (`DisplayName`, `DisplayVersion`, `Publisher`), making it easier to distinguish the fork from the official build in Windows “Programs and Features”.

Upstream note: the code already contains groundwork for a text-based output-path editor, but the corresponding UI entry point is currently hidden; the visible behavior today is the inline multi-output/per-item layout. The current `AboutDialog.*` files do not show a semantic diff and look like line-ending-only changes, so I would keep them out of the functional PR.

Se quiser, eu também posso transformar isso no formato exato de corpo de PR do GitHub, com seções como `Summary`, `Motivation`, `Implementation Details`, `Risks` e `Validation`, já pronto para colar.

**Português**
Título sugerido: `Support multiple output archives, per-item packaging flow, and packaging/uninstall hardening`

Este PR amplia o fluxo de compressão do fork para suportar múltiplos arquivos de saída e, quando a operação parte de múltiplos itens selecionados, executar um empacotamento item a item com isolamento de destino. A mudança foi propagada da UI até a camada de opções/execução, com inicialização segura dos novos estados para preservar o comportamento legado quando o recurso não é utilizado.

- A estrutura de dados foi estendida para transportar múltiplos destinos e mapeamentos item→arquivo com `ArcPaths`, `SeparateItemMode`, `SeparateItemPaths`, `SeparateItemArchivePaths`, `ItemPaths`, `ItemOutputItemPaths` e `ItemArcPaths`.
- A janela de compressão foi reestruturada para aceitar até 5 caminhos de saída, com contador de destinos, combos adicionais, redimensionamento dinâmico do layout e grupos por item quando há multi-seleção.
- O tratamento de nomes e extensões foi endurecido: ao trocar formato ou SFX, o código remove a extensão anterior antes de anexar a nova, evitando resultados como `.7z.zip` ou `.exe.exe`, sem quebrar formatos `KeepName` ou handlers baseados em hash.
- O fechamento do diálogo agora valida caminhos vazios e duplicados, normaliza caminhos finais e bloqueia combinações inseguras com `Delete after compression`, especialmente quando o mesmo item poderia alimentar múltiplos outputs.
- O pipeline de update deixou de assumir um único `CUpdateArchiveCommand`. O `UpdateGUI` agora aceita vários destinos, recompõe `WorkingDir` por arquivo de saída e, em modo separado, executa `UpdateArchive()` individualmente por item/destino para isolar erros e preservar o vínculo entre item selecionado e arquivo gerado.
- A coleta dos itens de entrada passou a ser feita a partir do `censor`, com deduplicação explícita, garantindo consistência entre a seleção real da UI e a execução.
- O suporte a idiomas e empacotamento foi ajustado para layouts redistribuídos: `LangUtils.cpp` agora procura a pasta `Lang` subindo a árvore de diretórios, enquanto o bundle do File Manager copia os arquivos de idioma para os diretórios de saída relevantes.
- Os makefiles foram fortalecidos com dependências explícitas dos novos recursos, e `Build.mak` deixou de passar `$**` ao `rc`, usando apenas o `.rc` alvo como entrada efetiva do compilador de recursos.
- O desinstalador foi reforçado para cenários reais do Windows: remoção prévia de CLSIDs da shell extension, tentativa de descarregar `7-zip.dll` e `7-zip32.dll` do `explorer.exe`, limpeza de arquivos `read-only`, remoção de variantes `.tmp*`, agendamento para exclusão após reboot quando o arquivo está travado, notificação de alteração de associações e autoelevação com `runas`.
- O instalador também passou a gravar metadados próprios do fork no registro de desinstalação (`DisplayName`, `DisplayVersion`, `Publisher`), facilitando a distinção entre o fork e a build oficial no painel “Programs and Features”.

Nota para a equipe upstream: a infraestrutura de um editor textual de caminhos de saída já existe no código, mas a entrada visual correspondente permanece oculta; hoje, o comportamento efetivamente exposto é o layout inline com múltiplos destinos e o fluxo por item em multi-seleção. Os arquivos `AboutDialog.*` não exibem diff semântico no estado atual e parecem ser apenas normalização de fim de linha, então eu os separaria da PR funcional.
2026-03-21 18:17:58 -03:00

371 lines
7.7 KiB
C++

// LangUtils.cpp
#include "StdAfx.h"
#include "../../../Common/Lang.h"
#include "../../../Windows/DLL.h"
#include "../../../Windows/FileFind.h"
#include "../../../Windows/FileName.h"
#include "../../../Windows/Synchronization.h"
#include "../../../Windows/Window.h"
#include "LangUtils.h"
#include "RegistryUtils.h"
using namespace NWindows;
#ifndef _UNICODE
extern bool g_IsNT;
#endif
UString g_LangID;
// static
CLang g_Lang;
static bool g_Loaded = false;
static NSynchronization::CCriticalSection g_CriticalSection;
bool LangOpen(CLang &lang, CFSTR fileName);
bool LangOpen(CLang &lang, CFSTR fileName)
{
return lang.Open(fileName, "7-Zip");
}
static bool GetParentDirPrefix(FString &dirPrefix)
{
if (dirPrefix.IsEmpty())
return false;
NFile::NName::NormalizeDirPathPrefix(dirPrefix);
FString parent = dirPrefix;
if (!NFile::NName::IsDriveRootPath_SuperAllowed(parent))
parent.DeleteBack();
const int pos = parent.ReverseFind_PathSepar();
if (pos < 0)
return false;
parent.DeleteFrom((unsigned)(pos + 1));
if (parent.IsEmpty() || parent == dirPrefix)
return false;
dirPrefix = parent;
return true;
}
FString GetLangDirPrefix()
{
const FString moduleDir = NDLL::GetModuleDirPrefix();
FString dirPrefix = moduleDir;
NFile::NName::NormalizeDirPathPrefix(dirPrefix);
for (;;)
{
const FString langDir = dirPrefix + FTEXT("Lang") FSTRING_PATH_SEPARATOR;
if (NFile::NFind::DoesDirExist(langDir))
return langDir;
if (!GetParentDirPrefix(dirPrefix))
break;
}
return moduleDir + FTEXT("Lang") FSTRING_PATH_SEPARATOR;
}
#ifdef Z7_LANG
void LoadLangOneTime()
{
NSynchronization::CCriticalSectionLock lock(g_CriticalSection);
if (g_Loaded)
return;
g_Loaded = true;
ReloadLang();
}
void LangSetDlgItemText(HWND dialog, UInt32 controlID, UInt32 langID)
{
const wchar_t *s = g_Lang.Get(langID);
if (s)
{
CWindow window(GetDlgItem(dialog, (int)controlID));
window.SetText(s);
}
}
#ifndef IDCONTINUE
#define IDCONTINUE 11
#endif
static const CIDLangPair kLangPairs[] =
{
{ IDOK, 401 },
{ IDCANCEL, 402 },
{ IDYES, 406 },
{ IDNO, 407 },
{ IDCLOSE, 408 },
{ IDHELP, 409 },
{ IDCONTINUE, 411 }
};
void LangSetDlgItems(HWND dialog, const UInt32 *ids, unsigned numItems)
{
unsigned i;
for (i = 0; i < Z7_ARRAY_SIZE(kLangPairs); i++)
{
const CIDLangPair &pair = kLangPairs[i];
CWindow window(GetDlgItem(dialog, (int)pair.ControlID));
if (window)
{
const wchar_t *s = g_Lang.Get(pair.LangID);
if (s)
window.SetText(s);
}
}
for (i = 0; i < numItems; i++)
{
const UInt32 id = ids[i];
LangSetDlgItemText(dialog, id, id);
}
}
void LangSetDlgItems_Colon(HWND dialog, const UInt32 *ids, unsigned numItems)
{
for (unsigned i = 0; i < numItems; i++)
{
const UInt32 id = ids[i];
const wchar_t *s = g_Lang.Get(id);
if (s)
{
CWindow window(GetDlgItem(dialog, (int)id));
UString s2 = s;
s2.Add_Colon();
window.SetText(s2);
}
}
}
void LangSetDlgItems_RemoveColon(HWND dialog, const UInt32 *ids, unsigned numItems)
{
for (unsigned i = 0; i < numItems; i++)
{
const UInt32 id = ids[i];
const wchar_t *s = g_Lang.Get(id);
if (s)
{
CWindow window(GetDlgItem(dialog, (int)id));
UString s2 = s;
if (!s2.IsEmpty() && s2.Back() == ':')
s2.DeleteBack();
window.SetText(s2);
}
}
}
void LangSetWindowText(HWND window, UInt32 langID)
{
const wchar_t *s = g_Lang.Get(langID);
if (s)
MySetWindowText(window, s);
}
UString LangString(UInt32 langID)
{
const wchar_t *s = g_Lang.Get(langID);
if (s)
return s;
return MyLoadString(langID);
}
void AddLangString(UString &s, UInt32 langID)
{
s += LangString(langID);
}
void LangString(UInt32 langID, UString &dest)
{
const wchar_t *s = g_Lang.Get(langID);
if (s)
{
dest = s;
return;
}
MyLoadString(langID, dest);
}
void LangString_OnlyFromLangFile(UInt32 langID, UString &dest)
{
dest.Empty();
const wchar_t *s = g_Lang.Get(langID);
if (s)
dest = s;
}
static const char * const kLangs =
"ar.bg.ca.zh.-tw.-cn.cs.da.de.el.en.es.fi.fr.he.hu.is."
"it.ja.ko.nl.no.=nb.=nn.pl.pt.-br.rm.ro.ru.sr.=hr.-spl.-spc.=hr.=bs.sk.sq.sv.th.tr."
"ur.id.uk.be.sl.et.lv.lt.tg.fa.vi.hy.az.eu.hsb.mk."
"st.ts.tn.ve.xh.zu.af.ka.fo.hi.mt.se.ga.yi.ms.kk."
"ky.sw.tk.uz.-latn.-cyrl.tt.bn.pa.-in.gu.or.ta.te.kn.ml.as.mr.sa."
"mn.=mn.=mng.bo.cy.kh.lo.my.gl.kok..sd.syr.si..iu.am.tzm."
"ks.ne.fy.ps.tl.dv..ff.ha..yo.qu.st.ba.lb.kl."
"ig.kr.om.ti.gn..la.so.ii..arn..moh..br.."
"ug.mi.oc.co."
// "gsw.sah.qut.rw.wo....prs...."
// ".gd."
;
static void FindShortNames(UInt32 primeLang, AStringVector &names)
{
UInt32 index = 0;
for (const char *p = kLangs; *p != 0;)
{
const char *p2 = p;
for (; *p2 != '.'; p2++);
bool isSub = (p[0] == '-' || p[0] == '=');
if (!isSub)
index++;
if (index >= primeLang)
{
if (index > primeLang)
break;
AString s;
if (isSub)
{
if (p[0] == '-')
s = names[0];
else
p++;
}
while (p != p2)
s.Add_Char((char)(Byte)*p++);
names.Add(s);
}
p = p2 + 1;
}
}
/*
#include "../../../Common/IntToString.h"
static struct CC1Lang
{
CC1Lang()
{
for (int i = 1; i < 150; i++)
{
UString s;
char ttt[32];
ConvertUInt32ToHex(i, ttt);
s += ttt;
UStringVector names;
FindShortNames(i, names);
FOR_VECTOR (k, names)
{
s.Add_Space();
s += names[k];
}
OutputDebugStringW(s);
}
}
} g_cc1;
*/
// typedef LANGID (WINAPI *GetUserDefaultUILanguageP)();
void Lang_GetShortNames_for_DefaultLang(AStringVector &names, unsigned &subLang)
{
names.Clear();
subLang = 0;
// Region / Administative / Language for non-Unicode programs:
const LANGID sysLang = GetSystemDefaultLangID();
// Region / Formats / Format:
const LANGID userLang = GetUserDefaultLangID();
if (PRIMARYLANGID(sysLang) !=
PRIMARYLANGID(userLang))
return;
const LANGID langID = userLang;
// const LANGID langID = MAKELANGID(0x1a, 1); // for debug
/*
LANGID sysUILang; // english in XP64
LANGID userUILang; // english in XP64
GetUserDefaultUILanguageP fn = (GetUserDefaultUILanguageP)GetProcAddress(
GetModuleHandle("kernel32"), "GetUserDefaultUILanguage");
if (fn)
userUILang = fn();
fn = (GetUserDefaultUILanguageP)GetProcAddress(
GetModuleHandle("kernel32"), "GetSystemDefaultUILanguage");
if (fn)
sysUILang = fn();
*/
const WORD primLang = (WORD)(PRIMARYLANGID(langID));
subLang = SUBLANGID(langID);
FindShortNames(primLang, names);
}
static void OpenDefaultLang()
{
AStringVector names;
unsigned subLang;
Lang_GetShortNames_for_DefaultLang(names, subLang);
{
const FString dirPrefix (GetLangDirPrefix());
for (unsigned i = 0; i < 2; i++)
{
const unsigned index = (i == 0 ? subLang : 0);
if (index < names.Size())
{
const AString &name = names[index];
if (!name.IsEmpty())
{
FString path (dirPrefix);
path += name;
path += ".txt";
if (LangOpen(g_Lang, path))
{
g_LangID = name;
return;
}
}
}
}
}
}
void ReloadLang()
{
g_Lang.Clear();
ReadRegLang(g_LangID);
if (g_LangID.IsEmpty())
{
#ifndef _UNICODE
if (g_IsNT)
#endif
OpenDefaultLang();
return;
}
if (g_LangID.Len() > 1 || g_LangID[0] != L'-')
{
FString s = us2fs(g_LangID);
if (s.ReverseFind_PathSepar() < 0)
{
if (s.ReverseFind_Dot() < 0)
s += ".txt";
s.Insert(0, GetLangDirPrefix());
LangOpen(g_Lang, s);
}
}
}
#endif