#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 #include #include #include #include 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(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(); case DOCK_ID_LOG: return std::make_unique(); case DOCK_ID_SETTINGS: return std::make_unique(); case DOCK_ID_GENPREVIEW: return std::make_unique(); case DOCK_ID_FILEINFO: return std::make_unique(); } 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(), DS_DOCKED_BOTTOM, DpiScaleInt(220), DOCK_ID_QUEUE); m_pDockQueue = static_cast(pDockQ); auto pDockL = m_pDockQueue->AddDockedChild(std::make_unique(), DS_DOCKED_RIGHT, DpiScaleInt(360), DOCK_ID_LOG); m_pDockLog = static_cast(pDockL); auto pDockS = AddDockedChild(std::make_unique(), DS_DOCKED_RIGHT, DpiScaleInt(280), DOCK_ID_SETTINGS); m_pDockSettings = static_cast(pDockS); auto pDockGP = m_pDockSettings->AddDockedChild(std::make_unique(), DS_DOCKED_BOTTOM, DpiScaleInt(200), DOCK_ID_GENPREVIEW); m_pDockGenPreview = static_cast(pDockGP); auto pDockFI = m_pDockGenPreview->AddDockedChild(std::make_unique(), DS_DOCKED_BOTTOM, DpiScaleInt(160), DOCK_ID_FILEINFO); m_pDockFileInfo = static_cast(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 (GetDockFromID(DOCK_ID_QUEUE)); m_pDockLog = static_cast (GetDockFromID(DOCK_ID_LOG)); m_pDockSettings = static_cast (GetDockFromID(DOCK_ID_SETTINGS)); m_pDockGenPreview = static_cast(GetDockFromID(DOCK_ID_GENPREVIEW)); m_pDockFileInfo = static_cast (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 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 v = { path }; AddFilesToQueue(v); } CoTaskMemFree(pidl); } void CMainFrame::AddFilesToQueue(const std::vector& 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(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> 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 CMainFrame::GetSelectedQueueItems() { std::vector 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(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(::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(::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(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(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(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(p) % 4) p++; DLGITEMTEMPLATE* item = reinterpret_cast(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(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(&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> 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(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(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(wparam); int state = static_cast(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(wparam); int fail = static_cast(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(wparam); int state = static_cast(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(wparam); int fail = static_cast(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(wparam); if (ws) { LogMessage(CString(ws)); delete[] ws; } return 0; } LRESULT CMainFrame::OnGeneratedFile(WPARAM wparam) { auto* pair = reinterpret_cast*>(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(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(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(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(p) % 4) p++; DLGITEMTEMPLATE* item = reinterpret_cast(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(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(::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(&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(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(wparam); UINT count = DragQueryFileW(hDrop, 0xFFFFFFFF, nullptr, 0); std::vector 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; }