mono/packages/media/cpp/src/win/ui_next/Mainfrm.cpp

1460 lines
53 KiB
C++

#include "stdafx.h"
#include "Mainfrm.h"
#include "Resource.h"
#include "ProviderDlg.h"
#include "core/resize.hpp"
#include "core/transform.hpp"
#include "win/settings_store.hpp"
#include <shlobj.h>
#include <filesystem>
#include <fstream>
#include <sstream>
#include <UIRibbon.h>
namespace fs = std::filesystem;
using json = nlohmann::json;
static std::string wide_to_utf8(const std::wstring& w) {
if (w.empty()) return {};
int n = WideCharToMultiByte(CP_UTF8, 0, w.c_str(), (int)w.size(), nullptr, 0, nullptr, nullptr);
if (n <= 0) return {};
std::string s(n, '\0');
WideCharToMultiByte(CP_UTF8, 0, w.c_str(), (int)w.size(), s.data(), n, nullptr, nullptr);
return s;
}
static std::wstring utf8_to_wide_mf(const std::string& s) {
if (s.empty()) return {};
int n = MultiByteToWideChar(CP_UTF8, 0, s.c_str(), (int)s.size(), nullptr, 0);
if (n <= 0) return {};
std::wstring w(n, L'\0');
MultiByteToWideChar(CP_UTF8, 0, s.c_str(), (int)s.size(), w.data(), n);
return w;
}
CMainFrame::CMainFrame()
{
try {
m_settingsPath = media::settings::get_settings_json_path().string();
} catch (...) {
m_settingsPath = (fs::current_path() / "settings.json").string();
}
LoadPresets();
}
void CMainFrame::LogMessage(const CString& msg)
{
if (m_pDockLog)
m_pDockLog->GetLogContainer().GetLogView().AppendLine(msg);
}
HWND CMainFrame::Create(HWND parent)
{
SetView(m_view);
LoadRegistrySettings(L"Polymech\\pm-image-ui");
return CRibbonDockFrame::Create(parent);
}
void CMainFrame::SwitchSettingsMode(CSettingsView::Mode mode)
{
if (!m_pDockSettings) return;
auto& sv = m_pDockSettings->GetSettingsContainer().GetSettingsView();
if (sv.GetMode() == mode) return;
sv.SetMode(mode);
const wchar_t* caption = (mode == CSettingsView::MODE_RESIZE)
? L"Resize Settings" : L"Settings";
m_pDockSettings->GetSettingsContainer().SetDockCaption(caption);
m_pDockSettings->GetSettingsContainer().SetTabText(
(mode == CSettingsView::MODE_RESIZE) ? L"Resize" : L"Settings");
m_pDockSettings->GetSettingsContainer().Invalidate();
m_pDockSettings->RedrawWindow();
}
STDMETHODIMP CMainFrame::Execute(UINT32 cmdID, UI_EXECUTIONVERB verb,
const PROPERTYKEY*, const PROPVARIANT*, IUISimplePropertySet*)
{
if (verb == UI_EXECUTIONVERB_EXECUTE) {
switch (cmdID) {
case IDC_CMD_ADD_FILES: OnAddFiles(); break;
case IDC_CMD_ADD_FOLDER: OnAddFolder(); break;
case IDC_CMD_CLEAR: OnClearQueue(); break;
case IDC_CMD_SAVE_AS: OnSaveAs(); break;
case IDC_CMD_RESIZE:
SwitchSettingsMode(CSettingsView::MODE_RESIZE);
OnResize();
break;
case IDC_CMD_PROMPT:
SwitchSettingsMode(CSettingsView::MODE_TRANSFORM);
OnPrompt();
break;
case IDC_CMD_RUN:
SwitchSettingsMode(CSettingsView::MODE_TRANSFORM);
OnRun();
break;
case IDC_CMD_PRESETS: OnPresets(); break;
case IDC_CMD_PROVIDER_KEYS: ShowProviderSettingsDlg(GetHwnd()); break;
case IDC_CMD_ABOUT: OnHelp(); break;
case IDC_CMD_EXIT: OnExit(); break;
case IDC_RIBBONHELP: OnHelp(); break;
// Model toggle
case IDC_CMD_MODEL_PRO:
case IDC_CMD_MODEL_FLASH:
HandleModelSelect(cmdID);
break;
// Aspect toggle
case IDC_CMD_ASPECT_AUTO:
case IDC_CMD_ASPECT_1_1:
case IDC_CMD_ASPECT_3_2:
case IDC_CMD_ASPECT_4_3:
case IDC_CMD_ASPECT_16_9:
case IDC_CMD_ASPECT_9_16:
HandleAspectSelect(cmdID);
break;
// Size toggle
case IDC_CMD_SIZE_DEFAULT:
case IDC_CMD_SIZE_1K:
case IDC_CMD_SIZE_2K:
case IDC_CMD_SIZE_4K:
HandleSizeSelect(cmdID);
break;
// View panel toggles — each knows its parent and dock side
case IDC_CMD_VIEW_QUEUE:
TogglePanelView(m_pDockQueue, DS_DOCKED_BOTTOM,
GetDockAncestor(), DpiScaleInt(220), IDC_CMD_VIEW_QUEUE);
break;
case IDC_CMD_VIEW_LOG:
TogglePanelView(m_pDockLog, DS_DOCKED_RIGHT,
m_pDockQueue, DpiScaleInt(360), IDC_CMD_VIEW_LOG);
break;
case IDC_CMD_VIEW_SETTINGS:
TogglePanelView(m_pDockSettings, DS_DOCKED_RIGHT,
GetDockAncestor(), DpiScaleInt(280), IDC_CMD_VIEW_SETTINGS);
break;
case IDC_CMD_VIEW_FILEINFO:
TogglePanelView(m_pDockFileInfo, DS_DOCKED_BOTTOM,
m_pDockGenPreview, DpiScaleInt(160), IDC_CMD_VIEW_FILEINFO);
break;
case IDC_CMD_VIEW_GENPREVIEW:
TogglePanelView(m_pDockGenPreview, DS_DOCKED_BOTTOM,
m_pDockSettings, DpiScaleInt(200), IDC_CMD_VIEW_GENPREVIEW);
break;
case IDC_CMD_RESET_LAYOUT:
ResetLayout();
break;
case IDC_CMD_DEBUG_STATE:
DebugDockState();
break;
default: break;
}
}
if (cmdID == cmdTabHome)
SwitchSettingsMode(CSettingsView::MODE_RESIZE);
else if (cmdID == cmdTabAI)
SwitchSettingsMode(CSettingsView::MODE_TRANSFORM);
return S_OK;
}
STDMETHODIMP CMainFrame::OnViewChanged(UINT32, UI_VIEWTYPE typeId,
IUnknown* pView, UI_VIEWVERB verb, INT32)
{
if (typeId == UI_VIEWTYPE_RIBBON) {
switch (verb) {
case UI_VIEWVERB_CREATE:
m_pIUIRibbon = reinterpret_cast<IUIRibbon*>(pView);
return S_OK;
case UI_VIEWVERB_SIZE:
RecalcLayout();
return S_OK;
case UI_VIEWVERB_DESTROY:
m_pIUIRibbon = nullptr;
return S_OK;
case UI_VIEWVERB_ERROR:
return E_FAIL;
}
}
return E_NOTIMPL;
}
STDMETHODIMP CMainFrame::UpdateProperty(UINT32 cmdID, REFPROPERTYKEY key,
const PROPVARIANT* currentValue, PROPVARIANT* newValue)
{
if (key == UI_PKEY_BooleanValue) {
newValue->vt = VT_BOOL;
newValue->boolVal = IsToggleSelected(cmdID) ? VARIANT_TRUE : VARIANT_FALSE;
return S_OK;
}
return CRibbonDockFrame::UpdateProperty(cmdID, key, currentValue, newValue);
}
void CMainFrame::InvalidateToggle(UINT32 cmdID)
{
IUIFramework* pFW = GetRibbonFramework();
if (pFW)
pFW->InvalidateUICommand(cmdID, UI_INVALIDATIONS_PROPERTY, &UI_PKEY_BooleanValue);
}
bool CMainFrame::IsToggleSelected(UINT32 cmdID) const
{
if (cmdID == m_selModel || cmdID == m_selAspect || cmdID == m_selSize)
return true;
// View panel toggles reflect current visibility
switch (cmdID) {
case IDC_CMD_VIEW_QUEUE: return IsPanelVisible(m_pDockQueue);
case IDC_CMD_VIEW_LOG: return IsPanelVisible(m_pDockLog);
case IDC_CMD_VIEW_SETTINGS: return IsPanelVisible(m_pDockSettings);
case IDC_CMD_VIEW_FILEINFO: return IsPanelVisible(m_pDockFileInfo);
case IDC_CMD_VIEW_GENPREVIEW: return IsPanelVisible(m_pDockGenPreview);
}
return false;
}
void CMainFrame::HandleModelSelect(UINT32 cmdID)
{
UINT32 prev = m_selModel;
m_selModel = cmdID;
InvalidateToggle(prev);
InvalidateToggle(cmdID);
}
void CMainFrame::HandleAspectSelect(UINT32 cmdID)
{
UINT32 prev = m_selAspect;
m_selAspect = cmdID;
InvalidateToggle(prev);
InvalidateToggle(cmdID);
}
void CMainFrame::HandleSizeSelect(UINT32 cmdID)
{
UINT32 prev = m_selSize;
m_selSize = cmdID;
InvalidateToggle(prev);
InvalidateToggle(cmdID);
}
media::TransformOptions CMainFrame::BuildTransformOptions() const
{
media::TransformOptions opts;
switch (m_selModel) {
case IDC_CMD_MODEL_PRO: opts.model = "gemini-3-pro-image-preview"; break;
case IDC_CMD_MODEL_FLASH: opts.model = "gemini-3.1-flash-image-preview"; break;
}
switch (m_selAspect) {
case IDC_CMD_ASPECT_AUTO: opts.aspect_ratio = ""; break;
case IDC_CMD_ASPECT_1_1: opts.aspect_ratio = "1:1"; break;
case IDC_CMD_ASPECT_3_2: opts.aspect_ratio = "3:2"; break;
case IDC_CMD_ASPECT_4_3: opts.aspect_ratio = "4:3"; break;
case IDC_CMD_ASPECT_16_9: opts.aspect_ratio = "16:9"; break;
case IDC_CMD_ASPECT_9_16: opts.aspect_ratio = "9:16"; break;
}
switch (m_selSize) {
case IDC_CMD_SIZE_DEFAULT: opts.image_size = ""; break;
case IDC_CMD_SIZE_1K: opts.image_size = "1K"; break;
case IDC_CMD_SIZE_2K: opts.image_size = "2K"; break;
case IDC_CMD_SIZE_4K: opts.image_size = "4K"; break;
}
return opts;
}
BOOL CMainFrame::OnCommand(WPARAM wparam, LPARAM)
{
switch (LOWORD(wparam)) {
case IDM_ADD_FILES: OnAddFiles(); return TRUE;
case IDM_ADD_FOLDER: OnAddFolder(); return TRUE;
case IDM_CLEAR_QUEUE: OnClearQueue(); return TRUE;
case IDM_RESIZE: OnResize(); return TRUE;
case IDM_EXIT: OnExit(); return TRUE;
case IDM_ABOUT: return OnHelp();
case IDW_VIEW_STATUSBAR: return OnViewStatusBar();
case IDW_VIEW_TOOLBAR: return OnViewToolBar();
}
return FALSE;
}
BOOL CMainFrame::OnHelp()
{
::MessageBox(GetHwnd(), L"pm-image \u2014 resize & transform\nWin32++ UI",
L"About pm-image", MB_ICONINFORMATION | MB_OK);
return TRUE;
}
// Called by LoadDockRegistrySettings() to instantiate the right CDocker subclass.
// Do NOT store the raw pointer here — Win32++ may call CloseAllDockers() on failure
// which deletes the objects and leaves our stored pointers dangling.
// Member pointers are fetched safely via GetDockFromID() after a successful load.
DockPtr CMainFrame::NewDockerFromID(int id)
{
switch (id) {
case DOCK_ID_QUEUE: return std::make_unique<CDockQueue>();
case DOCK_ID_LOG: return std::make_unique<CDockLog>();
case DOCK_ID_SETTINGS: return std::make_unique<CDockSettings>();
case DOCK_ID_GENPREVIEW: return std::make_unique<CDockGenPreview>();
case DOCK_ID_FILEINFO: return std::make_unique<CDockFileInfo>();
}
return nullptr;
}
// Apply container single-tab hiding to any non-null panel pointer.
static void SetupDockContainers(CDockQueue* q, CDockLog* l, CDockSettings* s,
CDockGenPreview* gp, CDockFileInfo* fi)
{
if (q) q->GetQueueContainer().SetHideSingleTab(TRUE);
if (l) l->GetLogContainer().SetHideSingleTab(TRUE);
if (s) s->GetSettingsContainer().SetHideSingleTab(TRUE);
if (gp) gp->GetContainer().SetHideSingleTab(TRUE);
if (fi) fi->GetInfoContainer().SetHideSingleTab(TRUE);
}
// Helper: build the hardcoded default panel hierarchy.
void CMainFrame::BuildDefaultDockLayout()
{
CloseAllDockers(); // clear any partial state Win32++ might have
m_pDockQueue = nullptr; m_pDockLog = nullptr; m_pDockSettings = nullptr;
m_pDockGenPreview = nullptr; m_pDockFileInfo = nullptr;
auto pDockQ = AddDockedChild(std::make_unique<CDockQueue>(),
DS_DOCKED_BOTTOM, DpiScaleInt(220), DOCK_ID_QUEUE);
m_pDockQueue = static_cast<CDockQueue*>(pDockQ);
auto pDockL = m_pDockQueue->AddDockedChild(std::make_unique<CDockLog>(),
DS_DOCKED_RIGHT, DpiScaleInt(360), DOCK_ID_LOG);
m_pDockLog = static_cast<CDockLog*>(pDockL);
auto pDockS = AddDockedChild(std::make_unique<CDockSettings>(),
DS_DOCKED_RIGHT, DpiScaleInt(280), DOCK_ID_SETTINGS);
m_pDockSettings = static_cast<CDockSettings*>(pDockS);
auto pDockGP = m_pDockSettings->AddDockedChild(std::make_unique<CDockGenPreview>(),
DS_DOCKED_BOTTOM, DpiScaleInt(200), DOCK_ID_GENPREVIEW);
m_pDockGenPreview = static_cast<CDockGenPreview*>(pDockGP);
auto pDockFI = m_pDockGenPreview->AddDockedChild(std::make_unique<CDockFileInfo>(),
DS_DOCKED_BOTTOM, DpiScaleInt(160), DOCK_ID_FILEINFO);
m_pDockFileInfo = static_cast<CDockFileInfo*>(pDockFI);
}
void CMainFrame::OnInitialUpdate()
{
// Member pointers start null; they are set after a confirmed successful load
// (NOT inside NewDockerFromID, where they would dangle if Win32++ rolls back).
m_pDockQueue = nullptr; m_pDockLog = nullptr; m_pDockSettings = nullptr;
m_pDockGenPreview = nullptr; m_pDockFileInfo = nullptr;
bool fromRegistry = false;
try {
// Attempt to restore the full dock topology (parent, style, size, floating
// rect, hidden state) from the Win32++ registry key saved at WM_CLOSE.
fromRegistry = LoadDockRegistrySettings(L"Polymech\\pm-image-ui");
if (fromRegistry) {
// Fetch member pointers by dock ID now that all dockers are safely alive.
// GetDockFromID walks the m_allDockers list built by LoadDockRegistrySettings.
m_pDockQueue = static_cast<CDockQueue*> (GetDockFromID(DOCK_ID_QUEUE));
m_pDockLog = static_cast<CDockLog*> (GetDockFromID(DOCK_ID_LOG));
m_pDockSettings = static_cast<CDockSettings*> (GetDockFromID(DOCK_ID_SETTINGS));
m_pDockGenPreview = static_cast<CDockGenPreview*>(GetDockFromID(DOCK_ID_GENPREVIEW));
m_pDockFileInfo = static_cast<CDockFileInfo*> (GetDockFromID(DOCK_ID_FILEINFO));
// If any panel is missing the registry save is incomplete — fall back.
if (!m_pDockQueue || !m_pDockLog || !m_pDockSettings ||
!m_pDockGenPreview || !m_pDockFileInfo)
{
LogMessage(L"[Layout] Registry restore incomplete — using default layout.");
fromRegistry = false;
}
}
}
catch (const std::exception& ex) {
LogMessage(CString(L"[Layout] Registry restore threw: ") + ex.what());
fromRegistry = false;
}
catch (...) {
LogMessage(L"[Layout] Registry restore threw unknown exception.");
fromRegistry = false;
}
if (!fromRegistry) {
// Delete the stale/bad registry key so the next launch starts clean.
const CString appKey = _T("Software\\Polymech\\pm-image-ui");
CRegKey k;
if (ERROR_SUCCESS == k.Open(HKEY_CURRENT_USER, appKey))
k.RecurseDeleteKey(_T("Dock Settings"));
BuildDefaultDockLayout();
}
SetupDockContainers(m_pDockQueue, m_pDockLog, m_pDockSettings, m_pDockGenPreview, m_pDockFileInfo);
// Modern flat dock caption styling (null-safe).
COLORREF capBg = RGB(240, 240, 240);
COLORREF capFg = RGB(68, 68, 68);
COLORREF capBgInv = RGB(240, 240, 240);
COLORREF capFgInv = RGB(140, 140, 140);
COLORREF capPen = RGB(220, 220, 220);
auto styleDocker = [&](CDocker* d) {
if (!d) return;
d->SetCaptionColors(capFg, capBg, capFgInv, capBgInv, capPen);
d->SetCaptionHeight(22);
};
styleDocker(m_pDockQueue);
styleDocker(m_pDockLog);
styleDocker(m_pDockSettings);
styleDocker(m_pDockGenPreview);
styleDocker(m_pDockFileInfo);
DragAcceptFiles(TRUE);
SetWindowText(L"pm-image");
GetStatusBar().SetPartText(0, L"Drop files or use Add Files to begin.");
// Restore window placement and dock sizes saved from last session.
// Must run after all docks are created so SetDockSize is valid.
LoadLayout();
}
void CMainFrame::SetupToolBar()
{
// Toolbar is unused — the ribbon provides all commands.
// Leaving this empty avoids the IDW_MAIN BITMAP resource requirement.
}
void CMainFrame::OnAddFiles()
{
CFileDialog dlg(TRUE, nullptr, nullptr,
OFN_FILEMUSTEXIST | OFN_ALLOWMULTISELECT | OFN_EXPLORER,
L"Images (*.jpg;*.jpeg;*.png;*.webp;*.tif;*.tiff;*.bmp;*.gif;*.avif;*.heic)\0"
L"*.jpg;*.jpeg;*.png;*.webp;*.tif;*.tiff;*.bmp;*.gif;*.avif;*.heic\0"
L"All Files (*.*)\0*.*\0\0");
dlg.SetTitle(L"Add images to queue");
if (dlg.DoModal(*this) != IDOK) return;
std::vector<std::wstring> files;
int pos = 0;
CString path = dlg.GetNextPathName(pos);
while (!path.IsEmpty()) {
files.push_back(std::wstring(path.c_str()));
if (pos < 0) break;
path = dlg.GetNextPathName(pos);
}
AddFilesToQueue(files);
}
void CMainFrame::OnAddFolder()
{
BROWSEINFOW bi{};
bi.hwndOwner = GetHwnd();
bi.lpszTitle = L"Select folder with images";
bi.ulFlags = BIF_RETURNONLYFSDIRS | BIF_NEWDIALOGSTYLE;
wchar_t dn[MAX_PATH]{};
bi.pszDisplayName = dn;
PIDLIST_ABSOLUTE pidl = SHBrowseForFolderW(&bi);
if (!pidl) return;
wchar_t path[MAX_PATH * 4]{};
if (SHGetPathFromIDListW(pidl, path)) {
std::vector<std::wstring> v = { path };
AddFilesToQueue(v);
}
CoTaskMemFree(pidl);
}
void CMainFrame::AddFilesToQueue(const std::vector<std::wstring>& paths)
{
if (!m_pDockQueue) return;
auto& lv = m_pDockQueue->GetQueueContainer().GetListView();
bool firstAdded = (lv.QueueCount() == 0);
int firstNewIdx = lv.QueueCount();
for (auto& p : paths) {
std::error_code ec;
fs::path fp(p);
if (fs::is_directory(fp, ec)) {
for (auto& entry : fs::recursive_directory_iterator(fp,
fs::directory_options::skip_permission_denied, ec)) {
if (!entry.is_regular_file()) continue;
std::wstring ext = entry.path().extension().wstring();
for (auto& c : ext) c = static_cast<wchar_t>(towlower(c));
if (ext == L".jpg" || ext == L".jpeg" || ext == L".png" || ext == L".webp" ||
ext == L".tif" || ext == L".tiff" || ext == L".bmp" || ext == L".gif" ||
ext == L".avif" || ext == L".heic") {
lv.AddFile(CString(entry.path().c_str()));
}
}
} else {
lv.AddFile(CString(fp.c_str()));
}
}
CString status;
status.Format(L"%d file(s) in queue", lv.QueueCount());
GetStatusBar().SetPartText(0, status);
if (firstAdded && lv.QueueCount() > 0) {
CString firstPath = lv.GetItemPath(firstNewIdx);
m_view.LoadPicture(firstPath.c_str());
}
}
void CMainFrame::OnClearQueue()
{
if (m_pDockQueue) {
m_pDockQueue->GetQueueContainer().GetListView().ClearAll();
m_view.ClearPicture();
GetStatusBar().SetPartText(0, L"Queue cleared.");
}
}
void CMainFrame::OnSaveAs()
{
if (!m_pDockQueue) return;
auto& lv = m_pDockQueue->GetQueueContainer().GetListView();
// Use the first selected item, fall back to first item.
auto sel = GetSelectedQueueItems();
int idx = sel.empty() ? 0 : sel.front();
if (idx < 0 || idx >= lv.QueueCount()) {
::MessageBoxW(GetHwnd(), L"No file selected in the queue.", L"Save As", MB_ICONINFORMATION);
return;
}
CString srcPath = lv.GetItemPath(idx);
if (srcPath.IsEmpty()) return;
// Derive a suggested filename from the source path.
fs::path src(srcPath.c_str());
std::wstring suggestedName = src.filename().wstring();
std::wstring ext = src.extension().wstring();
// ext includes the dot, e.g. L".jpg"
if (ext.empty()) ext = L".*";
// Build the filter string: "JPEG Files (*.jpg)\0*.jpg\0All Files (*.*)\0*.*\0"
// We need a double-null-terminated sequence.
std::wstring extNoDot = ext.size() > 1 ? ext.substr(1) : L"*";
std::wstring extUpper = extNoDot;
for (auto& c : extUpper) c = towupper(c);
wchar_t filter[256] = {};
int fpos = 0;
auto appendStr = [&](const std::wstring& s) {
for (wchar_t c : s) filter[fpos++] = c;
filter[fpos++] = L'\0';
};
appendStr(extUpper + L" Files (*." + extNoDot + L")");
appendStr(L"*." + extNoDot);
appendStr(L"All Files (*.*)");
appendStr(L"*.*");
wchar_t destPath[MAX_PATH] = {};
wcscpy_s(destPath, suggestedName.c_str());
OPENFILENAMEW ofn{};
ofn.lStructSize = sizeof(ofn);
ofn.hwndOwner = GetHwnd();
ofn.lpstrFilter = filter;
ofn.lpstrFile = destPath;
ofn.nMaxFile = MAX_PATH;
ofn.lpstrTitle = L"Save As";
ofn.Flags = OFN_OVERWRITEPROMPT | OFN_PATHMUSTEXIST | OFN_NOCHANGEDIR;
ofn.lpstrDefExt = extNoDot.c_str();
if (!::GetSaveFileNameW(&ofn))
return; // user cancelled
std::error_code ec;
fs::copy_file(src, fs::path(destPath), fs::copy_options::overwrite_existing, ec);
if (ec) {
std::wstring msg = L"Copy failed: " + utf8_to_wide_mf(ec.message());
::MessageBoxW(GetHwnd(), msg.c_str(), L"Save As", MB_ICONERROR);
} else {
CString log;
log.Format(L"Saved: %s", destPath);
LogMessage(log);
GetStatusBar().SetPartText(0, log);
}
}
void CMainFrame::OnResize()
{
if (m_processing) {
::MessageBox(GetHwnd(), L"Already processing.", L"pm-image", MB_ICONWARNING);
return;
}
if (!m_pDockQueue || !m_pDockSettings) return;
auto& lv = m_pDockQueue->GetQueueContainer().GetListView();
int count = lv.QueueCount();
if (count == 0) {
::MessageBox(GetHwnd(), L"Queue is empty \u2014 drop files first.", L"pm-image", MB_ICONINFORMATION);
return;
}
media::ResizeOptions opt;
std::string out_dir;
m_pDockSettings->GetSettingsContainer().GetSettingsView().ReadOptions(opt, out_dir);
std::vector<std::pair<int, std::string>> items;
items.reserve(count);
for (int i = 0; i < count; ++i) {
CString p = lv.GetItemPath(i);
items.emplace_back(i, wide_to_utf8(std::wstring(p.c_str())));
}
m_processing = true;
HWND hwnd = GetHwnd();
LogMessage(L"--- Resize started ---");
CString rinfo;
rinfo.Format(L"%d file(s) | Max: %dx%d | Q: %d",
(int)items.size(), opt.max_width, opt.max_height, opt.quality);
LogMessage(rinfo);
m_worker = std::thread([items = std::move(items), opt, out_dir, hwnd]() mutable {
int ok = 0, fail = 0;
for (auto& [idx, input] : items) {
::PostMessage(hwnd, UWM_QUEUE_PROGRESS, (WPARAM)idx, 1);
std::string output_path;
if (!out_dir.empty()) {
namespace fs2 = std::filesystem;
fs2::path op = fs2::path(out_dir) / fs2::path(input).filename();
output_path = op.string();
std::error_code ec2;
fs2::create_directories(fs2::path(out_dir), ec2);
} else {
output_path = input;
}
std::string err;
bool success = media::resize_file(input, output_path, opt, err);
::PostMessage(hwnd, UWM_QUEUE_PROGRESS, (WPARAM)idx, success ? 2 : 3);
if (success) ++ok; else ++fail;
}
::PostMessage(hwnd, UWM_QUEUE_DONE, (WPARAM)ok, (LPARAM)fail);
});
m_worker.detach();
GetStatusBar().SetPartText(0, L"Resizing\u2026");
}
std::vector<int> CMainFrame::GetSelectedQueueItems()
{
std::vector<int> sel;
if (!m_pDockQueue) return sel;
auto& lv = m_pDockQueue->GetQueueContainer().GetListView();
int idx = -1;
while ((idx = lv.GetNextItem(idx, LVNI_SELECTED)) != -1)
sel.push_back(idx);
if (sel.empty()) {
int total = lv.QueueCount();
for (int i = 0; i < total; ++i) sel.push_back(i);
}
return sel;
}
static INT_PTR CALLBACK PromptDlgProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp)
{
switch (msg) {
case WM_INITDIALOG: {
::SetWindowLongPtr(hwnd, GWLP_USERDATA, lp);
auto* prompt = reinterpret_cast<std::wstring*>(lp);
if (!prompt->empty())
::SetDlgItemTextW(hwnd, IDC_EDIT_PROMPT, prompt->c_str());
RECT rc;
::GetClientRect(hwnd, &rc);
int w = rc.right - rc.left, h = rc.bottom - rc.top;
int margin = 10;
HWND hLabel = ::GetDlgItem(hwnd, IDC_STATIC_PROMPT_LABEL);
::SetWindowPos(hLabel, nullptr, margin, margin, w - 2*margin, 20, SWP_NOZORDER);
HWND hEdit = ::GetDlgItem(hwnd, IDC_EDIT_PROMPT);
::SetWindowPos(hEdit, nullptr, margin, 35, w - 2*margin, h - 80, SWP_NOZORDER);
HWND hOk = ::GetDlgItem(hwnd, IDOK);
HWND hCancel = ::GetDlgItem(hwnd, IDCANCEL);
int btnW = 80, btnH = 28;
::SetWindowPos(hOk, nullptr, w - 2*(btnW + margin), h - btnH - margin, btnW, btnH, SWP_NOZORDER);
::SetWindowPos(hCancel, nullptr, w - btnW - margin, h - btnH - margin, btnW, btnH, SWP_NOZORDER);
HFONT hFont = static_cast<HFONT>(::GetStockObject(DEFAULT_GUI_FONT));
::SendMessage(hLabel, WM_SETFONT, (WPARAM)hFont, TRUE);
::SendMessage(hEdit, WM_SETFONT, (WPARAM)hFont, TRUE);
::SendMessage(hOk, WM_SETFONT, (WPARAM)hFont, TRUE);
::SendMessage(hCancel, WM_SETFONT, (WPARAM)hFont, TRUE);
::SetFocus(hEdit);
return FALSE;
}
case WM_COMMAND:
if (LOWORD(wp) == IDOK) {
auto* prompt = reinterpret_cast<std::wstring*>(::GetWindowLongPtr(hwnd, GWLP_USERDATA));
wchar_t buf[4096]{};
::GetDlgItemTextW(hwnd, IDC_EDIT_PROMPT, buf, 4095);
*prompt = buf;
::EndDialog(hwnd, IDOK);
return TRUE;
}
if (LOWORD(wp) == IDCANCEL) {
::EndDialog(hwnd, IDCANCEL);
return TRUE;
}
break;
case WM_CLOSE:
::EndDialog(hwnd, IDCANCEL);
return TRUE;
}
return FALSE;
}
static HWND CreatePromptDialog(HWND parent, std::wstring& prompt)
{
// Build a dialog template in memory
alignas(DWORD) BYTE buf[2048]{};
DLGTEMPLATE* dlg = reinterpret_cast<DLGTEMPLATE*>(buf);
dlg->style = DS_MODALFRAME | DS_CENTER | WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_VISIBLE;
dlg->cdit = 4; // label, edit, OK, Cancel
dlg->cx = 280; dlg->cy = 160;
WORD* p = reinterpret_cast<WORD*>(dlg + 1);
*p++ = 0; // menu
*p++ = 0; // class
// title "AI Transform - Enter Prompt"
const wchar_t title[] = L"AI Transform - Enter Prompt";
memcpy(p, title, sizeof(title));
p += (sizeof(title) / sizeof(WORD));
// Align to DWORD
if (reinterpret_cast<uintptr_t>(p) % 4) p++;
auto addItem = [&](DWORD style, short x, short y, short cx, short cy, WORD id, const wchar_t* cls, const wchar_t* text) {
if (reinterpret_cast<uintptr_t>(p) % 4) p++;
DLGITEMTEMPLATE* item = reinterpret_cast<DLGITEMTEMPLATE*>(p);
item->style = style | WS_CHILD | WS_VISIBLE;
item->x = x; item->y = y; item->cx = cx; item->cy = cy;
item->id = id;
p = reinterpret_cast<WORD*>(item + 1);
// class
size_t clen = wcslen(cls) + 1;
memcpy(p, cls, clen * 2); p += clen;
// text
size_t tlen = wcslen(text) + 1;
memcpy(p, text, tlen * 2); p += tlen;
*p++ = 0; // extra
};
// Label
addItem(SS_LEFT, 6, 4, 268, 10, IDC_STATIC_PROMPT_LABEL,
L"Static", L"Describe the transformation:");
// Multiline edit
addItem(ES_MULTILINE | ES_AUTOVSCROLL | ES_WANTRETURN | WS_BORDER | WS_VSCROLL | WS_TABSTOP,
6, 18, 268, 110, IDC_EDIT_PROMPT, L"Edit", L"");
// OK button
addItem(BS_DEFPUSHBUTTON | WS_TABSTOP, 150, 134, 56, 18, IDOK, L"Button", L"Transform");
// Cancel button
addItem(WS_TABSTOP, 212, 134, 56, 18, IDCANCEL, L"Button", L"Cancel");
INT_PTR result = ::DialogBoxIndirectParam(
::GetModuleHandle(nullptr),
dlg, parent, PromptDlgProc,
reinterpret_cast<LPARAM>(&prompt));
return (result == IDOK) ? parent : nullptr;
}
void CMainFrame::OnPrompt()
{
std::wstring prompt = m_lastPrompt;
if (!CreatePromptDialog(GetHwnd(), prompt))
return;
m_lastPrompt = prompt;
if (!prompt.empty()) {
CString s;
s.Format(L"Prompt set: %.60s%s", prompt.c_str(), prompt.size() > 60 ? L"\u2026" : L"");
GetStatusBar().SetPartText(0, s);
LogMessage(s);
}
}
void CMainFrame::OnRun()
{
if (m_processing) {
::MessageBox(GetHwnd(), L"Already processing.", L"pm-image", MB_ICONWARNING);
return;
}
if (!m_pDockQueue) return;
auto indices = GetSelectedQueueItems();
if (indices.empty()) {
::MessageBox(GetHwnd(), L"Queue is empty \u2014 drop files first.", L"pm-image", MB_ICONINFORMATION);
return;
}
if (m_lastPrompt.empty()) {
OnPrompt();
if (m_lastPrompt.empty()) return;
}
std::string promptUtf8 = wide_to_utf8(m_lastPrompt);
// Look up API key: settings store first, env var as fallback.
std::string active_provider;
std::string api_key = media::settings::get_active_api_key(active_provider);
if (api_key.empty()) {
// Legacy env-var fallback (Google only)
const char* env_key = std::getenv("IMAGE_TRANSFORM_GOOGLE_API_KEY");
if (env_key && env_key[0] != '\0') {
api_key = env_key;
active_provider = "google";
}
}
if (api_key.empty()) {
int choice = ::MessageBoxW(GetHwnd(),
L"No API key configured.\n\nOpen the API Keys dialog now?",
L"pm-image", MB_ICONWARNING | MB_YESNO);
if (choice == IDYES)
ShowProviderSettingsDlg(GetHwnd());
return;
}
media::TransformOptions base_opts = BuildTransformOptions();
base_opts.provider = active_provider;
base_opts.prompt = promptUtf8;
base_opts.api_key = api_key;
auto& lv = m_pDockQueue->GetQueueContainer().GetListView();
std::vector<std::pair<int, std::string>> items;
for (int idx : indices) {
CString p = lv.GetItemPath(idx);
items.emplace_back(idx, wide_to_utf8(std::wstring(p.c_str())));
}
m_processing = true;
HWND hwnd = GetHwnd();
LogMessage(L"--- Transform started ---");
CString info;
info.Format(L"Model: %S | %d file(s) | Aspect: %S | Size: %S",
base_opts.model.c_str(), (int)items.size(),
base_opts.aspect_ratio.empty() ? "auto" : base_opts.aspect_ratio.c_str(),
base_opts.image_size.empty() ? "default" : base_opts.image_size.c_str());
LogMessage(info);
m_worker = std::thread([items = std::move(items), base_opts, hwnd]() {
int ok = 0, fail = 0;
for (auto& [idx, input] : items) {
::PostMessage(hwnd, UWM_TRANSFORM_PROGRESS, (WPARAM)idx, 1);
auto progress = [hwnd](const std::string& msg) {
auto* ws = new wchar_t[msg.size() * 2 + 2];
int n = MultiByteToWideChar(CP_UTF8, 0, msg.c_str(), (int)msg.size(), ws, (int)(msg.size() * 2 + 1));
ws[n] = L'\0';
::PostMessage(hwnd, UWM_LOG_MESSAGE, (WPARAM)ws, 0);
};
media::TransformOptions topts = base_opts;
std::string output = media::default_transform_output(input, topts.prompt);
auto result = media::transform_image(input, output, topts, progress);
if (result.ok) {
++ok;
::PostMessage(hwnd, UWM_TRANSFORM_PROGRESS, (WPARAM)idx, 2);
// Send both source path and generated path as a pair
std::wstring wsrc = std::wstring(input.begin(), input.end());
std::wstring wout;
int n = MultiByteToWideChar(CP_UTF8, 0, result.output_path.c_str(),
(int)result.output_path.size(), nullptr, 0);
if (n > 0) { wout.resize(n); MultiByteToWideChar(CP_UTF8, 0,
result.output_path.c_str(), (int)result.output_path.size(), wout.data(), n); }
auto* pair = new std::pair<std::wstring, std::wstring>(wsrc, wout);
::PostMessage(hwnd, UWM_GENERATED_FILE, (WPARAM)pair, 0);
} else {
++fail;
::PostMessage(hwnd, UWM_TRANSFORM_PROGRESS, (WPARAM)idx, 3);
auto* ws = new wchar_t[result.error.size() * 2 + 16];
std::wstring errmsg = L"Error: " + utf8_to_wide_mf(result.error);
wcscpy_s(ws, errmsg.size() + 1, errmsg.c_str());
::PostMessage(hwnd, UWM_LOG_MESSAGE, (WPARAM)ws, 0);
}
}
::PostMessage(hwnd, UWM_TRANSFORM_DONE, (WPARAM)ok, (LPARAM)fail);
});
m_worker.detach();
GetStatusBar().SetPartText(0, L"Transforming\u2026");
}
void CMainFrame::OnExit()
{
Close();
}
LRESULT CMainFrame::OnGetMinMaxInfo(UINT msg, WPARAM wparam, LPARAM lparam)
{
LPMINMAXINFO lpMMI = reinterpret_cast<LPMINMAXINFO>(lparam);
const CSize minSz(600, 400);
lpMMI->ptMinTrackSize.x = DpiScaleInt(minSz.cx);
lpMMI->ptMinTrackSize.y = DpiScaleInt(minSz.cy);
return FinalWindowProc(msg, wparam, lparam);
}
LRESULT CMainFrame::OnQueueProgress(WPARAM wparam, LPARAM lparam)
{
if (!m_pDockQueue) return 0;
auto& lv = m_pDockQueue->GetQueueContainer().GetListView();
int idx = static_cast<int>(wparam);
int state = static_cast<int>(lparam);
if (state == 1)
lv.SetItemStatus(idx, L"Resizing\u2026");
else if (state == 2)
lv.SetItemStatus(idx, L"\u2713 Done");
else
lv.SetItemStatus(idx, L"\u2717 Error");
return 0;
}
LRESULT CMainFrame::OnQueueDone(WPARAM wparam, LPARAM lparam)
{
m_processing = false;
int ok = static_cast<int>(wparam);
int fail = static_cast<int>(lparam);
CString s;
s.Format(L"Done: %d succeeded, %d failed", ok, fail);
GetStatusBar().SetPartText(0, s);
LogMessage(s);
if (fail > 0)
::MessageBox(GetHwnd(), s, L"pm-image", MB_ICONWARNING);
return 0;
}
LRESULT CMainFrame::OnTransformProgress(WPARAM wparam, LPARAM lparam)
{
if (!m_pDockQueue) return 0;
auto& lv = m_pDockQueue->GetQueueContainer().GetListView();
int idx = static_cast<int>(wparam);
int state = static_cast<int>(lparam);
if (state == 1)
lv.SetItemStatus(idx, L"Transforming\u2026");
else if (state == 2)
lv.SetItemStatus(idx, L"\u2713 Transformed");
else
lv.SetItemStatus(idx, L"\u2717 AI Error");
return 0;
}
LRESULT CMainFrame::OnTransformDone(WPARAM wparam, LPARAM lparam)
{
m_processing = false;
int ok = static_cast<int>(wparam);
int fail = static_cast<int>(lparam);
CString s;
s.Format(L"Transform done: %d succeeded, %d failed", ok, fail);
GetStatusBar().SetPartText(0, s);
LogMessage(s);
if (fail > 0)
::MessageBox(GetHwnd(), s, L"pm-image", MB_ICONWARNING);
return 0;
}
LRESULT CMainFrame::OnLogMessage(WPARAM wparam)
{
auto* ws = reinterpret_cast<wchar_t*>(wparam);
if (ws) {
LogMessage(CString(ws));
delete[] ws;
}
return 0;
}
LRESULT CMainFrame::OnGeneratedFile(WPARAM wparam)
{
auto* pair = reinterpret_cast<std::pair<std::wstring, std::wstring>*>(wparam);
if (pair && m_pDockQueue) {
m_generatedMap[pair->first] = pair->second;
auto& lv = m_pDockQueue->GetQueueContainer().GetListView();
int idx = lv.AddFile(CString(pair->second.c_str()));
lv.SetItemStatus(idx, L"\u2728 Generated");
LogMessage(CString(L"Generated: ") + pair->second.c_str());
delete pair;
}
return 0;
}
void CMainFrame::UpdateFileInfoForSelection(int idx)
{
if (!m_pDockQueue) return;
auto& lv = m_pDockQueue->GetQueueContainer().GetListView();
if (idx < 0 || idx >= lv.QueueCount()) return;
CString path = lv.GetItemPath(idx);
if (path.IsEmpty()) return;
if (m_pDockFileInfo && ::IsWindowVisible(m_pDockFileInfo->GetHwnd()))
m_pDockFileInfo->GetInfoContainer().GetInfoView().ShowFileInfo(path.c_str());
if (m_pDockGenPreview && ::IsWindowVisible(m_pDockGenPreview->GetHwnd()) && m_pDockSettings) {
auto& sv = m_pDockSettings->GetSettingsContainer().GetSettingsView();
if (sv.GetMode() == CSettingsView::MODE_TRANSFORM) {
std::wstring srcKey(path.c_str());
auto it = m_generatedMap.find(srcKey);
auto& gp = m_pDockGenPreview->GetContainer().GetPreview();
if (it != m_generatedMap.end())
gp.LoadPicture(it->second.c_str());
else
gp.ClearPicture();
}
}
}
void CMainFrame::ResetLayout()
{
// Dock hierarchy (must restore parents before children):
// ancestor → Queue (BOTTOM) → Log (RIGHT of Queue)
// ancestor → Settings (RIGHT) → GenPreview (BOTTOM) → FileInfo (BOTTOM of GenPreview)
// Ensure a docker is undocked before we re-dock it elsewhere.
auto ensureUndocked = [this](CDocker* p) {
if (!p) return;
if (p->IsDocked() || IsPanelVisible(p))
p->Hide(); // undocks + hides
};
// Undock everything first (children before parents to avoid layout thrash)
ensureUndocked(m_pDockFileInfo);
ensureUndocked(m_pDockGenPreview);
ensureUndocked(m_pDockLog);
ensureUndocked(m_pDockQueue);
ensureUndocked(m_pDockSettings);
// Re-dock in hierarchy order: parents first
CDocker* ancestor = GetDockAncestor();
if (m_pDockQueue && ancestor) {
ancestor->Dock(m_pDockQueue, DS_DOCKED_BOTTOM);
m_pDockQueue->SetDockSize(DpiScaleInt(220));
InvalidateToggle(IDC_CMD_VIEW_QUEUE);
}
if (m_pDockLog && m_pDockQueue) {
m_pDockQueue->Dock(m_pDockLog, DS_DOCKED_RIGHT);
m_pDockLog->SetDockSize(DpiScaleInt(360));
InvalidateToggle(IDC_CMD_VIEW_LOG);
}
if (m_pDockSettings && ancestor) {
ancestor->Dock(m_pDockSettings, DS_DOCKED_RIGHT);
m_pDockSettings->SetDockSize(DpiScaleInt(280));
InvalidateToggle(IDC_CMD_VIEW_SETTINGS);
}
if (m_pDockGenPreview && m_pDockSettings) {
m_pDockSettings->Dock(m_pDockGenPreview, DS_DOCKED_BOTTOM);
m_pDockGenPreview->SetDockSize(DpiScaleInt(200));
InvalidateToggle(IDC_CMD_VIEW_GENPREVIEW);
}
if (m_pDockFileInfo && m_pDockGenPreview) {
m_pDockGenPreview->Dock(m_pDockFileInfo, DS_DOCKED_BOTTOM);
m_pDockFileInfo->SetDockSize(DpiScaleInt(160));
InvalidateToggle(IDC_CMD_VIEW_FILEINFO);
}
// Centre window on the primary monitor
HMONITOR hMon = ::MonitorFromWindow(GetHwnd(), MONITOR_DEFAULTTOPRIMARY);
MONITORINFO mi{sizeof(mi)};
::GetMonitorInfoW(hMon, &mi);
const RECT& wa = mi.rcWork;
int w = std::min(1280L, wa.right - wa.left - 80);
int h = std::min(860L, wa.bottom - wa.top - 80);
int x = wa.left + (wa.right - wa.left - w) / 2;
int y = wa.top + (wa.bottom - wa.top - h) / 2;
::SetWindowPos(GetHwnd(), nullptr, x, y, w, h, SWP_NOZORDER | SWP_SHOWWINDOW);
RecalcLayout();
// Persist the clean defaults so the next session starts fresh.
// Save dock topology first (before settings.json window placement clear).
SaveDockRegistrySettings(L"Polymech\\pm-image-ui");
std::string err;
media::settings::WindowLayout defaults;
media::settings::save_window_layout(defaults, err);
}
void CMainFrame::DebugDockState()
{
auto dockState = [](const CDocker* p, const char* name) -> std::string {
if (!p) return std::string("\"") + name + "\": null";
std::string s = std::string("\"") + name + "\": { ";
s += "\"docked\": "; s += p->IsDocked() ? "true" : "false"; s += ", ";
s += "\"undocked\": "; s += p->IsUndocked() ? "true" : "false"; s += ", ";
s += "\"visible\": "; s += ::IsWindowVisible(p->GetHwnd()) ? "true" : "false"; s += ", ";
s += "\"size\": "; s += std::to_string(p->GetDockSize()); s += " }";
return s;
};
std::string json = "{\n ";
json += dockState(m_pDockQueue, "Queue"); json += ",\n ";
json += dockState(m_pDockLog, "Log"); json += ",\n ";
json += dockState(m_pDockSettings, "Settings"); json += ",\n ";
json += dockState(m_pDockGenPreview, "GenPreview"); json += ",\n ";
json += dockState(m_pDockFileInfo, "FileInfo");
json += "\n}";
LogMessage(CString(L"── Dock State ──────────────────────"));
// Print line by line so it's readable in the log
std::istringstream ss(json);
std::string line;
while (std::getline(ss, line))
LogMessage(CString(utf8_to_wide_mf(line).c_str()));
}
// ── Presets ─────────────────────────────────────────────
void CMainFrame::LoadPresets()
{
m_presets.clear();
try {
std::string raw;
std::string err;
if (!media::settings::load_settings_utf8(raw, err) || raw.empty())
return;
json j = json::parse(raw, nullptr, false);
if (j.is_discarded()) return;
if (j.contains("transform") && j["transform"].contains("prompt_presets")) {
for (auto& p : j["transform"]["prompt_presets"]) {
PromptPreset pp;
pp.name = p.value("name", "");
pp.prompt = p.value("prompt", "");
if (!pp.prompt.empty())
m_presets.push_back(std::move(pp));
}
}
} catch (...) {}
}
void CMainFrame::SavePresets()
{
json j;
try {
std::string existing;
std::string err;
if (media::settings::load_settings_utf8(existing, err) && !existing.empty()) {
j = json::parse(existing, nullptr, false);
if (j.is_discarded()) j = json::object();
} else {
j = json::object();
}
} catch (...) { j = json::object(); }
json arr = json::array();
for (auto& p : m_presets)
arr.push_back({ {"name", p.name}, {"prompt", p.prompt} });
j["transform"]["prompt_presets"] = arr;
try {
std::string err;
media::settings::save_settings_utf8(j.dump(2), err);
} catch (...) {}
}
void CMainFrame::AddPreset(const std::string& name, const std::string& prompt)
{
m_presets.push_back({ name, prompt });
SavePresets();
}
void CMainFrame::RemovePreset(int index)
{
if (index >= 0 && index < (int)m_presets.size()) {
m_presets.erase(m_presets.begin() + index);
SavePresets();
}
}
// ── Layout persistence ──────────────────────────────────────────────────
bool CMainFrame::IsPanelVisible(const CDocker* pDock) const
{
return pDock && ::IsWindowVisible(pDock->GetHwnd());
}
// Re-dock pDock into pParent with dockStyle if it is currently hidden or floating.
// If it is already docked, hide it instead (toggle semantics).
void CMainFrame::TogglePanelView(CDocker* pDock, UINT dockStyle, CDocker* pParent, int defaultSize, UINT32 cmdID)
{
if (!pDock || !pParent) return;
if (IsPanelVisible(pDock)) {
// Docked or floating → hide (also undocks if docked)
pDock->Hide();
} else {
// Hidden or not docked → re-dock into original parent
if (pDock->IsDocked()) {
// Shouldn't normally happen, but ensure it's undocked first
pDock->Hide();
}
pParent->Dock(pDock, dockStyle);
if (defaultSize > 0)
pDock->SetDockSize(defaultSize);
}
InvalidateToggle(cmdID);
}
void CMainFrame::SaveLayout()
{
// Dock topology (parent, style, size, floating rect, hidden) is saved by
// SaveDockRegistrySettings() in the WM_CLOSE handler. This function only
// persists the main window placement to settings.json.
WINDOWPLACEMENT wp{sizeof(wp)};
if (!GetWindowPlacement(wp))
return;
media::settings::WindowLayout layout;
layout.has_placement = true;
layout.show_cmd = (wp.showCmd == SW_SHOWMINIMIZED) ? SW_SHOWNORMAL : (int)wp.showCmd;
layout.min_pos = wp.ptMinPosition;
layout.max_pos = wp.ptMaxPosition;
layout.normal_rect = wp.rcNormalPosition;
std::string err;
media::settings::save_window_layout(layout, err);
}
void CMainFrame::LoadLayout()
{
// Dock topology (parent, style, size, floating rect, hidden) was already
// restored by LoadDockRegistrySettings() at the top of OnInitialUpdate().
// Here we only restore the main window placement from settings.json.
media::settings::WindowLayout layout;
std::string err;
if (!media::settings::load_window_layout(layout, err) || !layout.has_placement)
return;
// ── Multi-monitor safety ──────────────────────────────────────────
POINT centre = {
(layout.normal_rect.left + layout.normal_rect.right) / 2,
(layout.normal_rect.top + layout.normal_rect.bottom) / 2,
};
HMONITOR hMon = ::MonitorFromPoint(centre, MONITOR_DEFAULTTONULL);
if (!hMon) {
HMONITOR hPrimary = ::MonitorFromPoint({0, 0}, MONITOR_DEFAULTTOPRIMARY);
MONITORINFO mi{sizeof(mi)};
::GetMonitorInfoW(hPrimary, &mi);
const RECT& wa = mi.rcWork;
int w = std::min(1200L, wa.right - wa.left);
int h = std::min(800L, wa.bottom - wa.top);
layout.normal_rect = { wa.left + 40, wa.top + 40, wa.left + 40 + w, wa.top + 40 + h };
layout.show_cmd = SW_SHOWNORMAL;
}
WINDOWPLACEMENT wp{sizeof(wp)};
wp.showCmd = (UINT)layout.show_cmd;
wp.ptMinPosition = layout.min_pos;
wp.ptMaxPosition = layout.max_pos;
wp.rcNormalPosition = layout.normal_rect;
SetWindowPlacement(wp);
}
void CMainFrame::OnPresets()
{
ShowPresetsMenu();
}
void CMainFrame::ShowPresetsMenu()
{
constexpr UINT ID_PRESET_BASE = 50000;
constexpr UINT ID_REMOVE_BASE = 51000;
constexpr UINT ID_SAVE_PRESET = 52000;
HMENU hMenu = ::CreatePopupMenu();
if (!hMenu) return;
if (m_presets.empty()) {
::AppendMenuW(hMenu, MF_STRING | MF_GRAYED, 0, L"(no presets)");
} else {
for (int i = 0; i < (int)m_presets.size(); ++i) {
std::wstring label = utf8_to_wide_mf(m_presets[i].name);
if (label.empty())
label = utf8_to_wide_mf(m_presets[i].prompt.substr(0, 40));
::AppendMenuW(hMenu, MF_STRING, ID_PRESET_BASE + i, label.c_str());
}
}
::AppendMenuW(hMenu, MF_SEPARATOR, 0, nullptr);
::AppendMenuW(hMenu, MF_STRING, ID_SAVE_PRESET, L"Save current prompt\u2026");
if (!m_presets.empty()) {
HMENU hRemove = ::CreatePopupMenu();
for (int i = 0; i < (int)m_presets.size(); ++i) {
std::wstring label = utf8_to_wide_mf(m_presets[i].name);
if (label.empty())
label = utf8_to_wide_mf(m_presets[i].prompt.substr(0, 40));
::AppendMenuW(hRemove, MF_STRING, ID_REMOVE_BASE + i, label.c_str());
}
::AppendMenuW(hMenu, MF_POPUP, (UINT_PTR)hRemove, L"Remove preset");
}
POINT pt;
::GetCursorPos(&pt);
UINT choice = (UINT)::TrackPopupMenu(hMenu, TPM_RETURNCMD | TPM_NONOTIFY,
pt.x, pt.y, 0, GetHwnd(), nullptr);
::DestroyMenu(hMenu);
if (choice == 0) return;
if (choice == ID_SAVE_PRESET) {
std::wstring prompt = m_lastPrompt;
if (prompt.empty()) {
::MessageBox(GetHwnd(), L"No prompt to save.\nRun a transform first, or type a prompt.",
L"pm-image", MB_ICONINFORMATION);
return;
}
// Ask for a name
wchar_t nameBuf[256]{};
alignas(DWORD) BYTE dlgBuf[1024]{};
DLGTEMPLATE* dlg = reinterpret_cast<DLGTEMPLATE*>(dlgBuf);
dlg->style = DS_MODALFRAME | DS_CENTER | WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_VISIBLE;
dlg->cdit = 3;
dlg->cx = 220; dlg->cy = 60;
WORD* p = reinterpret_cast<WORD*>(dlg + 1);
*p++ = 0; *p++ = 0;
const wchar_t dlgTitle[] = L"Save Preset";
memcpy(p, dlgTitle, sizeof(dlgTitle)); p += sizeof(dlgTitle) / sizeof(WORD);
if (reinterpret_cast<uintptr_t>(p) % 4) p++;
auto addCtrl = [&](DWORD style, short x, short y, short cx, short cy, WORD id, const wchar_t* cls, const wchar_t* text) {
if (reinterpret_cast<uintptr_t>(p) % 4) p++;
DLGITEMTEMPLATE* item = reinterpret_cast<DLGITEMTEMPLATE*>(p);
item->style = style | WS_CHILD | WS_VISIBLE;
item->x = x; item->y = y; item->cx = cx; item->cy = cy; item->id = id;
p = reinterpret_cast<WORD*>(item + 1);
size_t clen = wcslen(cls) + 1; memcpy(p, cls, clen * 2); p += clen;
size_t tlen = wcslen(text) + 1; memcpy(p, text, tlen * 2); p += tlen;
*p++ = 0;
};
addCtrl(SS_LEFT, 6, 6, 208, 10, 0xFFFF, L"Static", L"Preset name:");
addCtrl(ES_AUTOHSCROLL | WS_BORDER | WS_TABSTOP, 6, 18, 208, 14, 1001, L"Edit", L"");
addCtrl(BS_DEFPUSHBUTTON | WS_TABSTOP, 160, 38, 50, 16, IDOK, L"Button", L"Save");
struct NameCtx { wchar_t* buf; int maxLen; };
NameCtx ctx = { nameBuf, 255 };
INT_PTR result = ::DialogBoxIndirectParam(
::GetModuleHandle(nullptr), dlg, GetHwnd(),
[](HWND h, UINT msg, WPARAM wp, LPARAM lp) -> INT_PTR {
if (msg == WM_INITDIALOG) {
::SetWindowLongPtr(h, GWLP_USERDATA, lp);
HFONT hf = (HFONT)::GetStockObject(DEFAULT_GUI_FONT);
::EnumChildWindows(h, [](HWND c, LPARAM f) -> BOOL {
::SendMessage(c, WM_SETFONT, (WPARAM)f, TRUE); return TRUE;
}, (LPARAM)hf);
::SetFocus(::GetDlgItem(h, 1001));
return FALSE;
}
if (msg == WM_COMMAND && LOWORD(wp) == IDOK) {
auto* c = reinterpret_cast<NameCtx*>(::GetWindowLongPtr(h, GWLP_USERDATA));
::GetDlgItemTextW(h, 1001, c->buf, c->maxLen);
::EndDialog(h, IDOK);
return TRUE;
}
if (msg == WM_CLOSE) { ::EndDialog(h, IDCANCEL); return TRUE; }
return FALSE;
}, reinterpret_cast<LPARAM>(&ctx));
if (result != IDOK || nameBuf[0] == L'\0') return;
AddPreset(wide_to_utf8(nameBuf), wide_to_utf8(prompt));
LogMessage(CString(L"Preset saved: ") + nameBuf);
return;
}
if (choice >= ID_REMOVE_BASE && choice < ID_REMOVE_BASE + m_presets.size()) {
int idx = choice - ID_REMOVE_BASE;
std::wstring name = utf8_to_wide_mf(m_presets[idx].name);
RemovePreset(idx);
LogMessage(CString(L"Preset removed: ") + name.c_str());
return;
}
if (choice >= ID_PRESET_BASE && choice < ID_PRESET_BASE + m_presets.size()) {
int idx = choice - ID_PRESET_BASE;
m_lastPrompt = utf8_to_wide_mf(m_presets[idx].prompt);
LogMessage(CString(L"Preset loaded: ") + utf8_to_wide_mf(m_presets[idx].name).c_str());
return;
}
}
LRESULT CMainFrame::WndProc(UINT msg, WPARAM wparam, LPARAM lparam)
{
try {
switch (msg) {
case WM_CLOSE:
// Save dock layout (parent, style, size, floating rect, hidden)
// to Win32++ registry BEFORE OnClose() hides & destroys the window.
SaveDockRegistrySettings(L"Polymech\\pm-image-ui");
// Save window placement to settings.json.
SaveLayout();
return WndProcDefault(msg, wparam, lparam);
case WM_GETMINMAXINFO: return OnGetMinMaxInfo(msg, wparam, lparam);
case UWM_QUEUE_PROGRESS: return OnQueueProgress(wparam, lparam);
case UWM_QUEUE_DONE: return OnQueueDone(wparam, lparam);
case UWM_TRANSFORM_PROGRESS: return OnTransformProgress(wparam, lparam);
case UWM_TRANSFORM_DONE: return OnTransformDone(wparam, lparam);
case UWM_LOG_MESSAGE: return OnLogMessage(wparam);
case UWM_GENERATED_FILE: return OnGeneratedFile(wparam);
case UWM_QUEUE_ITEM_CLICKED: {
int idx = static_cast<int>(wparam);
try {
if (m_pDockQueue) {
CString path = m_pDockQueue->GetQueueContainer().GetListView().GetItemPath(idx);
if (!path.IsEmpty())
m_view.LoadPicture(path.c_str());
UpdateFileInfoForSelection(idx);
}
}
catch (const std::exception& ex) {
LogMessage(CString(L"[Queue click] ") + ex.what());
}
catch (...) {
LogMessage(L"[Queue click] Unknown exception — panel state may be invalid.");
}
return 0;
}
case WM_DROPFILES: {
HDROP hDrop = reinterpret_cast<HDROP>(wparam);
UINT count = DragQueryFileW(hDrop, 0xFFFFFFFF, nullptr, 0);
std::vector<std::wstring> paths;
for (UINT i = 0; i < count; ++i) {
UINT len = DragQueryFileW(hDrop, i, nullptr, 0);
if (!len) continue;
std::wstring w(len + 1, L'\0');
DragQueryFileW(hDrop, i, w.data(), len + 1);
w.resize(len);
paths.push_back(std::move(w));
}
DragFinish(hDrop);
AddFilesToQueue(paths);
return 0;
}
}
return WndProcDefault(msg, wparam, lparam);
}
catch (const CException& e) {
CString s;
s << e.GetText() << L'\n' << e.GetErrorString();
::MessageBox(nullptr, s, L"Error", MB_ICONERROR);
}
catch (const std::exception& e) {
::MessageBoxA(nullptr, e.what(), "Error", MB_ICONERROR);
}
return 0;
}