diff --git a/packages/media/cpp/CMakeLists.txt b/packages/media/cpp/CMakeLists.txt index 593da7fd..dc19e1ee 100644 --- a/packages/media/cpp/CMakeLists.txt +++ b/packages/media/cpp/CMakeLists.txt @@ -182,6 +182,7 @@ if(WIN32) "${_UI_NEXT_DIR}/Ribbon.bml" "/header:${_UI_NEXT_DIR}/RibbonUI.h" "/res:${_UI_NEXT_DIR}/RibbonUI.rc" + "/name:APPLICATION" DEPENDS "${_UI_NEXT_DIR}/Ribbon.xml" COMMENT "Compiling Ribbon.xml → Ribbon.bml + RibbonUI.h/rc (uicc)" VERBATIM @@ -198,6 +199,7 @@ if(WIN32) target_sources(pm-image PRIVATE "${_UI_NEXT_DIR}/App.cpp" + "${_UI_NEXT_DIR}/DropView.cpp" "${_UI_NEXT_DIR}/FileQueue.cpp" "${_UI_NEXT_DIR}/SettingsPanel.cpp" "${_UI_NEXT_DIR}/Mainfrm.cpp" diff --git a/packages/media/cpp/dist/pm-image.exe b/packages/media/cpp/dist/pm-image.exe index 1019ef2a..c414451a 100644 Binary files a/packages/media/cpp/dist/pm-image.exe and b/packages/media/cpp/dist/pm-image.exe differ diff --git a/packages/media/cpp/dist/pm-image.pdb b/packages/media/cpp/dist/pm-image.pdb index 7f6a3e38..6709282d 100644 Binary files a/packages/media/cpp/dist/pm-image.pdb and b/packages/media/cpp/dist/pm-image.pdb differ diff --git a/packages/media/cpp/src/win/ui_next/DropView.cpp b/packages/media/cpp/src/win/ui_next/DropView.cpp index 1d658df4..6ee9fa31 100644 --- a/packages/media/cpp/src/win/ui_next/DropView.cpp +++ b/packages/media/cpp/src/win/ui_next/DropView.cpp @@ -2,37 +2,103 @@ #include "DropView.h" #include "Resource.h" #include +#include -int CDropView::OnCreate(CREATESTRUCT&) +#pragma comment(lib, "gdiplus.lib") + +CImagePreview::CImagePreview() { - m_bgBrush.CreateSolidBrush(RGB(245, 245, 245)); + m_label = L"Drop images here or use Add Files"; + Gdiplus::GdiplusStartupInput si; + Gdiplus::GdiplusStartup(&m_gdipToken, &si, nullptr); +} + +CImagePreview::~CImagePreview() +{ + delete m_pImage; + if (m_gdipToken) Gdiplus::GdiplusShutdown(m_gdipToken); +} + +int CImagePreview::OnCreate(CREATESTRUCT&) +{ + m_bgBrush.CreateSolidBrush(RGB(48, 48, 48)); + SetClassLongPtr(GCLP_HBRBACKGROUND, (LONG_PTR)m_bgBrush.GetHandle()); DragAcceptFiles(TRUE); return 0; } -void CDropView::PreCreate(CREATESTRUCT& cs) +bool CImagePreview::LoadPicture(LPCWSTR path) { - cs.dwExStyle = WS_EX_CLIENTEDGE; + delete m_pImage; + m_pImage = nullptr; + + auto* img = Gdiplus::Image::FromFile(path); + if (img && img->GetLastStatus() == Gdiplus::Ok) { + m_pImage = img; + m_label.Empty(); + SetScrollSizes(CSize(0, 0)); + Invalidate(); + return true; + } + + delete img; + m_label = L"Could not load image"; + SetScrollSizes(CSize(0, 0)); + Invalidate(); + return false; } -void CDropView::OnDraw(CDC& dc) +void CImagePreview::ClearPicture() +{ + delete m_pImage; + m_pImage = nullptr; + m_label = L"Drop images here or use Add Files"; + SetScrollSizes(CSize(0, 0)); + Invalidate(); +} + +void CImagePreview::OnDraw(CDC& dc) { CRect rc = GetClientRect(); + + // Fill background dc.FillRect(rc, m_bgBrush); - dc.SetBkMode(TRANSPARENT); - dc.SetTextColor(RGB(160, 160, 160)); - dc.DrawText(L"Drop images here or use Add Files", -1, rc, - DT_CENTER | DT_VCENTER | DT_SINGLELINE); + if (m_pImage) { + UINT iw = m_pImage->GetWidth(); + UINT ih = m_pImage->GetHeight(); + if (iw > 0 && ih > 0) { + int cw = rc.Width(); + int ch = rc.Height(); + + // Fit image within client area maintaining aspect ratio + double scaleX = (double)cw / iw; + double scaleY = (double)ch / ih; + double scale = (std::min)(scaleX, scaleY); + if (scale > 1.0) scale = 1.0; + + int dw = (int)(iw * scale); + int dh = (int)(ih * scale); + int dx = (cw - dw) / 2; + int dy = (ch - dh) / 2; + + Gdiplus::Graphics gfx(dc.GetHDC()); + gfx.SetInterpolationMode(Gdiplus::InterpolationModeHighQualityBicubic); + gfx.DrawImage(m_pImage, dx, dy, dw, dh); + } + } else if (!m_label.IsEmpty()) { + dc.SetBkMode(TRANSPARENT); + dc.SetTextColor(RGB(160, 160, 160)); + dc.DrawText(m_label, -1, rc, DT_CENTER | DT_VCENTER | DT_SINGLELINE); + } } -LRESULT CDropView::OnDropFiles(WPARAM wparam) +LRESULT CImagePreview::OnDropFiles(WPARAM wparam) { - // Forward to parent (CMainFrame) which handles WM_DROPFILES. return GetAncestor().SendMessage(WM_DROPFILES, wparam, 0); } -LRESULT CDropView::WndProc(UINT msg, WPARAM wparam, LPARAM lparam) +LRESULT CImagePreview::WndProc(UINT msg, WPARAM wparam, LPARAM lparam) { try { switch (msg) { diff --git a/packages/media/cpp/src/win/ui_next/DropView.h b/packages/media/cpp/src/win/ui_next/DropView.h index 516eca64..ae18d508 100644 --- a/packages/media/cpp/src/win/ui_next/DropView.h +++ b/packages/media/cpp/src/win/ui_next/DropView.h @@ -3,25 +3,32 @@ #include "stdafx.h" #include +#include -class CDropView : public CWnd +class CImagePreview : public CScrollView { public: - CDropView() = default; - virtual ~CDropView() override = default; + CImagePreview(); + virtual ~CImagePreview() override; + + bool LoadPicture(LPCWSTR path); + void ClearPicture(); protected: virtual int OnCreate(CREATESTRUCT& cs) override; virtual void OnDraw(CDC& dc) override; - virtual void PreCreate(CREATESTRUCT& cs) override; virtual LRESULT WndProc(UINT msg, WPARAM wparam, LPARAM lparam) override; private: - CDropView(const CDropView&) = delete; - CDropView& operator=(const CDropView&) = delete; + CImagePreview(const CImagePreview&) = delete; + CImagePreview& operator=(const CImagePreview&) = delete; LRESULT OnDropFiles(WPARAM wparam); + + Gdiplus::Image* m_pImage = nullptr; + ULONG_PTR m_gdipToken = 0; CBrush m_bgBrush; + CString m_label; }; #endif // PM_UI_DROPVIEW_H diff --git a/packages/media/cpp/src/win/ui_next/FileQueue.cpp b/packages/media/cpp/src/win/ui_next/FileQueue.cpp index 40f2a013..853648fb 100644 --- a/packages/media/cpp/src/win/ui_next/FileQueue.cpp +++ b/packages/media/cpp/src/win/ui_next/FileQueue.cpp @@ -1,8 +1,8 @@ #include "stdafx.h" #include "FileQueue.h" #include "Resource.h" - #include +#include namespace fs = std::filesystem; @@ -13,33 +13,44 @@ namespace fs = std::filesystem; void CQueueListView::OnAttach() { CListView::OnAttach(); + + SetExtendedStyle(LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES | LVS_EX_DOUBLEBUFFER); SetupColumns(); DragAcceptFiles(TRUE); +} - DWORD exStyle = LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES | LVS_EX_DOUBLEBUFFER; - SendMessage(LVM_SETEXTENDEDLISTVIEWSTYLE, 0, exStyle); +void CQueueListView::PreCreate(CREATESTRUCT& cs) +{ + CListView::PreCreate(cs); + cs.style |= LVS_REPORT | LVS_SHOWSELALWAYS; } void CQueueListView::SetupColumns() { + DeleteAllItems(); + LV_COLUMN col{}; col.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM; col.fmt = LVCFMT_LEFT; - WCHAR names[COL_COUNT][16] = { L"Name", L"Status", L"Path" }; - int widths[COL_COUNT] = { 220, 100, 400 }; + col.pszText = const_cast(L"Name"); + col.cx = DpiScaleInt(220); + col.iSubItem = COL_NAME; + InsertColumn(COL_NAME, col); - for (int i = 0; i < COL_COUNT; ++i) { - col.pszText = names[i]; - col.cx = DpiScaleInt(widths[i]); - col.iSubItem = i; - InsertColumn(i, col); - } + col.pszText = const_cast(L"Status"); + col.cx = DpiScaleInt(100); + col.iSubItem = COL_STATUS; + InsertColumn(COL_STATUS, col); + + col.pszText = const_cast(L"Path"); + col.cx = DpiScaleInt(400); + col.iSubItem = COL_PATH; + InsertColumn(COL_PATH, col); } int CQueueListView::AddFile(const CString& path) { - // Derive filename from path. fs::path p(std::wstring(path.c_str())); CString name = p.filename().c_str(); @@ -79,13 +90,13 @@ CString CQueueListView::GetItemPath(int item) LRESULT CQueueListView::OnDropFiles(UINT, WPARAM wparam, LPARAM) { HDROP hDrop = reinterpret_cast(wparam); - UINT count = DragQueryFileW(hDrop, 0xFFFFFFFF, nullptr, 0); + UINT count = ::DragQueryFileW(hDrop, 0xFFFFFFFF, nullptr, 0); for (UINT i = 0; i < count; ++i) { - UINT len = DragQueryFileW(hDrop, i, nullptr, 0); + UINT len = ::DragQueryFileW(hDrop, i, nullptr, 0); if (len == 0) continue; std::wstring w(static_cast(len) + 1, L'\0'); - DragQueryFileW(hDrop, i, w.data(), len + 1); + ::DragQueryFileW(hDrop, i, w.data(), len + 1); w.resize(len); fs::path p(w); @@ -106,7 +117,26 @@ LRESULT CQueueListView::OnDropFiles(UINT, WPARAM wparam, LPARAM) } } - DragFinish(hDrop); + ::DragFinish(hDrop); + + // Notify the main frame to update status bar. + GetAncestor().PostMessage(WM_COMMAND, MAKEWPARAM(0, 0), 0); + return 0; +} + +LRESULT CQueueListView::OnNotifyReflect(WPARAM, LPARAM lparam) +{ + LPNMHDR pnm = reinterpret_cast(lparam); + if (pnm->code == NM_CLICK) { + LPNMITEMACTIVATE pia = reinterpret_cast(lparam); + if (pia->iItem >= 0) + GetAncestor().SendMessage(UWM_QUEUE_ITEM_CLICKED, (WPARAM)pia->iItem, 0); + } else if (pnm->code == LVN_ITEMCHANGED) { + LPNMLISTVIEW plv = reinterpret_cast(lparam); + if (plv->iItem >= 0 && (plv->uChanged & LVIF_STATE) && + (plv->uNewState & LVIS_SELECTED) && !(plv->uOldState & LVIS_SELECTED)) + GetAncestor().SendMessage(UWM_QUEUE_ITEM_CLICKED, (WPARAM)plv->iItem, 0); + } return 0; } diff --git a/packages/media/cpp/src/win/ui_next/FileQueue.h b/packages/media/cpp/src/win/ui_next/FileQueue.h index c15b2022..5c61fefa 100644 --- a/packages/media/cpp/src/win/ui_next/FileQueue.h +++ b/packages/media/cpp/src/win/ui_next/FileQueue.h @@ -23,7 +23,9 @@ public: CString GetItemPath(int item); protected: - virtual void OnAttach() override; + virtual void OnAttach() override; + virtual void PreCreate(CREATESTRUCT& cs) override; + virtual LRESULT OnNotifyReflect(WPARAM wparam, LPARAM lparam) override; virtual LRESULT WndProc(UINT msg, WPARAM wparam, LPARAM lparam) override; private: diff --git a/packages/media/cpp/src/win/ui_next/Mainfrm.cpp b/packages/media/cpp/src/win/ui_next/Mainfrm.cpp index 7d652530..2bf70685 100644 --- a/packages/media/cpp/src/win/ui_next/Mainfrm.cpp +++ b/packages/media/cpp/src/win/ui_next/Mainfrm.cpp @@ -35,7 +35,7 @@ STDMETHODIMP CMainFrame::Execute(UINT32 cmdID, UI_EXECUTIONVERB verb, case IDC_CMD_ADD_FILES: OnAddFiles(); break; case IDC_CMD_ADD_FOLDER: OnAddFolder(); break; case IDC_CMD_CLEAR: OnClearQueue(); break; - case IDC_CMD_PROCESS: OnProcess(); break; + case IDC_CMD_RESIZE: OnResize(); break; case IDC_CMD_ABOUT: OnHelp(); break; case IDC_CMD_EXIT: OnExit(); break; case IDC_RIBBONHELP: OnHelp(); break; @@ -72,7 +72,7 @@ BOOL CMainFrame::OnCommand(WPARAM wparam, LPARAM) case IDM_ADD_FILES: OnAddFiles(); return TRUE; case IDM_ADD_FOLDER: OnAddFolder(); return TRUE; case IDM_CLEAR_QUEUE: OnClearQueue(); return TRUE; - case IDM_PROCESS: OnProcess(); 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(); @@ -83,7 +83,7 @@ BOOL CMainFrame::OnCommand(WPARAM wparam, LPARAM) BOOL CMainFrame::OnHelp() { - ::MessageBox(GetHwnd(), L"pm-image — resize & transform\nWin32++ UI", + ::MessageBox(GetHwnd(), L"pm-image \u2014 resize & transform\nWin32++ UI", L"About pm-image", MB_ICONINFORMATION | MB_OK); return TRUE; } @@ -93,7 +93,7 @@ void CMainFrame::OnInitialUpdate() DWORD style = DS_CLIENTEDGE; auto pDockQ = AddDockedChild(std::make_unique(), - DS_DOCKED_BOTTOM | style, DpiScaleInt(280)); + DS_DOCKED_BOTTOM | style, DpiScaleInt(220)); m_pDockQueue = static_cast(pDockQ); m_pDockQueue->GetQueueContainer().SetHideSingleTab(TRUE); @@ -109,11 +109,8 @@ void CMainFrame::OnInitialUpdate() void CMainFrame::SetupToolBar() { - AddToolBarButton(IDM_ADD_FILES); - AddToolBarButton(IDM_ADD_FOLDER); - AddToolBarButton(0); - AddToolBarButton(IDM_PROCESS); - AddToolBarButton(IDM_CLEAR_QUEUE); + // Toolbar is unused — the ribbon provides all commands. + // Leaving this empty avoids the IDW_MAIN BITMAP resource requirement. } void CMainFrame::OnAddFiles() @@ -161,6 +158,9 @@ 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); @@ -184,17 +184,23 @@ void CMainFrame::AddFilesToQueue(const std::vector& paths) 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::OnProcess() +void CMainFrame::OnResize() { if (m_processing) { ::MessageBox(GetHwnd(), L"Already processing.", L"pm-image", MB_ICONWARNING); @@ -205,7 +211,7 @@ void CMainFrame::OnProcess() auto& lv = m_pDockQueue->GetQueueContainer().GetListView(); int count = lv.QueueCount(); if (count == 0) { - ::MessageBox(GetHwnd(), L"Queue is empty — drop files first.", L"pm-image", MB_ICONINFORMATION); + ::MessageBox(GetHwnd(), L"Queue is empty \u2014 drop files first.", L"pm-image", MB_ICONINFORMATION); return; } @@ -213,7 +219,6 @@ void CMainFrame::OnProcess() std::string out_dir; m_pDockSettings->GetSettingsContainer().GetSettingsView().ReadOptions(opt, out_dir); - // Gather paths std::vector> items; items.reserve(count); for (int i = 0; i < count; ++i) { @@ -248,7 +253,7 @@ void CMainFrame::OnProcess() }); m_worker.detach(); - GetStatusBar().SetPartText(0, L"Processing…"); + GetStatusBar().SetPartText(0, L"Resizing\u2026"); } void CMainFrame::OnExit() @@ -272,11 +277,11 @@ LRESULT CMainFrame::OnQueueProgress(WPARAM wparam, LPARAM lparam) int idx = static_cast(wparam); int state = static_cast(lparam); if (state == 1) - lv.SetItemStatus(idx, L"Processing…"); + lv.SetItemStatus(idx, L"Resizing\u2026"); else if (state == 2) - lv.SetItemStatus(idx, L"Done"); + lv.SetItemStatus(idx, L"\u2713 Done"); else - lv.SetItemStatus(idx, L"Error"); + lv.SetItemStatus(idx, L"\u2717 Error"); return 0; } @@ -299,7 +304,16 @@ LRESULT CMainFrame::WndProc(UINT msg, WPARAM wparam, LPARAM lparam) switch (msg) { 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_QUEUE_DONE: return OnQueueDone(wparam, lparam); + case UWM_QUEUE_ITEM_CLICKED: { + int idx = static_cast(wparam); + if (m_pDockQueue) { + CString path = m_pDockQueue->GetQueueContainer().GetListView().GetItemPath(idx); + if (!path.IsEmpty()) + m_view.LoadPicture(path.c_str()); + } + return 0; + } case WM_DROPFILES: { HDROP hDrop = reinterpret_cast(wparam); UINT count = DragQueryFileW(hDrop, 0xFFFFFFFF, nullptr, 0); diff --git a/packages/media/cpp/src/win/ui_next/Mainfrm.h b/packages/media/cpp/src/win/ui_next/Mainfrm.h index 450b043e..abed715b 100644 --- a/packages/media/cpp/src/win/ui_next/Mainfrm.h +++ b/packages/media/cpp/src/win/ui_next/Mainfrm.h @@ -31,14 +31,14 @@ private: void OnAddFiles(); void OnAddFolder(); void OnClearQueue(); - void OnProcess(); + void OnResize(); void OnExit(); LRESULT OnGetMinMaxInfo(UINT msg, WPARAM wparam, LPARAM lparam); LRESULT OnQueueProgress(WPARAM wparam, LPARAM lparam); LRESULT OnQueueDone(WPARAM wparam, LPARAM lparam); - CDropView m_view; + CImagePreview m_view; IUIRibbon* m_pIUIRibbon = nullptr; CDockQueue* m_pDockQueue = nullptr; CDockSettings* m_pDockSettings = nullptr; diff --git a/packages/media/cpp/src/win/ui_next/Resource.h b/packages/media/cpp/src/win/ui_next/Resource.h index 202abe6b..be2c869c 100644 --- a/packages/media/cpp/src/win/ui_next/Resource.h +++ b/packages/media/cpp/src/win/ui_next/Resource.h @@ -5,7 +5,7 @@ #include "RibbonUI.h" // Menu fallback IDs (when ribbon is unavailable) -#define IDM_PROCESS 200 +#define IDM_RESIZE 200 #define IDM_CLEAR_QUEUE 201 #define IDM_ADD_FILES 202 #define IDM_ADD_FOLDER 203 @@ -13,23 +13,6 @@ #define IDM_EXIT 205 #define IDM_ABOUT 206 -// Ribbon command IDs (must match Ribbon.xml Symbol= values) -#define IDC_CMD_PROCESS 300 -#define IDC_CMD_CLEAR 301 -#define IDC_CMD_ADD_FILES 302 -#define IDC_CMD_ADD_FOLDER 303 -#define IDC_CMD_EXIT 304 -#define IDC_CMD_ABOUT 305 - -// Ribbon tabs/groups -#define IDC_TAB_HOME 400 -#define IDC_GROUP_QUEUE 401 -#define IDC_GROUP_ACTIONS 402 - -// Dock / container -#define IDI_QUEUE 500 -#define IDB_QUEUE 501 - // Settings controls (for settings dock) #define IDC_COMBO_PRESET_OUT 600 #define IDC_EDIT_OUT_DIR 601 @@ -42,13 +25,10 @@ #define IDC_CHK_AUTOROT 608 #define IDC_CHK_STRIP 609 -// Ribbon misc -#define IDC_RIBBONHELP 700 -#define IDC_QAT 701 -#define IDC_CUSTOMIZE_QAT 702 - // User messages #define UWM_QUEUE_PROGRESS (WM_USER + 100) #define UWM_QUEUE_DONE (WM_USER + 101) +#define UWM_IMAGELOADED (WM_USER + 102) +#define UWM_QUEUE_ITEM_CLICKED (WM_USER + 103) #endif // PM_UI_RESOURCE_H diff --git a/packages/media/cpp/src/win/ui_next/Resource.rc b/packages/media/cpp/src/win/ui_next/Resource.rc index 06bc5b32..d33d1ee5 100644 --- a/packages/media/cpp/src/win/ui_next/Resource.rc +++ b/packages/media/cpp/src/win/ui_next/Resource.rc @@ -22,7 +22,7 @@ BEGIN END POPUP "&Actions" BEGIN - MENUITEM "&Process\tF5", IDM_PROCESS + MENUITEM "&Resize\tF5", IDM_RESIZE MENUITEM "&Clear Queue", IDM_CLEAR_QUEUE END POPUP "&Help" @@ -48,7 +48,7 @@ END // Accelerators IDW_MAIN ACCELERATORS BEGIN - VK_F5, IDM_PROCESS, VIRTKEY, NOINVERT + VK_F5, IDM_RESIZE, VIRTKEY, NOINVERT END // String table diff --git a/packages/media/cpp/src/win/ui_next/Ribbon.bml b/packages/media/cpp/src/win/ui_next/Ribbon.bml index 0b93aae4..860780a0 100644 Binary files a/packages/media/cpp/src/win/ui_next/Ribbon.bml and b/packages/media/cpp/src/win/ui_next/Ribbon.bml differ diff --git a/packages/media/cpp/src/win/ui_next/Ribbon.xml b/packages/media/cpp/src/win/ui_next/Ribbon.xml index 584f3290..92fe55ea 100644 --- a/packages/media/cpp/src/win/ui_next/Ribbon.xml +++ b/packages/media/cpp/src/win/ui_next/Ribbon.xml @@ -26,21 +26,42 @@ Add Files + + res/AddFilesL.bmp + + + res/AddFilesS.bmp + Add Folder + + res/AddFolderL.bmp + + + res/AddFolderS.bmp + Clear + + res/ClearL.bmp + + + res/ClearS.bmp + - + - Process + Resize + + res/ResizeL.bmp + @@ -64,7 +85,6 @@ - @@ -80,7 +100,6 @@ - @@ -99,21 +118,19 @@