rom-properties/src/win32/RP_ShellPropSheetExt.cpp
David Korth cff90b5309 [win32] RP_ShellPropSheetExt: Initial scrolling for data widgets.
Currently only works if multiple tabs are displayed.

The child dialog control has WS_VSCROLL and the scroll information is
set after all the tabs are set up.

SubtabDlgProc:
- WM_VSCROLL: Scroll the window as necessary, using the description
  label height as the line height and one visible page worth as the
  page height.
- Call DefSubclassProc() instead of simply returning FALSE.

This partially fixes issue #204: Windows UI has no scroll bar
Reported by @InternalLoss.
2020-09-15 20:06:53 -04:00

4332 lines
127 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/***************************************************************************
* ROM Properties Page shell extension. (Win32) *
* RP_ShellPropSheetExt.cpp: IShellPropSheetExt implementation. *
* *
* Copyright (c) 2016-2020 by David Korth. *
* SPDX-License-Identifier: GPL-2.0-or-later *
***************************************************************************/
// References:
// - http://www.codeproject.com/Articles/338268/COM-in-C
// - https://code.msdn.microsoft.com/windowsapps/CppShellExtPropSheetHandler-d93b49b7
// - https://msdn.microsoft.com/en-us/library/ms677109(v=vs.85).aspx
#include "stdafx.h"
#include "RP_ShellPropSheetExt.hpp"
#include "RpImageWin32.hpp"
#include "res/resource.h"
#include "DragImageLabel.hpp"
#include "FontHandler.hpp"
#include "MessageWidget.hpp"
// libwin32common
#include "libwin32common/AutoGetDC.hpp"
#include "libwin32common/SubclassWindow.h"
using LibWin32Common::AutoGetDC;
using LibWin32Common::WTSSessionNotification;
// NOTE: Using "RomDataView" for the libi18n context, since that
// matches what's used for the KDE and GTK+ frontends.
// librpbase, librpfile, librptexture, libromdata
#include "librpbase/RomFields.hpp"
#include "librpbase/SystemRegion.hpp"
#include "librpbase/TextOut.hpp"
#include "librpbase/img/RpPng.hpp"
#include "librpfile/win32/RpFile_windres.hpp"
using namespace LibRpBase;
using namespace LibRpFile;
using LibRpTexture::rp_image;
using LibRomData::RomDataFactory;
// C++ STL classes.
#include <fstream>
#include <sstream>
using std::array;
using std::ofstream;
using std::ostringstream;
using std::set;
using std::string;
using std::unique_ptr;
using std::unordered_map;
using std::unordered_set;
using std::wstring;
using std::vector;
// GDI+ scoped token.
#include "librptexture/img/GdiplusHelper.hpp"
// Windows 10 and later
#ifndef DATE_MONTHDAY
# define DATE_MONTHDAY 0x00000080
#endif /* DATE_MONTHDAY */
// CLSID
const CLSID CLSID_RP_ShellPropSheetExt =
{0x2443C158, 0xDF7C, 0x4352, {0xB4, 0x35, 0xBC, 0x9F, 0x88, 0x5F, 0xFD, 0x52}};
// Control base IDs.
#define IDC_STATIC_BANNER 0x0100
#define IDC_STATIC_ICON 0x0101
#define IDC_TAB_WIDGET 0x0102
#define IDC_CBO_LANGUAGE 0x0103
#define IDC_MESSAGE_WIDGET 0x0104
#define IDC_TAB_PAGE(idx) (0x0200 + (idx))
#define IDC_STATIC_DESC(idx) (0x1000 + (idx))
#define IDC_RFT_STRING(idx) (0x1400 + (idx))
#define IDC_RFT_LISTDATA(idx) (0x1800 + (idx))
// Date/Time acts like a string widget internally.
#define IDC_RFT_DATETIME(idx) IDC_RFT_STRING(idx)
// Bitfield is last due to multiple controls per field.
#define IDC_RFT_BITFIELD(idx, bit) (0x7000 + ((idx) * 32) + (bit))
// "Options" menu item.
#define IDM_OPTIONS_MENU_BASE 0x8000
#define IDM_OPTIONS_MENU_EXPORT_TEXT (IDM_OPTIONS_MENU_BASE - 1)
#define IDM_OPTIONS_MENU_EXPORT_JSON (IDM_OPTIONS_MENU_BASE - 2)
#define IDM_OPTIONS_MENU_COPY_TEXT (IDM_OPTIONS_MENU_BASE - 3)
#define IDM_OPTIONS_MENU_COPY_JSON (IDM_OPTIONS_MENU_BASE - 4)
/** RP_ShellPropSheetExt_Private **/
// Workaround for RP_D() expecting the no-underscore naming convention.
#define RP_ShellPropSheetExtPrivate RP_ShellPropSheetExt_Private
class RP_ShellPropSheetExt_Private
{
public:
explicit RP_ShellPropSheetExt_Private(RP_ShellPropSheetExt *q, string &&filename);
~RP_ShellPropSheetExt_Private();
private:
RP_DISABLE_COPY(RP_ShellPropSheetExt_Private)
private:
RP_ShellPropSheetExt *const q_ptr;
public:
// Property for "D pointer".
// This points to the RP_ShellPropSheetExt_Private object.
static const TCHAR D_PTR_PROP[];
// Property for "tab pointer".
// This points to the RP_ShellPropSheetExt_Private::tab object.
static const TCHAR TAB_PTR_PROP[];
public:
// ROM filename.
string filename;
// ROM data. (Not opened until the properties tab is shown.)
RomData *romData;
// Useful window handles.
HWND hDlgSheet; // Property sheet.
HWND hBtnOptions; // Options button.
HMENU hMenuOptions; // Options menu.
tstring ts_prevExportDir;
// Fonts.
HFONT hFontDlg; // Main dialog font.
HFONT hFontBold; // Bold font.
FontHandler fontHandler;
// Controls with Warning formatting.
unordered_set<HWND> setWarningControls;
// ListView controls. (for toggling LVS_EX_DOUBLEBUFFER)
vector<HWND> hwndListViewControls;
// GDI+ token.
ScopedGdiplus gdipScope;
// Header row widgets.
HWND lblSysInfo;
POINT ptSysInfo;
RECT rectHeader;
// wtsapi32.dll for Remote Desktop status. (WinXP and later)
WTSSessionNotification wts;
// Alternate row color.
COLORREF colorAltRow;
bool isFullyInit; // True if the window is fully initialized.
// ListView data struct.
// NOTE: Not making vImageList a pointer, since that adds
// significantly more complexity.
struct LvData_t {
vector<vector<tstring> > vvStr; // String data.
vector<int> vImageList; // ImageList indexes.
uint32_t checkboxes; // Checkboxes.
bool hasCheckboxes; // True if checkboxes are valid.
// For RFT_LISTDATA_MULTI only!
HWND hListView;
const RomFields::Field *pField;
LvData_t()
: checkboxes(0), hasCheckboxes(false)
, hListView(nullptr), pField(nullptr) { }
};
// ListView data.
// - Key: ListView dialog ID
// - Value: LvData_t.
unordered_map<uint16_t, LvData_t> map_lvData;
/**
* ListView GetDispInfo function.
* @param plvdi [in/out] NMLVDISPINFO
* @return TRUE if handled; FALSE if not.
*/
inline BOOL ListView_GetDispInfo(NMLVDISPINFO *plvdi);
/**
* ListView CustomDraw function.
* @param plvcd [in/out] NMLVCUSTOMDRAW
* @return Return value.
*/
inline int ListView_CustomDraw(NMLVCUSTOMDRAW *plvcd);
// Banner and icon.
DragImageLabel *lblBanner;
DragImageLabel *lblIcon;
// Is the UI locale right-to-left?
// If so, this will be set to WS_EX_LAYOUTRTL.
DWORD dwExStyleRTL;
// Tab layout.
HWND tabWidget;
struct tab {
HWND hDlg; // Tab child dialog.
HWND lblCredits; // Credits label.
POINT curPt; // Current point.
int scrollPos; // Scrolling position.
tab() : hDlg(nullptr), lblCredits(nullptr), scrollPos(0) {
curPt.x = 0; curPt.y = 0;
}
};
vector<tab> tabs;
int curTabIndex;
// Sizes.
int lblDescHeight; // Description label height.
SIZE dlgSize; // Visible dialog size.
// MessageWidget for ROM operation notifications.
HWND hMessageWidget;
int iTabHeightOrig;
// Multi-language functionality.
uint32_t def_lc; // Default language code from RomFields.
set<uint32_t> set_lc; // Set of supported language codes.
HWND cboLanguage;
HIMAGELIST himglFlags;
/**
* Get the selected language code.
* @return Selected language code, or 0 for none (default).
*/
inline uint32_t sel_lc(void) const
{
uint32_t lc = 0;
if (!cboLanguage) {
// No language dropdown...
return lc;
}
const int sel_idx = ComboBox_GetCurSel(cboLanguage);
if (sel_idx >= 0) {
lc = static_cast<uint32_t>(ComboBox_GetItemData(cboLanguage, sel_idx));
}
return lc;
}
// RFT_STRING_MULTI value labels.
typedef std::pair<HWND, const RomFields::Field*> Data_StringMulti_t;
vector<Data_StringMulti_t> vecStringMulti;
public:
/**
* Load the banner and icon as HBITMAPs.
*
* This function should bee called on startup and if
* the window's background color changes.
*
* NOTE: The HWND isn't needed here, since this function
* doesn't touch the dialog at all.
*/
void loadImages(void);
private:
/**
* Check if we need to use WS_EX_LAYOUTRTL.
* TODO: Cache this value?
* @return WS_EX_LAYOUTRTL if process is RTL; otherwise 0.
*/
static inline DWORD checkLayoutRTL(void)
{
// Set WS_EX_LAYOUTRTL if the process is RTL.
DWORD dwDefaultLayout;
const BOOL bRet = GetProcessDefaultLayout(&dwDefaultLayout);
return (unlikely(bRet && dwDefaultLayout == LAYOUT_RTL))
? WS_EX_LAYOUTRTL
: 0;
}
/**
* Rescale an image to be as close to the required size as possible.
* @param req_sz [in] Required size.
* @param sz [in/out] Image size.
* @return True if nearest-neighbor scaling should be used (size was kept the same or enlarged); false if shrunken (so use interpolation).
*/
static bool rescaleImage(const SIZE &req_sz, SIZE &sz);
/**
* Create the header row.
* @param hDlg [in] Dialog window.
* @param pt_start [in] Starting position, in pixels.
* @param size [in] Width and height for a full-width single line label.
* @return Row height, in pixels.
*/
int createHeaderRow(HWND hDlg, const POINT &pt_start, const SIZE &size);
/**
* Initialize a string field. (Also used for Date/Time.)
* @param hDlg [in] Parent dialog window. (for dialog unit mapping)
* @param hWndTab [in] Tab window. (for the actual control)
* @param pt_start [in] Starting position, in pixels.
* @param size [in] Width and height for a single line label.
* @param field [in] RomFields::Field
* @param fieldIdx [in] Field index
* @param str [in,opt] String data. (If nullptr, field data is used.)
* @param pOutHWND [out,opt] Retrieves the control's HWND.
* @return Field height, in pixels.
*/
int initString(_In_ HWND hDlg, _In_ HWND hWndTab,
_In_ const POINT &pt_start, _In_ const SIZE &size,
_In_ const RomFields::Field &field, _In_ int fieldIdx,
_In_ LPCTSTR str = nullptr, _Outptr_opt_ HWND *pOutHWND = nullptr);
/**
* Initialize a bitfield layout.
* @param hDlg [in] Parent dialog window. (for dialog unit mapping)
* @param hWndTab [in] Tab window. (for the actual control)
* @param pt_start [in] Starting position, in pixels.
* @param field [in] RomFields::Field
* @param fieldIdx [in] Field index
* @return Field height, in pixels.
*/
int initBitfield(HWND hDlg, HWND hWndTab,
const POINT &pt_start,
const RomFields::Field &field, int fieldIdx);
/**
* Measure the width of a ListData string.
* This function handles newlines.
* @param hDC [in] HDC for text measurement.
* @param tstr [in] String to measure.
* @param pNlCount [out,opt] Newline count.
* @return Width.
*/
static int measureListDataString(HDC hDC, const tstring &tstr, int *pNlCount = nullptr);
/**
* Initialize a ListData field.
* @param hDlg [in] Parent dialog window. (for dialog unit mapping)
* @param hWndTab [in] Tab window. (for the actual control)
* @param pt_start [in] Starting position, in pixels.
* @param size [in] Width and height for a default ListView.
* @param doResize [in] If true, resize the ListView to accomodate rows_visible.
* @param field [in] RomFields::Field
* @param fieldIdx [in] Field index
* @return Field height, in pixels.
*/
int initListData(HWND hDlg, HWND hWndTab,
const POINT &pt_start, const SIZE &size, bool doResize,
const RomFields::Field &field, int fieldIdx);
/**
* Initialize a Date/Time field.
* This function internally calls initString().
* @param hDlg [in] Parent dialog window. (for dialog unit mapping)
* @param hWndTab [in] Tab window. (for the actual control)
* @param pt_start [in] Starting position, in pixels.
* @param size [in] Width and height for a single line label.
* @param field [in] RomFields::Field
* @param fieldIdx [in] Field index
* @return Field height, in pixels.
*/
int initDateTime(HWND hDlg, HWND hWndTab,
const POINT &pt_start, const SIZE &size,
const RomFields::Field &field, int fieldIdx);
/**
* Initialize an Age Ratings field.
* This function internally calls initString().
* @param hDlg [in] Parent dialog window. (for dialog unit mapping)
* @param hWndTab [in] Tab window. (for the actual control)
* @param pt_start [in] Starting position, in pixels.
* @param size [in] Width and height for a single line label.
* @param field [in] RomFields::Field
* @param fieldIdx [in] Field index
* @return Field height, in pixels.
*/
int initAgeRatings(HWND hDlg, HWND hWndTab,
const POINT &pt_start, const SIZE &size,
const RomFields::Field &field, int fieldIdx);
/**
* Initialize a Dimensions field.
* This function internally calls initString().
* @param hDlg [in] Parent dialog window. (for dialog unit mapping)
* @param hWndTab [in] Tab window. (for the actual control)
* @param pt_start [in] Starting position, in pixels.
* @param size [in] Width and height for a single line label.
* @param field [in] RomFields::Field
* @param fieldIdx [in] Field index
* @return Field height, in pixels.
*/
int initDimensions(HWND hDlg, HWND hWndTab,
const POINT &pt_start, const SIZE &size,
const RomFields::Field &field, int fieldIdx);
/**
* Initialize a multi-language string field.
* @param hDlg [in] Parent dialog window. (for dialog unit mapping)
* @param hWndTab [in] Tab window. (for the actual control)
* @param pt_start [in] Starting position, in pixels.
* @param size [in] Width and height for a single line label.
* @param field [in] RomFields::Field
* @param fieldIdx [in] Field index
* @return Field height, in pixels.
*/
int initStringMulti(HWND hDlg, HWND hWndTab,
const POINT &pt_start, const SIZE &size,
const RomFields::Field &field, int fieldIdx);
/**
* Build the cboLanguage image list.
*/
void buildCboLanguageImageList(void);
/**
* Update all multi-language fields.
* @param user_lc User-specified language code.
*/
void updateMulti(uint32_t user_lc);
/**
* Update a field's value.
* This is called after running a ROM operation.
* @param fieldIdx Field index.
* @return 0 on success; non-zero on error.
*/
int updateField(int fieldIdx);
/**
* Initialize the bold font.
* @param hFont Base font.
*/
void initBoldFont(HFONT hFont);
public:
/**
* Initialize the dialog. (hDlgSheet)
* Called by WM_INITDIALOG.
*/
void initDialog(void);
/**
* Adjust tabs for the message widget.
* Message widget must have been created first.
* Only run this after the message widget visibiliy has changed!
* @param bVisible True for visible; false for not.
*/
void adjustTabsForMessageWidgetVisibility(bool bVisible);
/**
* Show the message widget.
* Message widget must have been created first.
* @param messageType Message type.
* @param lpszMsg Message.
*/
void showMessageWidget(unsigned int messageType, const TCHAR *lpszMsg);
/**
* An "Options" menu action was triggered.
* @param menuId Menu ID. (Options ID + IDM_OPTIONS_MENU_BASE)
*/
void menuOptions_action_triggered(int menuId);
/**
* Dialog subclass procedure to intercept WM_COMMAND for the "Options" button.
* @param hWnd
* @param uMsg
* @param wParam
* @param lParam
* @param uIdSubclass
* @param dWRefData RP_ShellPropSheetExt_Private
*/
static LRESULT CALLBACK MainDialogSubclassProc(
HWND hWnd, UINT uMsg,
WPARAM wParam, LPARAM lParam,
UINT_PTR uIdSubclass, DWORD_PTR dwRefData);
/**
* Create the "Options" button in the parent window.
* Called by WM_INITDIALOG.
*/
void createOptionsButton(void);
private:
// Internal functions used by the callback functions.
INT_PTR DlgProc_WM_NOTIFY(HWND hDlg, NMHDR *pHdr);
INT_PTR DlgProc_WM_COMMAND(HWND hDlg, WPARAM wParam, LPARAM lParam);
INT_PTR DlgProc_WM_PAINT(HWND hDlg);
public:
// Property sheet callback functions.
static INT_PTR CALLBACK DlgProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
static UINT CALLBACK CallbackProc(HWND hWnd, UINT uMsg, LPPROPSHEETPAGE ppsp);
/**
* Dialog procedure for subtabs.
* @param hDlg
* @param uMsg
* @param wParam
* @param lParam
*/
static INT_PTR CALLBACK SubtabDlgProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam);
};
/** RP_ShellPropSheetExt_Private **/
// Property for "D pointer".
// This points to the ConfigDialogPrivate object.
const TCHAR RP_ShellPropSheetExt_Private::D_PTR_PROP[] = _T("RP_ShellPropSheetExt_Private");
// Property for "tab pointer".
// This points to the RP_ShellPropSheetExt_Private::tab object.
const TCHAR RP_ShellPropSheetExt_Private::TAB_PTR_PROP[] = _T("RP_ShellPropSheetExt_Private::tab");
RP_ShellPropSheetExt_Private::RP_ShellPropSheetExt_Private(RP_ShellPropSheetExt *q, string &&filename)
: q_ptr(q)
, filename(std::move(filename))
, romData(nullptr)
, hDlgSheet(nullptr)
, hBtnOptions(nullptr)
, hMenuOptions(nullptr)
, hFontDlg(nullptr)
, hFontBold(nullptr)
, fontHandler(nullptr)
, lblSysInfo(nullptr)
, colorAltRow(0)
, isFullyInit(false)
, lblBanner(nullptr)
, lblIcon(nullptr)
, dwExStyleRTL(0)
, tabWidget(nullptr)
, curTabIndex(0)
, lblDescHeight(0)
, hMessageWidget(nullptr)
, iTabHeightOrig(0)
, def_lc(0)
, cboLanguage(nullptr)
, himglFlags(nullptr)
{
// Initialize the alternate row color.
colorAltRow = LibWin32Common::getAltRowColor();
// Initialize other structs.
dlgSize.cx = 0;
dlgSize.cy = 0;
// Check for RTL.
// NOTE: Windows Explorer on Windows 7 seems to return 0 from GetProcessDefaultLayout(),
// even if an RTL language is in use. We'll check the taskbar layout instead.
// References:
// - https://stackoverflow.com/questions/10391669/how-to-detect-if-a-windows-installation-is-rtl
// - https://stackoverflow.com/a/10393376
HWND hTaskBar = FindWindow(_T("Shell_TrayWnd"), nullptr);
assert(hTaskBar != nullptr);
if (hTaskBar) {
dwExStyleRTL = static_cast<DWORD>(GetWindowLongPtr(hTaskBar, GWL_EXSTYLE)) & WS_EX_LAYOUTRTL;
}
}
RP_ShellPropSheetExt_Private::~RP_ShellPropSheetExt_Private()
{
// Delete the banner and icon frames.
delete lblBanner;
delete lblIcon;
// Delete the popup menu.
if (hMenuOptions) {
DestroyMenu(hMenuOptions);
}
// Unreference the RomData object.
UNREF(romData);
// Destroy the flags ImageList.
if (cboLanguage) {
SendMessage(cboLanguage, CBEM_SETIMAGELIST, 0, (LPARAM)nullptr);
}
if (himglFlags) {
ImageList_Destroy(himglFlags);
}
// Delete the fonts.
if (hFontBold) {
DeleteFont(hFontBold);
}
}
/**
* Load the banner and icon as HBITMAPs.
*
* This function should be called on startup and if
* the window's background color changes.
*
* NOTE: The HWND isn't needed here, since this function
* doesn't touch the dialog at all.
*/
void RP_ShellPropSheetExt_Private::loadImages(void)
{
// Supported image types.
const uint32_t imgbf = romData->supportedImageTypes();
// Banner.
bool ok = false;
if (imgbf & RomData::IMGBF_INT_BANNER) {
// Get the banner.
const rp_image *const banner = romData->image(RomData::IMG_INT_BANNER);
assert(banner != nullptr);
assert(banner->isValid());
if (banner && banner->isValid()) {
if (!lblBanner) {
lblBanner = new DragImageLabel(hDlgSheet);
// TODO: Required size? For now, disabling scaling.
lblBanner->setRequiredSize(0, 0);
}
ok = lblBanner->setRpImage(banner);
}
}
if (!ok) {
// No banner, or unable to load the banner.
// Delete the DragImageLabel if it was created previously.
delete lblBanner;
lblBanner = nullptr;
}
// Icon.
ok = false;
if (imgbf & RomData::IMGBF_INT_ICON) {
// Get the icon.
const rp_image *const icon = romData->image(RomData::IMG_INT_ICON);
assert(icon != nullptr);
assert(icon->isValid());
if (icon && icon->isValid()) {
if (!lblIcon) {
lblIcon = new DragImageLabel(hDlgSheet);
}
// Is this an animated icon?
ok = lblIcon->setIconAnimData(romData->iconAnimData());
if (!ok) {
// Not an animated icon, or invalid icon data.
// Set the static icon.
ok = lblIcon->setRpImage(icon);
}
}
}
if (!ok) {
// No icon, or unable to load the icon.
// Delete the DragImageLabel if it was created previously.
delete lblBanner;
lblBanner = nullptr;
}
}
/**
* Rescale an image to be as close to the required size as possible.
* @param req_sz [in] Required size.
* @param sz [in/out] Image size.
* @return True if nearest-neighbor scaling should be used (size was kept the same or enlarged); false if shrunken (so use interpolation).
*/
bool RP_ShellPropSheetExt_Private::rescaleImage(const SIZE &req_sz, SIZE &sz)
{
// TODO: Adjust req_sz for DPI.
if (sz.cx == req_sz.cx && sz.cy == req_sz.cy) {
// No resize necessary.
return true;
}
// Check if the image is too big.
if (sz.cx >= req_sz.cx || sz.cy >= req_sz.cy) {
// Image is too big. Shrink it.
// FIXME: Assuming the icon is always a power of two.
// Move TCreateThumbnail::rescale_aspect() into another file
// and make use of that.
sz.cx = 32;
sz.cy = 32;
return false;
}
// Image is too small.
// TODO: Ensure dimensions don't exceed req_img_size.
SIZE orig_sz = sz;
do {
// Increase by integer multiples until
// the icon is at least 32x32.
// TODO: Constrain to 32x32?
sz.cx += orig_sz.cx;
sz.cy += orig_sz.cy;
} while (sz.cx < req_sz.cx && sz.cy < req_sz.cy);
return true;
}
/**
* Create the header row.
* @param hDlg [in] Dialog window.
* @param pt_start [in] Starting position, in pixels.
* @param size [in] Width and height for a full-width single line label.
* @return Row height, in pixels.
*/
int RP_ShellPropSheetExt_Private::createHeaderRow(HWND hDlg, const POINT &pt_start, const SIZE &size)
{
if (!hDlg || !romData)
return 0;
// Total widget width.
int total_widget_width = 0;
// Label size.
SIZE size_lblSysInfo = {0, 0};
// Font to use.
// TODO: Handle these assertions in release builds.
assert(hFontBold != nullptr);
assert(hFontDlg != nullptr);
const HFONT hFont = (hFontBold ? hFontBold : hFontDlg);
// System name and file type.
// TODO: System logo and/or game title?
const char *systemName = romData->systemName(
RomData::SYSNAME_TYPE_LONG | RomData::SYSNAME_REGION_ROM_LOCAL);
const char *fileType = romData->fileType_string();
assert(systemName != nullptr);
assert(fileType != nullptr);
if (!systemName) {
systemName = C_("RomDataView", "(unknown system)");
}
if (!fileType) {
fileType = C_("RomDataView", "(unknown filetype)");
}
const tstring ts_sysInfo =
LibWin32Common::unix2dos(U82T_s(rp_sprintf_p(
// tr: %1$s == system name, %2$s == file type
C_("RomDataView", "%1$s\n%2$s"), systemName, fileType)));
if (!ts_sysInfo.empty()) {
// Determine the appropriate label size.
if (!LibWin32Common::measureTextSize(hDlg, hFont, ts_sysInfo, &size_lblSysInfo)) {
// Start the total_widget_width.
total_widget_width = size_lblSysInfo.cx;
} else {
// Error determining the label size.
// Don't draw the label.
size_lblSysInfo.cx = 0;
size_lblSysInfo.cy = 0;
}
}
// Add the banner and icon widths.
// Banner.
// TODO: Spacing between banner and text?
// Doesn't seem to be needed with Dreamcast saves...
const int banner_width = (lblBanner ? lblBanner->actualSize().cx : 0);
total_widget_width += banner_width;
// Icon.
const int icon_width = (lblIcon ? lblIcon->actualSize().cx : 0);
if (icon_width > 0) {
if (total_widget_width > 0) {
total_widget_width += pt_start.x;
}
total_widget_width += icon_width;
}
// Starting point.
POINT curPt = {
((size.cx - total_widget_width) / 2) + pt_start.x,
pt_start.y
};
// lblSysInfo
if (size_lblSysInfo.cx > 0 && size_lblSysInfo.cy > 0) {
ptSysInfo.x = curPt.x;
ptSysInfo.y = curPt.y;
lblSysInfo = CreateWindowEx(WS_EX_NOPARENTNOTIFY | WS_EX_TRANSPARENT,
WC_STATIC, ts_sysInfo.c_str(),
WS_CHILD | WS_VISIBLE | SS_CENTER,
ptSysInfo.x, ptSysInfo.y,
size_lblSysInfo.cx, size_lblSysInfo.cy,
hDlg, (HMENU)IDC_STATIC, nullptr, nullptr);
SetWindowFont(lblSysInfo, hFont, false);
curPt.x += size_lblSysInfo.cx + pt_start.x;
}
// Banner.
if (banner_width > 0) {
lblBanner->setPosition(curPt);
curPt.x += banner_width + pt_start.x;
}
// Icon.
if (icon_width > 0) {
lblIcon->setPosition(curPt);
curPt.x += icon_width + pt_start.x;
}
// Return the label height and some extra padding.
// TODO: Icon/banner height?
return size_lblSysInfo.cy + (pt_start.y * 5 / 8);
}
/**
* Initialize a string field. (Also used for Date/Time.)
* @param hDlg [in] Parent dialog window. (for dialog unit mapping)
* @param hWndTab [in] Tab window. (for the actual control)
* @param pt_start [in] Starting position, in pixels.
* @param size [in] Width and height for a single line label.
* @param field [in] RomFields::Field
* @param fieldIdx [in] Field index
* @param str [in,opt] String data. (If nullptr, field data is used.)
* @param pOutHWND [out,opt] Retrieves the control's HWND.
* @return Field height, in pixels.
*/
int RP_ShellPropSheetExt_Private::initString(_In_ HWND hDlg, _In_ HWND hWndTab,
_In_ const POINT &pt_start, _In_ const SIZE &size,
_In_ const RomFields::Field &field, _In_ int fieldIdx,
_In_ LPCTSTR str, _Outptr_opt_ HWND *pOutHWND)
{
if (pOutHWND) {
// Clear the output HWND initially.
*pOutHWND = nullptr;
}
// NOTE: libromdata uses Unix-style newlines.
// For proper display on Windows, we have to
// add carriage returns.
// If string data wasn't specified, get the RFT_STRING data
// from the RomFields::Field object.
int lf_count = 0;
tstring str_nl;
if (!str) {
if (field.type != RomFields::RFT_STRING)
return 0;
// NULL string == empty string
if (field.data.str) {
str_nl = LibWin32Common::unix2dos(U82T_s(*(field.data.str)), &lf_count);
}
} else {
// Use the specified string.
str_nl = LibWin32Common::unix2dos(str, &lf_count);
}
// Field height.
int field_cy = size.cy;
if (lf_count > 0) {
// Multiple lines.
// NOTE: Only add 5/8 of field_cy per line.
// FIXME: 5/8 needs adjustment...
field_cy += (field_cy * lf_count) * 5 / 8;
}
// Get the default font.
HFONT hFont = hFontDlg;
// Check for any formatting options.
bool isWarning = false, isMonospace = false;
if (field.type == RomFields::RFT_STRING) {
// FIXME: STRF_MONOSPACE | STRF_WARNING is not supported.
// Preferring STRF_WARNING.
assert((field.desc.flags &
(RomFields::STRF_MONOSPACE | RomFields::STRF_WARNING)) !=
(RomFields::STRF_MONOSPACE | RomFields::STRF_WARNING));
if (field.desc.flags & RomFields::STRF_WARNING) {
// "Warning" font.
if (hFontBold) {
hFont = hFontBold;
isWarning = true;
// Set the font of the description control.
HWND hStatic = GetDlgItem(hWndTab, IDC_STATIC_DESC(fieldIdx));
if (hStatic) {
SetWindowFont(hStatic, hFont, false);
setWarningControls.insert(hStatic);
}
}
} else if (field.desc.flags & RomFields::STRF_MONOSPACE) {
// Monospaced font.
isMonospace = true;
}
}
// Dialog item.
const HMENU cId = (HMENU)(INT_PTR)(IDC_RFT_STRING(fieldIdx));
HWND hDlgItem;
if (field.type == RomFields::RFT_STRING &&
(field.desc.flags & RomFields::STRF_CREDITS))
{
// Align to the bottom of the dialog and center-align the text.
// 7x7 DLU margin is recommended by the Windows UX guidelines.
// Reference: http://stackoverflow.com/questions/2118603/default-dialog-padding
RECT tmpRect = {7, 7, 8, 8};
MapDialogRect(hWndTab, &tmpRect);
RECT winRect;
GetClientRect(hWndTab, &winRect);
// NOTE: We need to move left by 1px.
OffsetRect(&winRect, -1, 0);
// There should be a maximum of one STRF_CREDITS per tab.
auto &tab = tabs[field.tabIdx];
assert(tab.lblCredits == nullptr);
if (tab.lblCredits != nullptr) {
// Duplicate credits label.
return 0;
}
// Create a SysLink widget.
// SysLink allows the user to click a link and
// open a webpage. It does NOT allow highlighting.
// TODO: SysLink + EDIT?
// FIXME: Centered text alignment?
// TODO: With subtabs:
// - Verify behavior of LWS_TRANSPARENT.
// - Show below subtabs.
#ifdef UNICODE
hDlgItem = CreateWindowEx(WS_EX_NOPARENTNOTIFY | WS_EX_TRANSPARENT | dwExStyleRTL,
WC_LINK, str_nl.c_str(),
WS_CHILD | WS_TABSTOP | WS_VISIBLE,
0, 0, 0, 0, // will be adjusted afterwards
hWndTab, cId, nullptr, nullptr);
if (!hDlgItem)
#endif /* UNICODE */
{
// Unable to create a SysLink control
// This might happen if this is an ANSI build
// or if we're running on Windows 2000.
// FIXME: Remove links from the text before creating
// a plain-old WC_EDIT control.
// Create a read-only EDIT control.
// The STATIC control doesn't allow the user
// to highlight and copy data.
DWORD dwStyle;
if (lf_count > 0) {
// Multiple lines.
dwStyle = WS_CHILD | WS_TABSTOP | WS_VISIBLE | WS_CLIPSIBLINGS | ES_READONLY | ES_AUTOHSCROLL | ES_MULTILINE;
} else {
// Single line.
dwStyle = WS_CHILD | WS_TABSTOP | WS_VISIBLE | WS_CLIPSIBLINGS | ES_READONLY | ES_AUTOHSCROLL;
}
hDlgItem = CreateWindowEx(WS_EX_NOPARENTNOTIFY | WS_EX_TRANSPARENT | dwExStyleRTL,
WC_EDIT, str_nl.c_str(), dwStyle,
0, 0, 0, 0, // will be adjusted afterwards
hWndTab, cId, nullptr, nullptr);
// Subclass multi-line EDIT controls to work around Enter/Escape issues.
// We're also subclassing single-line EDIT controls to disable the
// initial selection. (DLGC_HASSETSEL)
// Reference: http://blogs.msdn.com/b/oldnewthing/archive/2007/08/20/4470527.aspx
// TODO: Error handling?
SUBCLASSPROC proc = (dwStyle & ES_MULTILINE)
? LibWin32Common::MultiLineEditProc
: LibWin32Common::SingleLineEditProc;
SetWindowSubclass(hDlgItem, proc,
reinterpret_cast<UINT_PTR>(cId),
reinterpret_cast<DWORD_PTR>(GetParent(hDlgSheet)));
}
tab.lblCredits = hDlgItem;
SetWindowFont(hDlgItem, hFont, false);
// NOTE: We can't use LibWin32Common::measureTextSize() because
// that includes the HTML markup, and LM_GETIDEALSIZE is Vista+ only.
// Use a wrapper measureTextSizeLink() that removes HTML-like
// tags and then calls measureTextSize().
SIZE szText;
LibWin32Common::measureTextSizeLink(hWndTab, hFont, str_nl, &szText);
// Determine the position.
const int x = (((winRect.right - winRect.left) - szText.cx) / 2) + winRect.left;
const int y = winRect.bottom - tmpRect.top - szText.cy;
// Set the position and size.
SetWindowPos(hDlgItem, 0, x, y, szText.cx, szText.cy,
SWP_NOACTIVATE | SWP_NOOWNERZORDER | SWP_NOZORDER);
// Clear field_cy so the description widget won't show up
// and the "normal" area will be empty.
field_cy = 0;
} else {
// Create a read-only EDIT control.
// The STATIC control doesn't allow the user
// to highlight and copy data.
DWORD dwStyle;
if (lf_count > 0) {
// Multiple lines.
dwStyle = WS_CHILD | WS_TABSTOP | WS_VISIBLE | WS_CLIPSIBLINGS | ES_READONLY | ES_AUTOHSCROLL | ES_MULTILINE;
} else {
// Single line.
dwStyle = WS_CHILD | WS_TABSTOP | WS_VISIBLE | WS_CLIPSIBLINGS | ES_READONLY | ES_AUTOHSCROLL;
}
hDlgItem = CreateWindowEx(WS_EX_NOPARENTNOTIFY | WS_EX_TRANSPARENT | dwExStyleRTL,
WC_EDIT, str_nl.c_str(), dwStyle,
pt_start.x, pt_start.y,
size.cx, field_cy,
hWndTab, cId, nullptr, nullptr);
SetWindowFont(hDlgItem, hFont, false);
// Get the EDIT control margins.
const DWORD dwMargins = (DWORD)SendMessage(hDlgItem, EM_GETMARGINS, 0, 0);
// Adjust the window size to compensate for the margins not being clickable.
// NOTE: Not adjusting the right margins.
SetWindowPos(hDlgItem, nullptr, pt_start.x - LOWORD(dwMargins), pt_start.y,
size.cx + LOWORD(dwMargins), field_cy,
SWP_NOACTIVATE | SWP_NOOWNERZORDER | SWP_NOZORDER);
// Subclass multi-line EDIT controls to work around Enter/Escape issues.
// We're also subclassing single-line EDIT controls to disable the
// initial selection. (DLGC_HASSETSEL)
// Reference: http://blogs.msdn.com/b/oldnewthing/archive/2007/08/20/4470527.aspx
// TODO: Error handling?
SUBCLASSPROC proc = (dwStyle & ES_MULTILINE)
? LibWin32Common::MultiLineEditProc
: LibWin32Common::SingleLineEditProc;
SetWindowSubclass(hDlgItem, proc,
reinterpret_cast<UINT_PTR>(cId),
reinterpret_cast<DWORD_PTR>(GetParent(hDlgSheet)));
}
// Save the control in the appropriate container, if necessary.
if (isWarning) {
setWarningControls.insert(hDlgItem);
}
if (isMonospace) {
fontHandler.addMonoControl(hDlgItem);
}
// Return the HWND if requested.
if (pOutHWND) {
*pOutHWND = hDlgItem;
}
return field_cy;
}
/**
* Initialize a bitfield layout.
* @param hDlg [in] Parent dialog window. (for dialog unit mapping)
* @param hWndTab [in] Tab window. (for the actual control)
* @param pt_start [in] Starting position, in pixels.
* @param field [in] RomFields::Field
* @param fieldIdx [in] Field index
* @return Field height, in pixels.
*/
int RP_ShellPropSheetExt_Private::initBitfield(HWND hDlg, HWND hWndTab,
const POINT &pt_start,
const RomFields::Field &field, int fieldIdx)
{
// Checkbox size.
// Reference: http://stackoverflow.com/questions/1164868/how-to-get-size-of-check-and-gap-in-check-box
RECT rect_chkbox = {0, 0, 12+4, 11};
MapDialogRect(hDlg, &rect_chkbox);
// Dialog font and device context.
// NOTE: Using the parent dialog's font.
AutoGetDC hDC(hWndTab, hFontDlg);
// Create a grid of checkboxes.
const auto &bitfieldDesc = field.desc.bitfield;
int count = (int)bitfieldDesc.names->size();
assert(count <= 32);
if (count > 32)
count = 32;
// Determine the available width for checkboxes.
RECT rectDlg;
GetClientRect(hWndTab, &rectDlg);
// NOTE: We need to move left by 1px.
OffsetRect(&rectDlg, -1, 0);
const int max_width = rectDlg.right - pt_start.x;
// Convert the bitfield description names to the
// native Windows encoding once.
vector<tstring> tnames;
tnames.reserve(count);
std::for_each(bitfieldDesc.names->cbegin(), bitfieldDesc.names->cend(),
[&tnames](const string &name) {
if (name.empty()) {
// Skip U82T_s() for empty strings.
tnames.emplace_back(tstring());
} else {
tnames.emplace_back(U82T_s(name));
}
}
);
// Column widths for multi-row layouts.
// (Includes the checkbox size.)
std::vector<int> col_widths;
int row = 0, col = 0;
int elemsPerRow = bitfieldDesc.elemsPerRow;
if (elemsPerRow == 1) {
// Optimization: Use the entire width of the dialog.
// TODO: Testing; right margin.
col_widths.emplace_back(max_width);
} else if (elemsPerRow > 1) {
// Determine the widest entry in each column.
// If the columns are wider than the available area,
// reduce the number of columns until it fits.
for (; elemsPerRow > 1; elemsPerRow--) {
col_widths.resize(elemsPerRow);
row = 0; col = 0;
auto iter = tnames.cbegin();
for (int j = 0; j < count; ++iter, j++) {
const tstring &tname = *iter;
if (tname.empty())
continue;
// Get the width of this specific entry.
// TODO: Use LibWin32Common::measureTextSize()?
SIZE textSize;
GetTextExtentPoint32(hDC, tname.data(), (int)tname.size(), &textSize);
int chk_w = rect_chkbox.right + textSize.cx;
if (chk_w > col_widths[col]) {
col_widths[col] = chk_w;
}
// Next column.
col++;
if (col == elemsPerRow) {
// Next row.
row++;
col = 0;
}
}
// Add up the widths.
const int total_width = std::accumulate(col_widths.cbegin(), col_widths.cend(), 0);
// TODO: "DLL" on Windows executables is forced to the next line.
// Add 7x7 DLU margins?
if (total_width <= max_width) {
// Everything fits.
break;
}
// Too wide; try removing a column.
// Reset the column widths first.
// TODO: Better way to clear a vector?
// TODO: Skip the last element?
memset(col_widths.data(), 0, col_widths.size() * sizeof(int));
}
if (elemsPerRow == 1) {
// We're left with 1 column.
col_widths.resize(1);
col_widths[0] = max_width;
}
}
// Initial position on the dialog, in pixels.
POINT pt = pt_start;
// Subtract 0.5 DLU from the starting row.
RECT rect_subtract = {0, 0, 1, 1};
MapDialogRect(hDlg, &rect_subtract);
if (rect_subtract.bottom > 1) {
rect_subtract.bottom /= 2;
}
pt.y -= rect_subtract.bottom;
// WS_EX_LAYOUTRTL
const DWORD dwExStyleRTL = checkLayoutRTL();
row = 0; col = 0;
auto iter = tnames.cbegin();
uint32_t bitfield = field.data.bitfield;
for (int bit = 0; bit < count; ++iter, bit++, bitfield >>= 1) {
const tstring &tname = *iter;
if (tname.empty())
continue;
// Get the text size.
int chk_w;
if (elemsPerRow == 0) {
// Get the width of this specific entry.
// TODO: Use LibWin32Common::measureTextSize()?
SIZE textSize;
GetTextExtentPoint32(hDC, tname.data(), (int)tname.size(), &textSize);
chk_w = rect_chkbox.right + textSize.cx;
} else {
if (col == elemsPerRow) {
// Next row.
row++;
col = 0;
pt.x = pt_start.x;
pt.y += rect_chkbox.bottom;
}
// Use the largest width in the column.
chk_w = col_widths[col];
}
// FIXME: Tab ordering?
HWND hCheckBox = CreateWindowEx(WS_EX_NOPARENTNOTIFY | WS_EX_TRANSPARENT | dwExStyleRTL,
WC_BUTTON, tname.c_str(),
WS_CHILD | WS_TABSTOP | WS_VISIBLE | BS_CHECKBOX,
pt.x, pt.y, chk_w, rect_chkbox.bottom,
hWndTab, (HMENU)(INT_PTR)(IDC_RFT_BITFIELD(fieldIdx, bit)),
nullptr, nullptr);
SetWindowFont(hCheckBox, hFontDlg, false);
// Set the checkbox state.
Button_SetCheck(hCheckBox, (bitfield & 1) ? BST_CHECKED : BST_UNCHECKED);
// Next column.
pt.x += chk_w;
col++;
}
// Return the total number of rows times the checkbox height,
// plus another 0.25 of a checkbox.
int ret = ((row + 1) * rect_chkbox.bottom);
ret += (rect_chkbox.bottom / 4);
return ret;
}
/**
* Measure the width of a ListData string.
* This function handles newlines.
* @param hDC [in] HDC for text measurement.
* @param tstr [in] String to measure.
* @param pNlCount [out,opt] Newline count.
* @return Width.
*/
int RP_ShellPropSheetExt_Private::measureListDataString(HDC hDC, const tstring &tstr, int *pNlCount)
{
// TODO: Actual padding value?
static const int COL_WIDTH_PADDING = 8*2;
// Measured width.
int width = 0;
// Count newlines.
size_t prev_nl_pos = 0;
size_t cur_nl_pos;
int nl = 0;
while ((cur_nl_pos = tstr.find(_T('\n'), prev_nl_pos)) != tstring::npos) {
// Measure the width, plus padding on both sides.
//
// LVSCW_AUTOSIZE_USEHEADER doesn't work for entries with newlines.
// This allows us to set a good initial size, but it won't help if
// someone double-clicks the column splitter, triggering an automatic
// resize.
//
// TODO: Use ownerdraw instead? (WM_MEASUREITEM / WM_DRAWITEM)
// NOTE: Not using LibWin32Common::measureTextSize()
// because that does its own newline checks.
// TODO: Verify the values here.
SIZE textSize;
GetTextExtentPoint32(hDC, &tstr[prev_nl_pos], (int)(cur_nl_pos - prev_nl_pos), &textSize);
width = std::max<int>(width, textSize.cx + COL_WIDTH_PADDING);
nl++;
prev_nl_pos = cur_nl_pos + 1;
}
if (nl > 0) {
// Measure the last line.
// TODO: Verify the values here.
SIZE textSize;
GetTextExtentPoint32(hDC, &tstr[prev_nl_pos], (int)(tstr.size() - prev_nl_pos), &textSize);
width = std::max<int>(width, textSize.cx + COL_WIDTH_PADDING);
}
if (pNlCount) {
*pNlCount = nl;
}
// FIXME: Don't use LVSCW_AUTOSIZE_USEHEADER.
// LVS_OWNERDATA doesn't handle this properly. (only gets what's onscreen)
// TODO: Figure out the correct padding so the columns aren't truncated.
return (nl > 0 ? width : LVSCW_AUTOSIZE_USEHEADER);
}
/**
* Initialize a ListData field.
* @param hDlg [in] Parent dialog window. (for dialog unit mapping)
* @param hWndTab [in] Tab window. (for the actual control)
* @param pt_start [in] Starting position, in pixels.
* @param size [in] Width and height for a default ListView.
* @param doResize [in] If true, resize the ListView to accomodate rows_visible.
* @param field [in] RomFields::Field
* @param fieldIdx [in] Field index
* @return Field height, in pixels.
*/
int RP_ShellPropSheetExt_Private::initListData(HWND hDlg, HWND hWndTab,
const POINT &pt_start, const SIZE &size, bool doResize,
const RomFields::Field &field, int fieldIdx)
{
const auto &listDataDesc = field.desc.list_data;
// NOTE: listDataDesc.names can be nullptr,
// which means we don't have any column headers.
// Single language ListData_t.
// For RFT_LISTDATA_MULTI, this is only used for row and column count.
const RomFields::ListData_t *list_data;
const bool isMulti = !!(listDataDesc.flags & RomFields::RFT_LISTDATA_MULTI);
if (isMulti) {
// Multiple languages.
const auto *const multi = field.data.list_data.data.multi;
assert(multi != nullptr);
assert(!multi->empty());
if (!multi || multi->empty()) {
// No data...
return 0;
}
list_data = &multi->cbegin()->second;
} else {
// Single language.
list_data = field.data.list_data.data.single;
}
assert(list_data != nullptr);
assert(!list_data->empty());
if (!list_data || list_data->empty()) {
// No list data...
return 0;
}
// Validate flags.
// Cannot have both checkboxes and icons.
const bool hasCheckboxes = !!(listDataDesc.flags & RomFields::RFT_LISTDATA_CHECKBOXES);
const bool hasIcons = !!(listDataDesc.flags & RomFields::RFT_LISTDATA_ICONS);
assert(!(hasCheckboxes && hasIcons));
if (hasCheckboxes && hasIcons) {
// Both are set. This shouldn't happen...
return 0;
}
if (hasIcons) {
assert(field.data.list_data.mxd.icons != nullptr);
if (!field.data.list_data.mxd.icons) {
// No icons vector...
return 0;
}
}
// Create a ListView widget.
// NOTE: Separate row option is handled by the caller.
// TODO: Enable sorting?
// TODO: Optimize by not using OR?
DWORD lvsStyle = WS_CHILD | WS_VISIBLE | WS_TABSTOP | LVS_ALIGNLEFT |
LVS_REPORT | LVS_SINGLESEL | LVS_NOSORTHEADER | LVS_OWNERDATA;
if (!listDataDesc.names) {
lvsStyle |= LVS_NOCOLUMNHEADER;
}
const uint16_t dlgID = IDC_RFT_LISTDATA(fieldIdx);
HWND hListView = CreateWindowEx(WS_EX_NOPARENTNOTIFY | WS_EX_CLIENTEDGE | dwExStyleRTL,
WC_LISTVIEW, nullptr, lvsStyle,
pt_start.x, pt_start.y,
size.cx, size.cy,
hWndTab, (HMENU)(INT_PTR)(dlgID),
nullptr, nullptr);
SetWindowFont(hListView, hFontDlg, false);
hwndListViewControls.emplace_back(hListView);
// Set extended ListView styles.
DWORD lvsExStyle = LVS_EX_FULLROWSELECT;
if (!GetSystemMetrics(SM_REMOTESESSION)) {
// Not RDP (or is RemoteFX): Enable double buffering.
lvsExStyle |= LVS_EX_DOUBLEBUFFER;
}
if (hasCheckboxes) {
lvsExStyle |= LVS_EX_CHECKBOXES;
}
ListView_SetExtendedListViewStyle(hListView, lvsExStyle);
// Insert columns.
int colCount = 1;
if (listDataDesc.names) {
colCount = (int)listDataDesc.names->size();
} else {
// No column headers.
// Use the first row.
colCount = (int)list_data->at(0).size();
}
// Column widths.
// LVSCW_AUTOSIZE_USEHEADER doesn't work for entries with newlines.
// TODO: Use ownerdraw instead? (WM_MEASUREITEM / WM_DRAWITEM)
unique_ptr<int[]> col_width(new int[colCount]);
// Format table.
// All values are known to fit in uint8_t.
static const uint8_t align_tbl[4] = {
// Order: TXA_D, TXA_L, TXA_C, TXA_R
LVCFMT_LEFT, LVCFMT_LEFT, LVCFMT_CENTER, LVCFMT_RIGHT
};
// NOTE: ListView header alignment matches data alignment.
// We'll prefer the data alignment value.
uint32_t align = listDataDesc.alignment.data;
LVCOLUMN lvColumn;
if (listDataDesc.names) {
auto iter = listDataDesc.names->cbegin();
for (int i = 0; i < colCount; ++iter, i++, align >>= 2) {
lvColumn.mask = LVCF_TEXT | LVCF_FMT;
lvColumn.fmt = align_tbl[align & 3];
const string &str = *iter;
if (!str.empty()) {
// NOTE: pszText is LPTSTR, not LPCTSTR...
const tstring tstr = U82T_s(str);
lvColumn.pszText = const_cast<LPTSTR>(tstr.c_str());
ListView_InsertColumn(hListView, i, &lvColumn);
} else {
// Don't show this column.
// FIXME: Zero-width column is a bad hack...
lvColumn.pszText = _T("");
lvColumn.mask |= LVCF_WIDTH;
lvColumn.cx = 0;
ListView_InsertColumn(hListView, i, &lvColumn);
}
col_width[i] = LVSCW_AUTOSIZE_USEHEADER;
}
} else {
lvColumn.mask = LVCF_FMT;
for (int i = 0; i < colCount; i++, align >>= 2) {
lvColumn.fmt = align_tbl[align & 3];
ListView_InsertColumn(hListView, i, &lvColumn);
col_width[i] = LVSCW_AUTOSIZE_USEHEADER;
}
}
// Dialog font and device context.
// NOTE: Using the parent dialog's font.
AutoGetDC hDC(hListView, hFontDlg);
// Add the row data.
uint32_t checkboxes = 0;
if (hasCheckboxes) {
checkboxes = field.data.list_data.mxd.checkboxes;
}
// NOTE: We're converting the strings for use with
// LVS_OWNERDATA.
vector<vector<tstring> > lvStringData;
lvStringData.reserve(list_data->size());
LvData_t lvData;
lvData.vvStr.reserve(list_data->size());
lvData.hasCheckboxes = hasCheckboxes;
if (hasIcons) {
lvData.vImageList.reserve(list_data->size());
}
int lv_row_num = 0, data_row_num = 0;
int nl_max = 0; // Highest number of newlines in any string.
const auto list_data_cend = list_data->cend();
for (auto iter = list_data->cbegin(); iter != list_data_cend; ++iter, data_row_num++) {
const vector<string> &data_row = *iter;
// FIXME: Skip even if we don't have checkboxes?
// (also check other UI frontends)
if (hasCheckboxes) {
if (data_row.empty()) {
// Skip this row.
checkboxes >>= 1;
continue;
} else {
// Store the checkbox value for this row.
if (checkboxes & 1) {
lvData.checkboxes |= (1U << lv_row_num);
}
checkboxes >>= 1;
}
}
// Destination row.
lvData.vvStr.resize(lvData.vvStr.size()+1);
auto &lv_row_data = lvData.vvStr.at(lvData.vvStr.size()-1);
lv_row_data.reserve(data_row.size());
// String data.
if (isMulti) {
// RFT_LISTDATA_MULTI. Allocate space for the strings,
// but don't initialize them here.
lv_row_data.resize(data_row.size());
// Check newline counts in all strings to find nl_max.
const auto *const multi = field.data.list_data.data.multi;
const auto multi_cend = multi->cend();
for (auto iter_m = multi->cbegin(); iter_m != multi_cend; ++iter_m) {
const RomFields::ListData_t &ld = iter_m->second;
const auto ld_cend = ld.cend();
for (auto iter_row = ld.cbegin(); iter_row != ld_cend; ++iter_row) {
const auto &data_row = *iter_row;
const auto data_row_cend = data_row.cend();
for (auto iter_col = data_row.cbegin(); iter_col != data_row_cend; ++iter_col) {
size_t prev_nl_pos = 0;
size_t cur_nl_pos;
int nl = 0;
while ((cur_nl_pos = iter_col->find('\n', prev_nl_pos)) != tstring::npos) {
nl++;
prev_nl_pos = cur_nl_pos + 1;
}
nl_max = std::max(nl_max, nl);
}
}
}
} else {
// Single language.
int col = 0;
const auto data_row_cend = data_row.cend();
for (auto iter = data_row.cbegin(); iter != data_row_cend; ++iter, col++) {
tstring tstr = U82T_s(*iter);
int nl_count;
int width = measureListDataString(hDC, tstr, &nl_count);
if (col < colCount) {
col_width[col] = std::max(col_width[col], width);
}
nl_max = std::max(nl_max, nl_count);
// TODO: Store the icon index if necessary.
lv_row_data.emplace_back(std::move(tstr));
}
}
// Next row.
lv_row_num++;
}
// Icons.
if (hasIcons) {
// Icon size is 32x32, adjusted for DPI. (TODO: WM_DPICHANGED)
// ImageList will resize the original icons to 32x32.
// NOTE: LVS_REPORT doesn't allow variable row sizes,
// at least not without using ownerdraw. Hence, we'll
// use a hackish workaround: If any entry has more than
// two newlines, increase the Imagelist icon size by
// 16 pixels.
// TODO: Handle this better.
// FIXME: This only works if the RFT_LISTDATA has icons.
const int px = rp_AdjustSizeForDpi(32, rp_GetDpiForWindow(hDlg));
SIZE sizeListIcon = {px, px};
bool resizeNeeded = false;
float factor = 1.0f;
if (nl_max >= 2) {
// Two or more newlines.
// Add half of the icon size per newline over 1.
sizeListIcon.cy += ((px/2) * (nl_max - 1));
resizeNeeded = true;
factor = (float)sizeListIcon.cy / (float)px;
}
HIMAGELIST himl = ImageList_Create(sizeListIcon.cx, sizeListIcon.cy,
ILC_COLOR32, static_cast<int>(list_data->size()), 0);
assert(himl != nullptr);
if (himl) {
// NOTE: ListView uses LVSIL_SMALL for LVS_REPORT.
// TODO: The row highlight doesn't surround the empty area
// of the icon. LVS_OWNERDRAW is probably needed for that.
ListView_SetImageList(hListView, himl, LVSIL_SMALL);
uint32_t lvBgColor[2];
lvBgColor[0] = LibWin32Common::GetSysColor_ARGB32(COLOR_WINDOW);
lvBgColor[1] = LibWin32Common::getAltRowColor_ARGB32();
// Add icons.
uint8_t rowColorIdx = 0;
const auto &icons = field.data.list_data.mxd.icons;
const auto icons_cend = icons->cend();
for (auto iter = icons->cbegin(); iter != icons_cend;
++iter, rowColorIdx = !rowColorIdx)
{
bool needsUnref = false;
int iImage = -1;
const rp_image *icon = *iter;
if (!icon) {
// No icon for this row.
lvData.vImageList.emplace_back(iImage);
continue;
}
if (dwExStyleRTL != 0) {
// WS_EX_LAYOUTRTL will flip bitmaps in the ListView.
// ILC_MIRROR mirrors the bitmaps if the process is mirrored,
// but we can't rely on that being the case, and this option
// was first introduced in Windows XP.
// We'll flip the image here to counteract it.
const rp_image *const flipimg = icon->flip(rp_image::FLIP_H);
assert(flipimg != nullptr);
if (flipimg) {
icon = flipimg;
needsUnref = true;
}
}
// Resize the icon, if necessary.
if (resizeNeeded) {
SIZE szResize = {icon->width(), icon->height()};
szResize.cy = static_cast<LONG>(szResize.cy * factor);
// If the original icon is CI8, it needs to be
// converted to ARGB32 first. Otherwise, the
// "empty" background area will be black.
// NOTE: We still need to specify a background color,
// since the ListView highlight won't show up on
// alpha-transparent pixels.
// TODO: Handle this in rp_image::resized()?
// TODO: Handle theme changes?
// TODO: Error handling.
if (icon->format() != rp_image::Format::ARGB32) {
const rp_image *const icon32 = icon->dup_ARGB32();
if (icon32) {
if (needsUnref) {
icon->unref();
}
icon = icon32;
needsUnref = true;
}
}
// Resize the icon.
const rp_image *const icon_resized = icon->resized(
szResize.cx, szResize.cy,
rp_image::AlignVCenter, lvBgColor[rowColorIdx]);
assert(icon_resized != nullptr);
if (icon_resized) {
if (needsUnref) {
icon->unref();
}
icon = icon_resized;
needsUnref = true;
}
}
HICON hIcon = RpImageWin32::toHICON(icon);
if (needsUnref) {
icon->unref();
}
assert(hIcon != nullptr);
if (hIcon) {
int idx = ImageList_AddIcon(himl, hIcon);
if (idx >= 0) {
// Icon added.
iImage = idx;
}
// ImageList makes a copy of the icon.
DestroyIcon(hIcon);
}
// Save the ImageList index for later.
lvData.vImageList.emplace_back(iImage);
}
}
}
if (isMulti) {
lvData.hListView = hListView;
lvData.pField = &field;
}
// Save the LvData_t.
// TODO: Verify that std::move() works here.
map_lvData.insert(std::make_pair(dlgID, std::move(lvData)));
// Set the virtual list item count.
ListView_SetItemCountEx(hListView, lv_row_num,
LVSICF_NOINVALIDATEALL | LVSICF_NOSCROLL);
if (!isMulti) {
// Resize all of the columns.
// TODO: Do this on system theme change?
// TODO: Add a flag for 'main data column' and adjust it to
// not exceed the viewport.
for (int i = 0; i < colCount; i++) {
ListView_SetColumnWidth(hListView, i, col_width[i]);
}
}
// Get the dialog margin.
// 7x7 DLU margin is recommended by the Windows UX guidelines.
// Reference: http://stackoverflow.com/questions/2118603/default-dialog-padding
RECT dlgMargin = {7, 7, 8, 8};
MapDialogRect(hDlg, &dlgMargin);
// Increase the ListView height.
// Default: 5 rows, plus the header.
// FIXME: Might not work for LVS_OWNERDATA.
int cy = 0;
if (doResize && ListView_GetItemCount(hListView) > 0) {
if (listDataDesc.names) {
// Get the header rect.
HWND hHeader = ListView_GetHeader(hListView);
assert(hHeader != nullptr);
if (hHeader) {
RECT rectListViewHeader;
GetClientRect(hHeader, &rectListViewHeader);
cy = rectListViewHeader.bottom;
}
}
// Get an item rect.
RECT rectItem;
ListView_GetItemRect(hListView, 0, &rectItem, LVIR_BOUNDS);
const int item_cy = (rectItem.bottom - rectItem.top);
if (item_cy > 0) {
// Multiply by the requested number of visible rows.
int rows_visible = field.desc.list_data.rows_visible;
if (rows_visible == 0) {
rows_visible = 5;
}
cy += (item_cy * rows_visible);
// Add half of the dialog margin.
// TODO Get the ListView border size?
cy += (dlgMargin.top/2);
} else {
// TODO: Can't handle this case...
cy = size.cy;
}
} else {
// TODO: Can't handle this if no items are present.
cy = size.cy;
}
// TODO: Skip this if cy == size.cy?
SetWindowPos(hListView, nullptr, 0, 0, size.cx, cy,
SWP_NOACTIVATE | SWP_NOOWNERZORDER | SWP_NOZORDER | SWP_NOMOVE);
return cy;
}
/**
* Initialize a Date/Time field.
* This function internally calls initString().
* @param hDlg [in] Parent dialog window. (for dialog unit mapping)
* @param hWndTab [in] Tab window. (for the actual control)
* @param pt_start [in] Starting position, in pixels.
* @param size [in] Width and height for a single line label.
* @param field [in] RomFields::Field
* @param fieldIdx [in] Field index
* @return Field height, in pixels.
*/
int RP_ShellPropSheetExt_Private::initDateTime(HWND hDlg, HWND hWndTab,
const POINT &pt_start, const SIZE &size,
const RomFields::Field &field, int fieldIdx)
{
if (field.data.date_time == -1) {
// Invalid date/time.
return initString(hDlg, hWndTab, pt_start, size, field, fieldIdx,
U82T_c(C_("RomDataView", "Unknown")));
}
// Format the date/time using the system locale.
TCHAR dateTimeStr[256];
int start_pos = 0;
int cchBuf = _countof(dateTimeStr);
// Convert from Unix time to Win32 SYSTEMTIME.
SYSTEMTIME st;
UnixTimeToSystemTime(field.data.date_time, &st);
// At least one of Date and/or Time must be set.
assert((field.desc.flags &
(RomFields::RFT_DATETIME_HAS_DATE | RomFields::RFT_DATETIME_HAS_TIME)) != 0);
if (!(field.desc.flags & RomFields::RFT_DATETIME_IS_UTC)) {
// Convert to the current timezone.
SYSTEMTIME st_utc = st;
BOOL ret = SystemTimeToTzSpecificLocalTime(nullptr, &st_utc, &st);
if (!ret) {
// Conversion failed.
return 0;
}
}
if (field.desc.flags & RomFields::RFT_DATETIME_HAS_DATE) {
// Format the date.
int ret;
if (field.desc.flags & RomFields::RFT_DATETIME_NO_YEAR) {
// Try Windows 10's DATE_MONTHDAY first.
ret = GetDateFormat(
MAKELCID(LOCALE_USER_DEFAULT, SORT_DEFAULT),
DATE_MONTHDAY,
&st, nullptr, &dateTimeStr[start_pos], cchBuf);
if (ret == 0) {
// DATE_MONTHDAY failed.
// Fall back to a hard-coded format string.
// TODO: Localization.
ret = GetDateFormat(
MAKELCID(LOCALE_USER_DEFAULT, SORT_DEFAULT),
0, &st, _T("MMM d"), &dateTimeStr[start_pos], cchBuf);
}
} else {
ret = GetDateFormat(
MAKELCID(LOCALE_USER_DEFAULT, SORT_DEFAULT),
DATE_SHORTDATE,
&st, nullptr, &dateTimeStr[start_pos], cchBuf);
}
if (ret <= 0) {
// Error!
return 0;
}
// Adjust the buffer position.
// NOTE: ret includes the NULL terminator.
start_pos += (ret-1);
cchBuf -= (ret-1);
}
if (field.desc.flags & RomFields::RFT_DATETIME_HAS_TIME) {
// Format the time.
if (start_pos > 0 && cchBuf >= 1) {
// Add a space.
dateTimeStr[start_pos] = L' ';
dateTimeStr[start_pos+1] = 0;
start_pos++;
cchBuf--;
}
int ret = GetTimeFormat(
MAKELCID(LOCALE_USER_DEFAULT, SORT_DEFAULT),
0, &st, nullptr, &dateTimeStr[start_pos], cchBuf);
if (ret <= 0) {
// Error!
return 0;
}
// Adjust the buffer position.
// NOTE: ret includes the NULL terminator.
start_pos += (ret-1);
cchBuf -= (ret-1);
}
if (start_pos == 0) {
// Empty string.
// Something failed...
return 0;
}
// Initialize the string.
return initString(hDlg, hWndTab, pt_start, size, field, fieldIdx, dateTimeStr);
}
/**
* Initialize an Age Ratings field.
* This function internally calls initString().
* @param hDlg [in] Parent dialog window. (for dialog unit mapping)
* @param hWndTab [in] Tab window. (for the actual control)
* @param pt_start [in] Starting position, in pixels.
* @param size [in] Width and height for a single line label.
* @param field [in] RomFields::Field
* @param fieldIdx [in] Field index
* @return Field height, in pixels.
*/
int RP_ShellPropSheetExt_Private::initAgeRatings(HWND hDlg, HWND hWndTab,
const POINT &pt_start, const SIZE &size,
const RomFields::Field &field, int fieldIdx)
{
const RomFields::age_ratings_t *const age_ratings = field.data.age_ratings;
assert(age_ratings != nullptr);
if (!age_ratings) {
// No age ratings data.
return initString(hDlg, hWndTab, pt_start, size, field, fieldIdx,
U82T_c(C_("RomDataView", "ERROR")));
}
// Convert the age ratings field to a string.
string str = RomFields::ageRatingsDecode(age_ratings);
// Initialize the string field.
return initString(hDlg, hWndTab, pt_start, size, field, fieldIdx, U82T_s(str));
}
/**
* Initialize a Dimensions field.
* This function internally calls initString().
* @param hDlg [in] Parent dialog window. (for dialog unit mapping)
* @param hWndTab [in] Tab window. (for the actual control)
* @param pt_start [in] Starting position, in pixels.
* @param size [in] Width and height for a single line label.
* @param field [in] RomFields::Field
* @param fieldIdx [in] Field index
* @return Field height, in pixels.
*/
int RP_ShellPropSheetExt_Private::initDimensions(HWND hDlg, HWND hWndTab,
const POINT &pt_start, const SIZE &size,
const RomFields::Field &field, int fieldIdx)
{
// TODO: 'x' or '×'? Using 'x' for now.
const int *const dimensions = field.data.dimensions;
TCHAR tbuf[64];
if (dimensions[1] > 0) {
if (dimensions[2] > 0) {
_sntprintf(tbuf, _countof(tbuf), _T("%dx%dx%d"),
dimensions[0], dimensions[1], dimensions[2]);
} else {
_sntprintf(tbuf, _countof(tbuf), _T("%dx%d"),
dimensions[0], dimensions[1]);
}
} else {
_sntprintf(tbuf, _countof(tbuf), _T("%d"), dimensions[0]);
}
// Initialize the string field.
return initString(hDlg, hWndTab, pt_start, size, field, fieldIdx, tbuf);
}
/**
* Initialize a multi-language string field.
* @param hDlg [in] Parent dialog window. (for dialog unit mapping)
* @param hWndTab [in] Tab window. (for the actual control)
* @param pt_start [in] Starting position, in pixels.
* @param size [in] Width and height for a single line label.
* @param field [in] RomFields::Field
* @param fieldIdx [in] Field index
* @return Field height, in pixels.
*/
int RP_ShellPropSheetExt_Private::initStringMulti(HWND hDlg, HWND hWndTab,
const POINT &pt_start, const SIZE &size,
const RomFields::Field &field, int fieldIdx)
{
// Mutli-language string.
// NOTE: The string contents won't be initialized here.
// They will be initialized separately, since the user will
// be able to change the displayed language.
HWND lblStringMulti = nullptr;
int field_cy = initString(hDlg, hWndTab, pt_start, size, field, fieldIdx,
_T(""), &lblStringMulti);
if (lblStringMulti) {
vecStringMulti.emplace_back(std::make_pair(lblStringMulti, &field));
}
return field_cy;
}
/**
* Build the cboLanguage image list.
*/
void RP_ShellPropSheetExt_Private::buildCboLanguageImageList(void)
{
if (cboLanguage) {
// Removing the existing ImageList first.
SendMessage(cboLanguage, CBEM_SETIMAGELIST, 0, (LPARAM)nullptr);
}
if (himglFlags) {
// Deleting the existing ImageList first.
ImageList_Destroy(himglFlags);
himglFlags = nullptr;
}
if (vecStringMulti.empty() || set_lc.size() <= 1) {
// No multi-language string fields, or not enough
// languages for cboLanguage.
return;
}
// Get the icon size for the current DPI.
// Reference: https://docs.microsoft.com/en-us/windows/win32/hidpi/high-dpi-desktop-application-development-on-windows
// TODO: Adjust cboLanguage if necessary?
const UINT dpi = rp_GetDpiForWindow(hDlgSheet);
unsigned int iconSize;
uint16_t flagResource;
if (dpi < 120) {
// [96,120) dpi: Use 16x16.
iconSize = 16;
flagResource = IDP_FLAGS_16x16;
} else if (dpi <= 144) {
// [120,144] dpi: Use 24x24.
// TODO: Maybe needs to be slightly higher?
iconSize = 24;
flagResource = IDP_FLAGS_24x24;
} else {
// >144dpi: Use 32x32.
iconSize = 32;
flagResource = IDP_FLAGS_32x32;
}
// Load the flags sprite sheet.
// TODO: Is premultiplied alpha needed?
// Reference: https://stackoverflow.com/questions/307348/how-to-draw-32-bit-alpha-channel-bitmaps
RpFile_windres *const f_res = new RpFile_windres(HINST_THISCOMPONENT, MAKEINTRESOURCE(flagResource), MAKEINTRESOURCE(RT_PNG));
if (!f_res->isOpen()) {
// Unable to open the resource.
f_res->unref();
return;
}
rp_image *imgFlagsSheet = RpPng::loadUnchecked(f_res);
f_res->unref();
if (!imgFlagsSheet) {
// Unable to load the flags sprite sheet.
return;
}
const int flagStride = imgFlagsSheet->stride() / sizeof(uint32_t);
// Make sure the bitmap has the expected size.
assert(imgFlagsSheet->width() == (iconSize * SystemRegion::FLAGS_SPRITE_SHEET_COLS));
assert(imgFlagsSheet->height() == (iconSize * SystemRegion::FLAGS_SPRITE_SHEET_ROWS));
if (imgFlagsSheet->width() != (iconSize * SystemRegion::FLAGS_SPRITE_SHEET_COLS) ||
imgFlagsSheet->height() != (iconSize * SystemRegion::FLAGS_SPRITE_SHEET_ROWS))
{
// Incorrect size. We can't use it.
imgFlagsSheet->unref();
return;
}
if (dwExStyleRTL != 0) {
// WS_EX_LAYOUTRTL will flip bitmaps in the dropdown box.
// ILC_MIRROR mirrors the bitmaps if the process is mirrored,
// but we can't rely on that being the case, and this option
// was first introduced in Windows XP.
// We'll flip the image here to counteract it.
rp_image *const flipimg = imgFlagsSheet->flip(rp_image::FLIP_H);
assert(flipimg != nullptr);
if (flipimg) {
imgFlagsSheet->unref();
imgFlagsSheet = flipimg;
}
}
// Create the image list.
himglFlags = ImageList_Create(iconSize, iconSize, ILC_COLOR32, 13, 16);
assert(himglFlags != nullptr);
if (!himglFlags) {
// Unable to create the ImageList.
imgFlagsSheet->unref();
return;
}
const BITMAPINFOHEADER bmihDIBSection = {
sizeof(BITMAPINFOHEADER), // biSize
(LONG)iconSize, // biWidth
-(LONG)iconSize, // biHeight (negative for right-side up)
1, // biPlanes
32, // biBitCount
BI_RGB, // biCompression
0, // biSizeImage
(LONG)dpi, // biXPelsPerMeter
(LONG)dpi, // biYPelsPerMeter
0, // biClrUsed
0, // biClrImportant
};
HDC hdcIcon = GetDC(nullptr);
const auto set_lc_cend = set_lc.cend();
for (auto iter = set_lc.cbegin(); iter != set_lc_cend; ++iter) {
int col, row;
int ret = SystemRegion::getFlagPosition(*iter, &col, &row);
assert(ret == 0);
if (ret != 0) {
// Icon not found. Use a blank icon to prevent issues.
col = 3;
row = 3;
}
if (dwExStyleRTL != 0) {
// Flag sprite sheet is flipped for RTL.
col = 3 - col;
}
// Create a DIB section for the sub-icon.
void *pvBits;
HBITMAP hbmIcon = CreateDIBSection(
hdcIcon, // hdc
reinterpret_cast<const BITMAPINFO*>(&bmihDIBSection), // pbmi
DIB_RGB_COLORS, // usage
&pvBits, // ppvBits
nullptr, // hSection
0); // offset
GdiFlush(); // TODO: Not sure if needed here...
assert(hbmIcon != nullptr);
if (hbmIcon) {
// Copy the icon from the sprite sheet.
const size_t rowBytes = iconSize * sizeof(uint32_t);
const uint32_t *pSrc = static_cast<const uint32_t*>(imgFlagsSheet->scanLine(row * iconSize));
pSrc += (col * iconSize);
uint32_t *pDest = static_cast<uint32_t*>(pvBits);
for (UINT bmRow = iconSize; bmRow > 0; bmRow--) {
memcpy(pDest, pSrc, rowBytes);
pDest += iconSize;
pSrc += flagStride;
}
// Add the icon to the ImageList.
GdiFlush();
ImageList_Add(himglFlags, hbmIcon, nullptr);
DeleteBitmap(hbmIcon);
}
}
ReleaseDC(nullptr, hdcIcon);
imgFlagsSheet->unref();
if (cboLanguage) {
// Set the new ImageList.
SendMessage(cboLanguage, CBEM_SETIMAGELIST, 0, (LPARAM)himglFlags);
}
}
/**
* Update all multi-language fields.
* @param user_lc User-specified language code.
*/
void RP_ShellPropSheetExt_Private::updateMulti(uint32_t user_lc)
{
// RFT_STRING_MULTI
const auto vecStringMulti_cend = vecStringMulti.cend();
for (auto iter = vecStringMulti.cbegin();
iter != vecStringMulti_cend; ++iter)
{
const HWND lblString = iter->first;
const RomFields::Field *const pField = iter->second;
const auto *const pStr_multi = pField->data.str_multi;
assert(pStr_multi != nullptr);
assert(!pStr_multi->empty());
if (!pStr_multi || pStr_multi->empty()) {
// Invalid multi-string...
continue;
}
if (!cboLanguage) {
// Need to add all supported languages.
// TODO: Do we need to do this for all of them, or just one?
const auto pStr_multi_cend = pStr_multi->cend();
for (auto iter_sm = pStr_multi->cbegin();
iter_sm != pStr_multi_cend; ++iter_sm)
{
set_lc.insert(iter_sm->first);
}
}
// Get the string and update the text.
const string *const pStr = RomFields::getFromStringMulti(pStr_multi, def_lc, user_lc);
assert(pStr != nullptr);
if (pStr != nullptr) {
SetWindowText(lblString, U82T_s(*pStr));
} else {
SetWindowText(lblString, _T(""));
}
}
// RFT_LISTDATA_MULTI
const auto map_lvData_end = map_lvData.end();
for (auto iter = map_lvData.begin(); iter != map_lvData_end; ++iter) {
LvData_t &lvData = iter->second;
if (!lvData.hListView) {
// Not an RFT_LISTDATA_MULTI.
continue;
}
const HWND hListView = lvData.hListView;
const RomFields::Field *const pField = lvData.pField;
const auto *const pListData_multi = pField->data.list_data.data.multi;
assert(pListData_multi != nullptr);
assert(!pListData_multi->empty());
if (!pListData_multi || pListData_multi->empty()) {
// Invalid RFT_LISTDATA_MULTI...
continue;
}
if (!cboLanguage) {
// Need to add all supported languages.
// TODO: Do we need to do this for all of them, or just one?
const auto pListData_multi_cend = pListData_multi->cend();
for (auto iter_sm = pListData_multi->cbegin();
iter_sm != pListData_multi_cend; ++iter_sm)
{
set_lc.insert(iter_sm->first);
}
}
// Get the ListData_t.
const auto *const pListData = RomFields::getFromListDataMulti(pListData_multi, def_lc, user_lc);
assert(pListData != nullptr);
if (pListData != nullptr) {
// Column count.
const auto &listDataDesc = pField->desc.list_data;
int colCount = 1;
if (listDataDesc.names) {
colCount = (int)listDataDesc.names->size();
} else {
// No column headers.
// Use the first row.
assert(!pListData->empty());
if (!pListData->empty()) {
colCount = (int)pListData->at(0).size();
}
}
// Column widths.
// LVSCW_AUTOSIZE_USEHEADER doesn't work for entries with newlines.
// TODO: Use ownerdraw instead? (WM_MEASUREITEM / WM_DRAWITEM)
unique_ptr<int[]> col_width(new int[colCount]);
for (int i = 0; i < colCount; i++) {
col_width[i] = LVSCW_AUTOSIZE_USEHEADER;
}
// Dialog font and device context.
// NOTE: Using the parent dialog's font.
AutoGetDC hDC(hListView, hFontDlg);
// Get the ListView data vector for LVS_OWNERDATA.
vector<vector<tstring> > &vvStr = lvData.vvStr;
auto iter_ld_row = pListData->cbegin();
auto iter_vvStr_row = vvStr.begin();
const auto pListData_cend = pListData->cend();
const auto vvStr_end = vvStr.end();
for (; iter_ld_row != pListData_cend && iter_vvStr_row != vvStr_end;
++iter_ld_row, ++iter_vvStr_row)
{
const vector<string> &src_data_row = *iter_ld_row;
vector<tstring> &dest_data_row = *iter_vvStr_row;
int col = 0;
auto iter_sdr = src_data_row.cbegin();
auto iter_ddr = dest_data_row.begin();
const auto src_data_row_cend = src_data_row.cend();
const auto dest_data_row_end = dest_data_row.end();
for (; iter_sdr != src_data_row_cend && iter_ddr != dest_data_row_end;
++iter_sdr, ++iter_ddr, col++)
{
tstring tstr = U82T_s(*iter_sdr);
int width = measureListDataString(hDC, tstr);
if (col < colCount) {
col_width[col] = std::max(col_width[col], width);
}
*iter_ddr = std::move(tstr);
}
}
// Resize the columns to fit the contents.
// NOTE: Only done on first load.
// TODO: Need to measure text...
if (!cboLanguage) {
// TODO: Do this on system theme change?
// TODO: Add a flag for 'main data column' and adjust it to
// not exceed the viewport.
for (int i = colCount-1; i >= 0; i--) {
ListView_SetColumnWidth(hListView, i, col_width[i]);
}
}
// Redraw all items.
ListView_RedrawItems(hListView, 0, static_cast<int>(vvStr.size()));
}
}
if (!cboLanguage && set_lc.size() > 1) {
// Create the language combobox.
// Get the language strings and determine the
// maximum width.
SIZE maxSize = {0, 0};
vector<tstring> vec_lc_str;
vec_lc_str.reserve(set_lc.size());
const auto set_lc_cend = set_lc.cend();
for (auto iter = set_lc.cbegin(); iter != set_lc_cend; ++iter) {
const uint32_t lc = *iter;
const char *lc_str = SystemRegion::getLocalizedLanguageName(lc);
if (lc_str) {
vec_lc_str.emplace_back(U82T_c(lc_str));
} else {
// Invalid language code.
tstring s_lc;
s_lc.reserve(4);
for (uint32_t tmp_lc = lc; tmp_lc != 0; tmp_lc <<= 8) {
TCHAR chr = (TCHAR)(tmp_lc >> 24);
if (chr != 0) {
s_lc += chr;
}
}
vec_lc_str.emplace_back(std::move(s_lc));
}
const tstring &tstr = vec_lc_str.at(vec_lc_str.size()-1);
SIZE size;
if (!LibWin32Common::measureTextSize(hDlgSheet, hFontDlg, tstr.c_str(), &size)) {
maxSize.cx = std::max(maxSize.cx, size.cx);
maxSize.cy = std::max(maxSize.cy, size.cy);
}
}
// TODO:
// - Per-monitor DPI scaling (both v1 and v2)
// - Handle WM_DPICHANGED.
// Reference: https://docs.microsoft.com/en-us/windows/win32/hidpi/high-dpi-desktop-application-development-on-windows
const UINT dpi = rp_GetDpiForWindow(hDlgSheet);
unsigned int iconSize;
unsigned int iconMargin;
if (dpi < 120) {
// [96,120) dpi: Use 16x16.
iconSize = 16;
iconMargin = 2;
} else if (dpi <= 144) {
// [120,144] dpi: Use 24x24.
// TODO: Maybe needs to be slightly higher?
iconSize = 24;
iconMargin = 3;
} else {
// >144dpi: Use 32x32.
iconSize = 32;
iconMargin = 4;
}
// Add iconSize + iconMargin for the icon.
maxSize.cx += iconSize + iconMargin;
// Add vertical scrollbar width and CXEDGE.
// Reference: http://ntcoder.com/2013/10/07/mfc-resize-ccombobox-drop-down-list-based-on-contents/
maxSize.cx += rp_GetSystemMetricsForDpi(SM_CXVSCROLL, dpi);
maxSize.cx += (rp_GetSystemMetricsForDpi(SM_CXEDGE, dpi) * 4);
// Create the combobox.
// FIXME: May need to create this after the header row
// in order to preserve tab order. Need to check the
// KDE and GTK+ versions, too.
// ComboBoxEx was introduced in MSIE 3.0.
// NOTE: Height is based on icon size.
cboLanguage = CreateWindowEx(WS_EX_NOPARENTNOTIFY,
WC_COMBOBOXEX, nullptr,
CBS_DROPDOWNLIST | WS_CHILD | WS_TABSTOP | WS_VISIBLE,
rectHeader.right - maxSize.cx, rectHeader.top,
maxSize.cx, iconSize*(8+1) + maxSize.cy - (maxSize.cy / 8),
hDlgSheet, (HMENU)(INT_PTR)IDC_CBO_LANGUAGE,
nullptr, nullptr);
SetWindowFont(cboLanguage, hFontDlg, false);
SendMessage(cboLanguage, CBEM_SETIMAGELIST, 0, (LPARAM)himglFlags);
// Add the strings.
auto iter_str = vec_lc_str.cbegin();
auto iter_lc = set_lc.cbegin();
int sel_idx = -1;
COMBOBOXEXITEM cbItem;
cbItem.mask = CBEIF_TEXT | CBEIF_LPARAM | CBEIF_IMAGE | CBEIF_SELECTEDIMAGE;
cbItem.iItem = 0;
int iImage = 0;
const auto vec_lc_str_cend = vec_lc_str.cend();
for (; iter_str != vec_lc_str_cend; ++iter_str, ++iter_lc, cbItem.iItem++, iImage++) {
const uint32_t lc = *iter_lc;
cbItem.pszText = const_cast<LPTSTR>(iter_str->c_str());
cbItem.cchTextMax = static_cast<int>(iter_str->size());
cbItem.lParam = static_cast<LPARAM>(lc);
cbItem.iImage = iImage;
cbItem.iSelectedImage = iImage;
// Insert the item.
SendMessage(cboLanguage, CBEM_INSERTITEM, 0, (LPARAM)&cbItem);
// Save the default index:
// - ROM-default language code.
// - English if it's not available.
if (lc == def_lc) {
// Select this item.
sel_idx = static_cast<int>(cbItem.iItem);
} else if (lc == 'en') {
// English. Select this item if def_lc hasn't been found yet.
if (sel_idx < 0) {
sel_idx = static_cast<int>(cbItem.iItem);
}
}
}
// Build the ImageList.
buildCboLanguageImageList();
// Set the current index.
ComboBox_SetCurSel(cboLanguage, sel_idx);
// Get the dialog margin.
// 7x7 DLU margin is recommended by the Windows UX guidelines.
// Reference: http://stackoverflow.com/questions/2118603/default-dialog-padding
RECT dlgMargin = { 7, 7, 8, 8 };
MapDialogRect(hDlgSheet, &dlgMargin);
// Adjust the header row.
const int adj = (maxSize.cx + dlgMargin.left) / 2;
if (lblSysInfo) {
ptSysInfo.x -= adj;
SetWindowPos(lblSysInfo, nullptr, ptSysInfo.x, ptSysInfo.y, 0, 0,
SWP_NOSIZE | SWP_NOOWNERZORDER | SWP_NOZORDER);
}
if (lblBanner) {
POINT pos = lblBanner->position();
pos.x -= adj;
lblBanner->setPosition(pos);
}
if (lblIcon) {
POINT pos = lblIcon->position();
pos.x -= adj;
lblIcon->setPosition(pos);
}
}
}
/**
* Update a field's value.
* This is called after running a ROM operation.
* @param fieldIdx Field index.
* @return 0 on success; non-zero on error.
*/
int RP_ShellPropSheetExt_Private::updateField(int fieldIdx)
{
const RomFields *const pFields = romData->fields();
assert(pFields != nullptr);
if (!pFields) {
// No fields.
// TODO: Show an error?
return 1;
}
assert(fieldIdx >= 0);
assert(fieldIdx < pFields->count());
if (fieldIdx < 0 || fieldIdx >= pFields->count())
return 2;
const RomFields::Field *const field = pFields->at(fieldIdx);
assert(field != nullptr);
if (!field)
return 3;
// Get the tab dialog control the control is in.
assert(field->tabIdx >= 0);
assert(field->tabIdx < tabs.size());
if (field->tabIdx < 0 || field->tabIdx >= tabs.size())
return 4;
HWND hDlg = tabs[field->tabIdx].hDlg;
// Update the value widget(s).
int ret;
switch (field->type) {
case RomFields::RFT_INVALID:
assert(!"Cannot update an RFT_INVALID field.");
ret = 5;
break;
default:
assert(!"Unsupported field type.");
ret = 6;
break;
case RomFields::RFT_STRING: {
// HWND is a STATIC control.
HWND hLabel = GetDlgItem(hDlg, IDC_RFT_STRING(fieldIdx));
assert(hLabel != nullptr);
if (!hLabel) {
ret = 7;
break;
}
if (field->data.str && !field->data.str->empty()) {
const tstring ts_text = LibWin32Common::unix2dos(U82T_s(*field->data.str));
SetWindowText(hLabel, ts_text.c_str());
} else {
SetWindowText(hLabel, _T(""));
}
ret = 0;
break;
}
case RomFields::RFT_BITFIELD: {
// Multiple checkboxes with unique dialog IDs.
// Bits with a blank name aren't included, so we'll need to iterate
// over the bitfield description.
const auto &bitfieldDesc = field->desc.bitfield;
int count = (int)bitfieldDesc.names->size();
assert(count <= 32);
if (count > 32)
count = 32;
// Unlike GTK+ and KDE, we don't need to check bitfieldDesc.names to determine
// if a checkbox is present, since GetDlgItem() will return nullptr in that case.
uint32_t bitfield = field->data.bitfield;
int id = IDC_RFT_BITFIELD(fieldIdx, 0);
for (; count >= 0; count--, id++, bitfield >>= 1) {
HWND hCheckBox = GetDlgItem(hDlg, id);
if (!hCheckBox)
continue;
// Set the checkbox.
Button_SetCheck(hCheckBox, (bitfield & 1) ? BST_CHECKED : BST_UNCHECKED);
}
ret = 0;
break;
}
}
return ret;
}
/**
* Initialize the bold font.
* @param hFont Base font.
*/
void RP_ShellPropSheetExt_Private::initBoldFont(HFONT hFont)
{
assert(hFont != nullptr);
if (!hFont || hFontBold) {
// No base font, or the bold font
// is already initialized.
return;
}
// Create the bold font.
LOGFONT lfFontBold;
if (GetObject(hFont, sizeof(lfFontBold), &lfFontBold) != 0) {
// Adjust the font and create a new one.
lfFontBold.lfWeight = FW_BOLD;
hFontBold = CreateFontIndirect(&lfFontBold);
}
}
/**
* Initialize the dialog. (hDlgSheet)
* Called by WM_INITDIALOG.
*/
void RP_ShellPropSheetExt_Private::initDialog(void)
{
assert(hDlgSheet != nullptr);
assert(romData != nullptr);
if (!hDlgSheet || !romData) {
// No dialog, or no ROM data loaded.
return;
}
// Set the dialog to allow automatic right-to-left adjustment
// if the system is using an RTL language.
if (dwExStyleRTL != 0) {
LONG_PTR lpExStyle = GetWindowLongPtr(hDlgSheet, GWL_EXSTYLE);
lpExStyle |= WS_EX_LAYOUTRTL;
SetWindowLongPtr(hDlgSheet, GWL_EXSTYLE, lpExStyle);
}
// Get the fields.
const RomFields *const pFields = romData->fields();
assert(pFields != nullptr);
if (!pFields) {
// No fields.
// TODO: Show an error?
return;
}
const int count = pFields->count();
// Make sure we have all required window classes available.
// Reference: https://msdn.microsoft.com/en-us/library/windows/desktop/bb775507(v=vs.85).aspx
INITCOMMONCONTROLSEX initCommCtrl;
initCommCtrl.dwSize = sizeof(initCommCtrl);
initCommCtrl.dwICC = ICC_LISTVIEW_CLASSES | ICC_LINK_CLASS |
ICC_TAB_CLASSES | ICC_USEREX_CLASSES;
// TODO: Also ICC_STANDARD_CLASSES on XP+?
InitCommonControlsEx(&initCommCtrl);
// Dialog font and device context.
if (!hFontDlg) {
hFontDlg = GetWindowFont(hDlgSheet);
}
AutoGetDC hDC(hDlgSheet, hFontDlg);
// Initialize the fonts.
initBoldFont(hFontDlg);
fontHandler.setWindow(hDlgSheet);
// Convert the bitfield description names to the
// native Windows encoding once.
vector<tstring> t_desc_text;
t_desc_text.reserve(count);
// Determine the maximum length of all field names.
// TODO: Line breaks?
int max_text_width = 0;
SIZE textSize;
// tr: Field description label.
const char *const desc_label_fmt = C_("RomDataView", "%s:");
const auto pFields_cend = pFields->cend();
for (auto iter = pFields->cbegin(); iter != pFields_cend; ++iter) {
const RomFields::Field &field = *iter;
if (!field.isValid) {
t_desc_text.emplace_back(tstring());
continue;
} else if (field.name.empty()) {
t_desc_text.emplace_back(tstring());
continue;
}
const tstring desc_text = U82T_s(rp_sprintf(
desc_label_fmt, field.name.c_str()));
// Get the width of this specific entry.
// TODO: Use measureTextSize()?
if (field.desc.flags & RomFields::STRF_WARNING) {
// Label is bold. Use hFontBold.
HFONT hFontOrig = SelectFont(hDC, hFontBold);
GetTextExtentPoint32(hDC, desc_text.data(),
static_cast<int>(desc_text.size()), &textSize);
SelectFont(hDC, hFontOrig);
} else {
// Regular font.
GetTextExtentPoint32(hDC, desc_text.data(),
static_cast<int>(desc_text.size()), &textSize);
}
if (textSize.cx > max_text_width) {
max_text_width = textSize.cx;
}
// Save for later.
t_desc_text.emplace_back(std::move(desc_text));
}
// Add additional spacing between the ':' and the field.
// TODO: Use measureTextSize()?
// TODO: Reduce to 1 space?
GetTextExtentPoint32(hDC, _T(" "), 2, &textSize);
max_text_width += textSize.cx;
// Create the ROM field widgets.
// Each static control is max_text_width pixels wide
// and 8 DLUs tall, plus 4 vertical DLUs for spacing.
RECT tmpRect = {0, 0, 0, 8+4};
MapDialogRect(hDlgSheet, &tmpRect);
const SIZE descSize = {max_text_width, tmpRect.bottom};
this->lblDescHeight = descSize.cy;
// Get the dialog margin.
// 7x7 DLU margin is recommended by the Windows UX guidelines.
// Reference: http://stackoverflow.com/questions/2118603/default-dialog-padding
RECT dlgMargin = {7, 7, 8, 8};
MapDialogRect(hDlgSheet, &dlgMargin);
// Get the dialog size.
// - fullDlgRect: Full dialog size
// - dlgRect: Adjusted dialog size.
// FIXME: Vertical height is off by 3px on Win7...
// Verified with WinSpy++: expected 341x408, got 341x405.
RECT fullDlgRect, dlgRect;
GetClientRect(hDlgSheet, &fullDlgRect);
dlgRect = fullDlgRect;
// Adjust the rectangle for margins.
InflateRect(&dlgRect, -dlgMargin.left, -dlgMargin.top);
// Calculate the size for convenience purposes.
dlgSize.cx = dlgRect.right - dlgRect.left;
dlgSize.cy = dlgRect.bottom - dlgRect.top;
// Current position.
POINT headerPt = {dlgRect.left, dlgRect.top};
int dlg_value_width = dlgSize.cx - descSize.cx - 1;
// Create the header row.
const SIZE header_size = {dlgSize.cx, descSize.cy};
const int headerH = createHeaderRow(hDlgSheet, headerPt, header_size);
// Save the header rect for later.
rectHeader.left = headerPt.x;
rectHeader.top = headerPt.y;
rectHeader.right = headerPt.x + dlgSize.cx;
rectHeader.bottom = headerPt.y + headerH;
// Adjust values for the tabs.
dlgRect.top += headerH;
dlgSize.cy -= headerH;
headerPt.y += headerH;
// Do we need to create a tab widget?
int tabCount = pFields->tabCount();
if (tabCount > 1) {
// Increase the tab widget width by half of the margin.
InflateRect(&dlgRect, dlgMargin.left/2, 0);
dlgSize.cx += dlgMargin.left - 1;
// TODO: Do this regardless of tabs?
// NOTE: Margin with this change on Win7 is now 9px left, 12px bottom.
dlgRect.bottom = fullDlgRect.bottom - dlgRect.left;
dlgSize.cy = dlgRect.bottom - dlgRect.top;
// Create the tab widget.
tabs.resize(tabCount);
tabWidget = CreateWindowEx(WS_EX_NOPARENTNOTIFY | WS_EX_TRANSPARENT | dwExStyleRTL,
WC_TABCONTROL, nullptr,
WS_CHILD | WS_TABSTOP | WS_VISIBLE,
dlgRect.left, dlgRect.top, dlgSize.cx, dlgSize.cy,
hDlgSheet, (HMENU)(INT_PTR)IDC_TAB_WIDGET,
nullptr, nullptr);
SetWindowFont(tabWidget, hFontDlg, false);
curTabIndex = 0;
// Add tabs.
// NOTE: Tabs must be added *before* calling TabCtrl_AdjustRect();
// otherwise, the tab bar height won't be taken into account.
TCITEM tcItem;
tcItem.mask = TCIF_TEXT;
for (int i = 0; i < tabCount; i++) {
// Create a tab.
const char *name = pFields->tabName(i);
if (!name) {
// Skip this tab.
continue;
}
const tstring tstr = U82T_c(name);
tcItem.pszText = const_cast<LPTSTR>(tstr.c_str());
// FIXME: Does the index work correctly if a tab is skipped?
TabCtrl_InsertItem(tabWidget, i, &tcItem);
}
// Adjust the dialog size for subtabs.
TabCtrl_AdjustRect(tabWidget, false, &dlgRect);
// Update dlgSize.
dlgSize.cx = dlgRect.right - dlgRect.left;
dlgSize.cy = dlgRect.bottom - dlgRect.top;
iTabHeightOrig = dlgSize.cy; // for MessageWidget
// Update dlg_value_width.
// FIXME: Results in 9px left, 8px right margins for RFT_LISTDATA.
dlg_value_width = dlgSize.cx - descSize.cx - dlgMargin.left - 1;
// Create windows for each tab.
DWORD swpFlags = SWP_NOACTIVATE | SWP_NOOWNERZORDER | SWP_NOZORDER | SWP_SHOWWINDOW;
for (int i = 0; i < tabCount; i++) {
if (!pFields->tabName(i)) {
// Skip this tab.
continue;
}
auto &tab = tabs[i];
// Create a child dialog for the tab.
tab.hDlg = CreateDialog(HINST_THISCOMPONENT,
MAKEINTRESOURCE(IDD_SUBTAB_CHILD_DIALOG),
hDlgSheet, SubtabDlgProc);
SetWindowPos(tab.hDlg, nullptr,
dlgRect.left, dlgRect.top,
dlgSize.cx, dlgSize.cy,
swpFlags);
// Hide subsequent tabs.
swpFlags = SWP_NOACTIVATE | SWP_NOOWNERZORDER | SWP_NOZORDER | SWP_HIDEWINDOW;
// Store the D object pointer with this particular tab dialog.
SetProp(tab.hDlg, D_PTR_PROP, static_cast<HANDLE>(this));
// Store the D object pointer with this particular tab dialog.
SetProp(tab.hDlg, TAB_PTR_PROP, static_cast<HANDLE>(&tab));
// Current point should be equal to the margins.
// FIXME: On both WinXP and Win7, ths ends up with an
// 8px left margin, and 6px top/right margins.
// (Bottom margin is 6px on WinXP, 7px on Win7.)
tab.curPt.x = dlgMargin.left/2;
tab.curPt.y = dlgMargin.top/2;
}
} else {
// No tabs.
// Don't create a WC_TABCONTROL, but simulate a single
// tab in tabs[] to make it easier to work with.
tabCount = 1;
tabs.resize(1);
auto &tab = tabs[0];
tab.hDlg = hDlgSheet;
tab.curPt = headerPt;
}
int fieldIdx = 0; // needed for control IDs
auto iter_desc = t_desc_text.cbegin();
for (auto iter = pFields->cbegin(); iter != pFields_cend; ++iter, ++iter_desc, fieldIdx++) {
assert(iter_desc != t_desc_text.cend());
const RomFields::Field &field = *iter;
if (!field.isValid)
continue;
// Verify the tab index.
const int tabIdx = field.tabIdx;
assert(tabIdx >= 0 && tabIdx < (int)tabs.size());
if (tabIdx < 0 || tabIdx >= (int)tabs.size()) {
// Tab index is out of bounds.
continue;
} else if (!tabs[tabIdx].hDlg) {
// Tab name is empty. Tab is hidden.
continue;
}
// Current tab.
auto &tab = tabs[tabIdx];
// Create the static text widget. (FIXME: Disable mnemonics?)
HWND hStatic = CreateWindowEx(WS_EX_NOPARENTNOTIFY | WS_EX_TRANSPARENT,
WC_STATIC, iter_desc->c_str(),
WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS | SS_LEFT,
tab.curPt.x, tab.curPt.y, descSize.cx, descSize.cy,
tab.hDlg, (HMENU)(INT_PTR)(IDC_STATIC_DESC(fieldIdx)),
nullptr, nullptr);
SetWindowFont(hStatic, hFontDlg, false);
// Create the value widget.
int field_cy = descSize.cy; // Default row size.
const POINT pt_start = {tab.curPt.x + descSize.cx, tab.curPt.y};
SIZE size = {dlg_value_width, field_cy};
switch (field.type) {
case RomFields::RFT_INVALID:
// No data here.
field_cy = 0;
break;
case RomFields::RFT_STRING:
// String data.
field_cy = initString(hDlgSheet, tab.hDlg, pt_start, size, field, fieldIdx, nullptr);
break;
case RomFields::RFT_BITFIELD:
// Create checkboxes starting at the current point.
field_cy = initBitfield(hDlgSheet, tab.hDlg, pt_start, field, fieldIdx);
break;
case RomFields::RFT_LISTDATA: {
// Create a ListView control.
size.cy *= 6; // TODO: Is this needed?
POINT pt_ListData = pt_start;
// Should the RFT_LISTDATA be placed on its own row?
bool doVBox = false;
if (field.desc.list_data.flags & RomFields::RFT_LISTDATA_SEPARATE_ROW) {
// Separate row.
size.cx = dlgSize.cx - 1;
// NOTE: This varies depending on if we have subtabs.
if (tabCount > 1) {
// Subtract the dialog margin.
size.cx -= dlgMargin.left;
} else {
// Subtract another pixel.
size.cx--;
}
pt_ListData.x = tab.curPt.x;
pt_ListData.y += (descSize.cy - (dlgMargin.top/3));
// If this is the last RFT_LISTDATA in the tab,
// extend it vertically.
if (tabIdx + 1 == tabCount && fieldIdx == count-1) {
// Last tab, and last field.
doVBox = true;
} else {
// Check if the next field is on the next tab.
RomFields::const_iterator nextIter = iter;
++nextIter;
if (nextIter != pFields_cend && nextIter->tabIdx != tabIdx) {
// Next field is on the next tab.
doVBox = true;
}
}
if (doVBox) {
// Extend it vertically.
size.cy = dlgSize.cy - pt_ListData.y;
if (tabCount > 1) {
// FIXME: This seems a bit wonky...
size.cy -= ((dlgMargin.top / 2) + 1);
} else {
// This also seems wonky...
size.cy += dlgRect.top - 1;
}
}
}
field_cy = initListData(hDlgSheet, tab.hDlg, pt_ListData, size, !doVBox, field, fieldIdx);
if (field_cy > 0) {
// Add the extra row if necessary.
if (field.desc.list_data.flags & RomFields::RFT_LISTDATA_SEPARATE_ROW) {
const int szAdj = descSize.cy - (dlgMargin.top/3);
field_cy += szAdj;
// Reduce the hStatic size slightly.
SetWindowPos(hStatic, nullptr, 0, 0, descSize.cx, szAdj,
SWP_NOACTIVATE | SWP_NOOWNERZORDER | SWP_NOZORDER | SWP_NOMOVE);
}
}
break;
}
case RomFields::RFT_DATETIME:
// Date/Time in Unix format.
field_cy = initDateTime(hDlgSheet, tab.hDlg, pt_start, size, field, fieldIdx);
break;
case RomFields::RFT_AGE_RATINGS:
// Age Ratings field.
field_cy = initAgeRatings(hDlgSheet, tab.hDlg, pt_start, size, field, fieldIdx);
break;
case RomFields::RFT_DIMENSIONS:
// Dimensions field.
field_cy = initDimensions(hDlgSheet, tab.hDlg, pt_start, size, field, fieldIdx);
break;
case RomFields::RFT_STRING_MULTI:
// Multi-language string field.
field_cy = initStringMulti(hDlgSheet, tab.hDlg, pt_start, size, field, fieldIdx);
break;
default:
// Unsupported data type.
assert(!"Unsupported RomFields::RomFieldsType.");
field_cy = 0;
break;
}
if (field_cy > 0) {
// Next row.
tab.curPt.y += field_cy;
} else /* if (field_cy == 0) */ {
// Failed to initialize the widget.
// Remove the description label.
DestroyWindow(hStatic);
}
}
// Update scrollbar settings.
// TODO: If a VScroll bar is added, adjust widths of RFT_LISTDATA.
// TODO: HScroll bar?
const auto tabs_cend = tabs.cend();
for (auto iter = tabs.cbegin(); iter != tabs_cend; ++iter) {
// Set scroll info.
// FIXME: Separate child dialog for no tabs.
const auto &tab = *iter;
// VScroll bar
SCROLLINFO si;
si.cbSize = sizeof(SCROLLINFO);
si.fMask = SIF_ALL;
si.nMin = 0;
si.nMax = tab.curPt.y - 2; // max is exclusive
si.nPage = dlgSize.cy;
si.nPos = 0;
SetScrollInfo(tab.hDlg, SB_VERT, &si, TRUE);
}
// Initial update of RFT_MULTI_STRING fields.
if (!vecStringMulti.empty()) {
def_lc = pFields->defaultLanguageCode();
updateMulti(0);
}
// Register for WTS session notifications. (Remote Desktop)
wts.registerSessionNotification(hDlgSheet, NOTIFY_FOR_THIS_SESSION);
// Window is fully initialized.
isFullyInit = true;
}
/**
* Adjust tabs for the message widget.
* Message widget must have been created first.
* @param bVisible True for visible; false for not.
*/
void RP_ShellPropSheetExt_Private::adjustTabsForMessageWidgetVisibility(bool bVisible)
{
if (tabs.size() == 1) {
// Only one tab. Nothing to do here.
return;
}
// NOTE: IsWindowVisible(hMessageWidget) doesn't seem to be
// correct when this function is called, so we have to take
// the visibility as a parameter instead.
RECT rectMsgw;
GetClientRect(hMessageWidget, &rectMsgw);
int tab_h = iTabHeightOrig;
if (bVisible) {
tab_h -= rectMsgw.bottom;
}
std::for_each(tabs.cbegin(), tabs.cend(),
[tab_h](const tab &tab) {
RECT tabRect;
GetClientRect(tab.hDlg, &tabRect);
if (tabRect.bottom != tab_h) {
SetWindowPos(tab.hDlg, nullptr, 0, 0, tabRect.right, tab_h,
SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOOWNERZORDER | SWP_NOZORDER);
}
}
);
}
/**
* Show the message widget.
* Message widget must have been created first.
* @param messageType Message type.
* @param lpszMsg Message.
*/
void RP_ShellPropSheetExt_Private::showMessageWidget(unsigned int messageType, const TCHAR *lpszMsg)
{
assert(hMessageWidget != nullptr);
if (!hMessageWidget)
return;
// Set the message widget stuff.
SendMessage(hMessageWidget, WM_MSGW_SET_MESSAGE_TYPE, messageType, 0);
SetWindowText(hMessageWidget, lpszMsg);
adjustTabsForMessageWidgetVisibility(true);
ShowWindow(hMessageWidget, SW_SHOW);
}
/**
* An "Options" menu action was triggered.
* @param menuId Menu ID. (Options ID + IDM_OPTIONS_MENU_BASE)
*/
void RP_ShellPropSheetExt_Private::menuOptions_action_triggered(int menuId)
{
if (menuId < IDM_OPTIONS_MENU_BASE) {
// Export to text or JSON.
const char *const rom_filename = romData->filename();
if (!rom_filename)
return;
bool toClipboard;
tstring ts_title;
const TCHAR *ts_default_ext = nullptr;
const char *s_filter = nullptr;
switch (menuId) {
case IDM_OPTIONS_MENU_EXPORT_TEXT:
toClipboard = false;
ts_title = U82T_c(C_("RomDataView", "Export to Text File"));
ts_default_ext = _T(".txt");
// tr: Text files filter. (RP format)
s_filter = C_("RomDataView", "Text Files|*.txt|text/plain|All Files|*.*|-");
break;
case IDM_OPTIONS_MENU_EXPORT_JSON:
toClipboard = false;
ts_title = U82T_c(C_("RomDataView", "Export to JSON File"));
ts_default_ext = _T(".json");
// tr: JSON files filter. (RP format)
s_filter = C_("RomDataView", "JSON Files|*.json|application/json|All Files|*.*|-");
break;
case IDM_OPTIONS_MENU_COPY_TEXT:
case IDM_OPTIONS_MENU_COPY_JSON:
toClipboard = true;
break;
default:
assert(!"Invalid action ID.");
return;
}
ofstream ofs;
tstring ts_out;
if (!toClipboard) {
if (ts_prevExportDir.empty()) {
ts_prevExportDir = U82T_c(rom_filename);
// Remove the filename portion.
size_t bspos = ts_prevExportDir.rfind(_T('\\'));
if (bspos != string::npos) {
if (bspos > 2) {
ts_prevExportDir.resize(bspos);
} else if (bspos == 2) {
ts_prevExportDir.resize(3);
}
}
}
tstring defaultFileName = ts_prevExportDir;
if (!defaultFileName.empty() && defaultFileName.at(defaultFileName.size()-1) != _T('\\')) {
defaultFileName += _T('\\');
}
// Get the base name of the ROM.
tstring rom_basename;
const char *const bspos = strrchr(rom_filename, '\\');
if (bspos) {
rom_basename = U82T_c(bspos+1);
} else {
rom_basename = U82T_c(rom_filename);
}
// Remove the extension, if present.
size_t extpos = rom_basename.rfind(_T('.'));
if (extpos != string::npos) {
rom_basename.resize(extpos);
}
defaultFileName += rom_basename + ts_default_ext;
const tstring tfilename = LibWin32Common::getSaveFileName(hDlgSheet,
ts_title.c_str(), s_filter, defaultFileName.c_str());
if (tfilename.empty())
return;
// Save the previous export directory.
ts_prevExportDir = tfilename;
size_t bspos2 = ts_prevExportDir.rfind(_T('\\'));
if (bspos2 != tstring::npos && bspos2 > 3) {
ts_prevExportDir.resize(bspos2);
}
#ifdef __GNUC__
// FIXME: MinGW doesn't have wchar_t overloads.
ofs.open(T2U8(tfilename).c_str(), ofstream::out);
#else /* !__GNUC__ */
ofs.open(tfilename.c_str(), ofstream::out);
#endif /* __GNUC__ */
if (ofs.fail())
return;
}
// TODO: Optimize this such that we can pass ofstream or ostringstream
// to a factored-out function.
switch (menuId) {
case IDM_OPTIONS_MENU_EXPORT_TEXT: {
ofs << "== " << rp_sprintf(C_("RomDataView", "File: '%s'"), rom_filename) << '\n';
ROMOutput ro(romData, sel_lc());
ofs << ro;
ofs.flush();
break;
}
case IDM_OPTIONS_MENU_EXPORT_JSON: {
JSONROMOutput jsro(romData);
ofs << jsro << '\n';
ofs.flush();
break;
}
case IDM_OPTIONS_MENU_COPY_TEXT: {
// NOTE: Some fields may have embedded newlines,
// so we'll need to convert everything afterwards.
ostringstream oss;
oss << "== " << rp_sprintf(C_("RomDataView", "File: '%s'"), rom_filename) << '\n';
ROMOutput ro(romData, sel_lc());
oss << ro;
oss.flush();
ts_out = LibWin32Common::unix2dos(U82T_s(oss.str()));
break;
}
case IDM_OPTIONS_MENU_COPY_JSON: {
ostringstream oss;
JSONROMOutput jsro(romData);
jsro.setCrlf(true);
oss << jsro << '\n';
oss.flush();
ts_out = U82T_s(oss.str());
break;
}
default:
assert(!"Invalid action ID.");
return;
}
if (toClipboard) {
if (OpenClipboard(hDlgSheet)) {
EmptyClipboard();
HGLOBAL hglbCopy = GlobalAlloc(GMEM_MOVEABLE, (ts_out.size() + 1) * sizeof(TCHAR));
if (hglbCopy) {
LPTSTR lpszCopy = static_cast<LPTSTR>(GlobalLock(hglbCopy));
memcpy(lpszCopy, ts_out.data(), ts_out.size() * sizeof(TCHAR));
lpszCopy[ts_out.size()] = _T('\0');
GlobalUnlock(hglbCopy);
SetClipboardData(CF_UNICODETEXT, hglbCopy);
}
CloseClipboard();
}
}
return;
}
// Run a ROM operation.
// TODO: Don't keep rebuilding this vector...
vector<RomData::RomOp> ops = romData->romOps();
const int id = menuId - IDM_OPTIONS_MENU_BASE;
assert(id < (int)ops.size());
if (id >= (int)ops.size()) {
// ID is out of range.
return;
}
string s_save_filename;
RomData::RomOpParams params;
const RomData::RomOp *op = &ops[id];
if (op->flags & RomData::RomOp::ROF_SAVE_FILE) {
// Add the "All Files" filter.
string filter = op->sfi.filter;
if (!filter.empty()) {
// Make sure the last field isn't empty.
if (filter.at(filter.size()-1) == '|') {
filter += '-';
}
filter += '|';
}
// tr: "All Files" filter (RP format)
filter += C_("RomData", "All Files|*.*|-");
// Initial file and directory, based on the current file.
string initialFile = FileSystem::replace_ext(romData->filename(), op->sfi.ext);
// Prompt for a save file.
tstring tstr = LibWin32Common::getSaveFileName(hDlgSheet,
U82T_c(op->sfi.title), filter.c_str(), U82T_s(initialFile));
if (tstr.empty())
return;
s_save_filename = T2U8(tstr);
params.save_filename = s_save_filename.c_str();
}
int ret = romData->doRomOp(id, &params);
unsigned int messageType;
if (ret == 0) {
// ROM operation completed.
messageType = MB_ICONINFORMATION;
// Update fields.
std::for_each(params.fieldIdx.cbegin(), params.fieldIdx.cend(),
[this](int fieldIdx) {
this->updateField(fieldIdx);
}
);
// Update the RomOp menu entry in case it changed.
// NOTE: Assuming the RomOps vector order hasn't changed.
ops = romData->romOps();
assert(id < (int)ops.size());
if (id < (int)ops.size()) {
const RomData::RomOp &op = ops[id];
UINT uFlags;
if (!(op.flags & RomData::RomOp::ROF_ENABLED)) {
uFlags = MF_BYCOMMAND | MF_STRING | MF_DISABLED;
} else {
uFlags = MF_BYCOMMAND | MF_STRING;
}
ModifyMenu(hMenuOptions, menuId, uFlags, menuId, U82T_c(op.desc));
}
} else {
// An error occurred...
// TODO: Show an error message.
messageType = MB_ICONWARNING;
}
MessageBeep(messageType);
if (!params.msg.empty()) {
if (!hMessageWidget) {
// FIXME: Make sure this works if multiple tabs are present.
MessageWidgetRegister();
// Align to the bottom of the dialog and center-align the text.
// 7x7 DLU margin is recommended by the Windows UX guidelines.
// Reference: http://stackoverflow.com/questions/2118603/default-dialog-padding
RECT tmpRect = {7, 7, 8, 8};
MapDialogRect(hDlgSheet, &tmpRect);
RECT winRect;
GetClientRect(hDlgSheet, &winRect);
// NOTE: We need to move left by 1px.
OffsetRect(&winRect, -1, 0);
// Determine the position.
// TODO: Update on DPI change.
const int cySmIcon = GetSystemMetrics(SM_CYSMICON);
POINT ptMsgw; SIZE szMsgw;
szMsgw.cy = cySmIcon + 8;
ptMsgw.x = winRect.left + tmpRect.left;
ptMsgw.y = winRect.bottom - tmpRect.top - szMsgw.cy;
szMsgw.cx = winRect.right - winRect.left - (tmpRect.left * 2);
hMessageWidget = CreateWindowEx(
WS_EX_NOPARENTNOTIFY | WS_EX_TRANSPARENT | dwExStyleRTL,
WC_MESSAGEWIDGET, nullptr,
WS_CHILD | WS_TABSTOP | WS_CLIPSIBLINGS | WS_CLIPCHILDREN,
ptMsgw.x, ptMsgw.y, szMsgw.cx, szMsgw.cy,
hDlgSheet, (HMENU)IDC_MESSAGE_WIDGET,
HINST_THISCOMPONENT, nullptr);
SetWindowFont(hMessageWidget, hFontDlg, false);
}
showMessageWidget(messageType, U82T_s(params.msg));
}
}
/**
* Dialog subclass procedure to intercept WM_COMMAND for the "Options" button.
* @param hWnd
* @param uMsg
* @param wParam
* @param lParam
* @param uIdSubclass
* @param dWRefData RP_ShellPropSheetExt_Private
*/
LRESULT CALLBACK RP_ShellPropSheetExt_Private::MainDialogSubclassProc(
HWND hWnd, UINT uMsg,
WPARAM wParam, LPARAM lParam,
UINT_PTR uIdSubclass, DWORD_PTR dwRefData)
{
switch (uMsg) {
case WM_NCDESTROY:
// Remove the window subclass.
// Reference: https://blogs.msdn.microsoft.com/oldnewthing/20031111-00/?p=41883
RemoveWindowSubclass(hWnd, MainDialogSubclassProc, uIdSubclass);
break;
case WM_COMMAND:
if (HIWORD(wParam) == BN_CLICKED && LOWORD(wParam) == IDC_RP_OPTIONS) {
// Pop up the menu.
auto *const d = reinterpret_cast<RP_ShellPropSheetExt_Private*>(dwRefData);
assert(d->hBtnOptions != nullptr);
assert(d->hMenuOptions != nullptr);
if (!d->hBtnOptions || !d->hMenuOptions)
break;
// Get the absolute position of the "Options" button.
RECT rect_btnOptions;
GetWindowRect(d->hBtnOptions, &rect_btnOptions);
int id = TrackPopupMenu(d->hMenuOptions,
TPM_LEFTALIGN | TPM_BOTTOMALIGN | TPM_VERNEGANIMATION |
TPM_NONOTIFY | TPM_RETURNCMD,
rect_btnOptions.left, rect_btnOptions.top, 0,
hWnd, nullptr);
if (id != 0) {
d->menuOptions_action_triggered(id);
}
return TRUE;
}
break;
default:
break;
}
return DefSubclassProc(hWnd, uMsg, wParam, lParam);
}
/**
* Create the "Options" button in the parent window.
* Called by WM_INITDIALOG.
*/
void RP_ShellPropSheetExt_Private::createOptionsButton(void)
{
assert(hDlgSheet != nullptr);
assert(romData != nullptr);
if (!hDlgSheet || !romData) {
// No dialog, or no ROM data loaded.
return;
}
HWND hWndParent = GetParent(hDlgSheet);
assert(hWndParent != nullptr);
if (!hWndParent) {
// No parent window...
return;
}
// is the "Options" button already present?
if (GetDlgItem(hWndParent, IDC_RP_OPTIONS) != nullptr) {
assert(!"IDC_RP_OPTIONS is already created.");
return;
}
// TODO: Verify RTL positioning.
HWND hBtnOK = GetDlgItem(hWndParent, IDOK);
HWND hTabControl = PropSheet_GetTabControl(hWndParent);
if (!hBtnOK || !hTabControl) {
return;
}
RECT rect_btnOK, rect_tabControl;
GetWindowRect(hBtnOK, &rect_btnOK);
GetWindowRect(hTabControl, &rect_tabControl);
MapWindowPoints(HWND_DESKTOP, hWndParent, (LPPOINT)&rect_btnOK, 2);
MapWindowPoints(HWND_DESKTOP, hWndParent, (LPPOINT)&rect_tabControl, 2);
// Create the "Options" button.
POINT ptBtn = {rect_tabControl.left, rect_btnOK.top};
const SIZE szBtn = {
rect_btnOK.right - rect_btnOK.left,
rect_btnOK.bottom - rect_btnOK.top
};
const bool isComCtl32_v610 = LibWin32Common::isComCtl32_v610();
tstring ts_caption;
LONG lStyle = WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_GROUP | BS_PUSHBUTTON | BS_CENTER;
if (isComCtl32_v610) {
// COMCTL32 is v6.10 or later. Use BS_SPLITBUTTON.
// (Windows Vista or later)
lStyle |= BS_SPLITBUTTON;
// tr: "Options" button.
ts_caption = U82T_c(C_("RomDataView", "Op&tions"));
} else {
// COMCTL32 is older than v6.10. Use a regular button.
// NOTE: The Unicode down arrow doesn't show on on Windows XP.
// Maybe we *should* use ownerdraw...
// tr: "Options" button. (WinXP version, with ellipsis.)
ts_caption = U82T_c(C_("RomDataView", "Op&tions..."));
}
hBtnOptions = CreateWindowEx(dwExStyleRTL, WC_BUTTON,
ts_caption.c_str(), lStyle,
ptBtn.x, ptBtn.y, szBtn.cx, szBtn.cy,
hWndParent, (HMENU)IDC_RP_OPTIONS, nullptr, nullptr);
SetWindowFont(hBtnOptions, hFontDlg, FALSE);
if (isComCtl32_v610) {
BUTTON_SPLITINFO bsi;
bsi.mask = BCSIF_STYLE;
bsi.uSplitStyle = BCSS_NOSPLIT;
Button_SetSplitInfo(hBtnOptions, &bsi);
}
// Fix up the tab order. ("Options" should be after "Apply".)
HWND hBtnApply = GetDlgItem(hWndParent, IDC_APPLY_BUTTON);
if (hBtnApply) {
SetWindowPos(hBtnOptions, hBtnApply,
0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE);
}
// Subclass the parent dialog so we can intercept WM_COMMAND.
SetWindowSubclass(hWndParent, MainDialogSubclassProc,
static_cast<UINT_PTR>(IDC_RP_OPTIONS),
reinterpret_cast<DWORD_PTR>(this));
// Create the menu.
hMenuOptions = CreatePopupMenu();
/** Standard actions. **/
static const struct {
const char *desc;
unsigned int id;
} stdacts[] = {
{NOP_C_("RomDataView|Options", "Export to Text..."), IDM_OPTIONS_MENU_EXPORT_TEXT},
{NOP_C_("RomDataView|Options", "Export to JSON..."), IDM_OPTIONS_MENU_EXPORT_JSON},
{NOP_C_("RomDataView|Options", "Copy as Text"), IDM_OPTIONS_MENU_COPY_TEXT},
{NOP_C_("RomDataView|Options", "Copy as JSON"), IDM_OPTIONS_MENU_COPY_JSON},
{nullptr, 0}
};
for (const auto *p = stdacts; p->desc != nullptr; p++) {
AppendMenu(hMenuOptions, MF_STRING, p->id,
U82T_c(dpgettext_expr(RP_I18N_DOMAIN, "RomDataView|Options", p->desc)));
}
/** ROM operations. **/
const vector<RomData::RomOp> ops = romData->romOps();
if (!ops.empty()) {
AppendMenu(hMenuOptions, MF_SEPARATOR, 0, nullptr);
unsigned int i = IDM_OPTIONS_MENU_BASE;
const auto ops_end = ops.cend();
for (auto iter = ops.cbegin(); iter != ops_end; ++iter, i++) {
UINT uFlags;
if (!(iter->flags & RomData::RomOp::ROF_ENABLED)) {
uFlags = MF_STRING | MF_DISABLED;
} else {
uFlags = MF_STRING;
}
AppendMenu(hMenuOptions, uFlags, i, U82T_c(iter->desc));
}
}
}
/** RP_ShellPropSheetExt **/
RP_ShellPropSheetExt::RP_ShellPropSheetExt()
: d_ptr(nullptr)
{
// NOTE: d_ptr is not initialized until we receive a valid
// ROM file. This reduces overhead in cases where there are
// lots of files with ROM-like file extensions but aren't
// actually supported by rom-properties.
}
RP_ShellPropSheetExt::~RP_ShellPropSheetExt()
{
delete d_ptr;
}
/** IUnknown **/
// Reference: https://msdn.microsoft.com/en-us/library/office/cc839627.aspx
IFACEMETHODIMP RP_ShellPropSheetExt::QueryInterface(REFIID riid, LPVOID *ppvObj)
{
#ifdef _MSC_VER
# pragma warning(push)
# pragma warning(disable: 4365 4838)
#endif /* _MSC_VER */
static const QITAB rgqit[] = {
QITABENT(RP_ShellPropSheetExt, IShellExtInit),
QITABENT(RP_ShellPropSheetExt, IShellPropSheetExt),
{ 0, 0 }
};
#ifdef _MSC_VER
# pragma warning(pop)
#endif /* _MSC_VER */
return LibWin32Common::rp_QISearch(this, rgqit, riid, ppvObj);
}
/** IShellExtInit **/
/** IShellPropSheetExt **/
// References:
// - https://msdn.microsoft.com/en-us/library/windows/desktop/bb775094(v=vs.85).aspx
IFACEMETHODIMP RP_ShellPropSheetExt::Initialize(
LPCITEMIDLIST pidlFolder, LPDATAOBJECT pDataObj, HKEY hKeyProgID)
{
((void)pidlFolder);
((void)hKeyProgID);
// Based on CppShellExtPropSheetHandler.
// https://code.msdn.microsoft.com/windowsapps/CppShellExtPropSheetHandler-d93b49b7
if (!pDataObj) {
return E_INVALIDARG;
}
// TODO: Handle CFSTR_MOUNTEDVOLUME for volumes mounted on an NTFS mount point.
FORMATETC fe = { CF_HDROP, NULL, DVASPECT_CONTENT, -1, TYMED_HGLOBAL };
STGMEDIUM stm;
// The pDataObj pointer contains the objects being acted upon. In this
// example, we get an HDROP handle for enumerating the selected files and
// folders.
if (FAILED(pDataObj->GetData(&fe, &stm)))
return E_FAIL;
// Get an HDROP handle.
HDROP hDrop = static_cast<HDROP>(GlobalLock(stm.hGlobal));
if (!hDrop) {
ReleaseStgMedium(&stm);
return E_FAIL;
}
HRESULT hr = E_FAIL;
UINT nFiles, cchFilename;
TCHAR *tfilename = nullptr;
RpFile *file = nullptr;
RomData *romData = nullptr;
string u8filename;
const Config *config;
// Determine how many files are involved in this operation. This
// code sample displays the custom context menu item when only
// one file is selected.
nFiles = DragQueryFile(hDrop, 0xFFFFFFFF, NULL, 0);
if (nFiles != 1) {
// Wrong file count.
goto cleanup;
}
// Get the path of the file.
cchFilename = DragQueryFile(hDrop, 0, nullptr, 0);
if (cchFilename == 0) {
// No filename.
goto cleanup;
}
tfilename = new TCHAR[cchFilename+1];
cchFilename = DragQueryFile(hDrop, 0, tfilename, cchFilename+1);
if (cchFilename == 0) {
// No filename.
goto cleanup;
}
// Convert the filename to UTF-8.
u8filename = T2U8(tfilename, cchFilename);
// Check for "bad" file systems.
config = Config::instance();
if (FileSystem::isOnBadFS(u8filename.c_str(),
config->enableThumbnailOnNetworkFS()))
{
// This file is on a "bad" file system.
goto cleanup;
}
// Open the file.
file = new RpFile(u8filename.c_str(), RpFile::FM_OPEN_READ_GZ);
if (!file->isOpen()) {
// Unable to open the file.
goto cleanup;
}
// Get the appropriate RomData class for this ROM.
// file is dup()'d by RomData.
romData = RomDataFactory::create(file);
if (!romData) {
// Could not open the RomData object.
goto cleanup;
}
// Unreference the RomData object.
// We only want to open the RomData if the "ROM Properties"
// tab is clicked, because otherwise the file will be held
// open and may block the user from changing attributes.
romData->unref();
// Save the filename in the private class for later.
if (!d_ptr) {
d_ptr = new RP_ShellPropSheetExt_Private(this, std::move(u8filename));
}
hr = S_OK;
cleanup:
UNREF(file);
GlobalUnlock(stm.hGlobal);
ReleaseStgMedium(&stm);
delete[] tfilename;
// If any value other than S_OK is returned from the method, the property
// sheet is not displayed.
return hr;
}
/** IShellPropSheetExt **/
IFACEMETHODIMP RP_ShellPropSheetExt::AddPages(LPFNADDPROPSHEETPAGE pfnAddPage, LPARAM lParam)
{
// Based on CppShellExtPropSheetHandler.
// https://code.msdn.microsoft.com/windowsapps/CppShellExtPropSheetHandler-d93b49b7
if (!d_ptr) {
// Not initialized.
return E_FAIL;
}
// tr: Tab title.
const tstring tsTabTitle = U82T_c(C_("RomDataView", "ROM Properties"));
// Create a property sheet page.
PROPSHEETPAGE psp;
psp.dwSize = sizeof(psp);
psp.dwFlags = PSP_USECALLBACK | PSP_USETITLE;
psp.hInstance = HINST_THISCOMPONENT;
psp.pszTemplate = MAKEINTRESOURCE(IDD_PROPERTY_SHEET);
psp.pszIcon = nullptr;
psp.pszTitle = tsTabTitle.c_str();
psp.pfnDlgProc = RP_ShellPropSheetExt_Private::DlgProc;
psp.pcRefParent = nullptr;
psp.pfnCallback = RP_ShellPropSheetExt_Private::CallbackProc;
psp.lParam = reinterpret_cast<LPARAM>(this);
HPROPSHEETPAGE hPage = CreatePropertySheetPage(&psp);
if (hPage == NULL) {
return E_OUTOFMEMORY;
}
// The property sheet page is then added to the property sheet by calling
// the callback function (LPFNADDPROPSHEETPAGE pfnAddPage) passed to
// IShellPropSheetExt::AddPages.
if (pfnAddPage(hPage, lParam)) {
// By default, after AddPages returns, the shell releases its
// IShellPropSheetExt interface and the property page cannot access the
// extension object. However, it is sometimes desirable to be able to use
// the extension object, or some other object, from the property page. So
// we increase the reference count and maintain this object until the
// page is released in PropPageCallbackProc where we call Release upon
// the extension.
this->AddRef();
} else {
DestroyPropertySheetPage(hPage);
return E_FAIL;
}
// If any value other than S_OK is returned from the method, the property
// sheet is not displayed.
return S_OK;
}
IFACEMETHODIMP RP_ShellPropSheetExt::ReplacePage(UINT uPageID, LPFNADDPROPSHEETPAGE pfnReplaceWith, LPARAM lParam)
{
// Not used.
RP_UNUSED(uPageID);
RP_UNUSED(pfnReplaceWith);
RP_UNUSED(lParam);
return E_NOTIMPL;
}
/** Property sheet callback functions. **/
/**
* ListView GetDispInfo function.
* @param plvdi [in/out] NMLVDISPINFO
* @return TRUE if handled; FALSE if not.
*/
inline BOOL RP_ShellPropSheetExtPrivate::ListView_GetDispInfo(NMLVDISPINFO *plvdi)
{
// TODO: Assertions for row/column indexes.
LVITEM *const plvItem = &plvdi->item;
const unsigned int idFrom = static_cast<unsigned int>(plvdi->hdr.idFrom);
bool ret = false;
auto iter_lvData = map_lvData.find(idFrom);
if (iter_lvData == map_lvData.end()) {
// ListView data not found...
return ret;
}
const LvData_t &lvData = iter_lvData->second;
if (plvItem->mask & LVIF_TEXT) {
// Fill in text.
const auto &vvStr = lvData.vvStr;
// Is this row in range?
if (plvItem->iItem >= 0 && plvItem->iItem < static_cast<int>(vvStr.size())) {
// Get the row data.
const auto &row_data = vvStr.at(plvItem->iItem);
// Is the column in range?
if (plvItem->iSubItem >= 0 && plvItem->iSubItem < static_cast<int>(row_data.size())) {
// Return the string data.
_tcscpy_s(plvItem->pszText, plvItem->cchTextMax, row_data[plvItem->iSubItem].c_str());
ret = true;
}
}
}
if (plvItem->mask & LVIF_IMAGE) {
// Fill in the ImageList index.
// NOTE: Only valid for the base item.
if (plvItem->iSubItem == 0) {
// LVS_OWNERDATA prevents LVS_EX_CHECKBOXES from working correctly,
// so we have to implement it here.
// Reference: https://www.codeproject.com/Articles/29064/CGridListCtrlEx-Grid-Control-Based-on-CListCtrl
if (lvData.hasCheckboxes) {
// We have checkboxes.
// Enable state handling.
plvItem->mask |= LVIF_STATE;
plvItem->stateMask = LVIS_STATEIMAGEMASK;
plvItem->state = INDEXTOSTATEIMAGEMASK(
((lvData.checkboxes & (1U << plvItem->iItem)) ? 2 : 1));
ret = true;
} else if (!lvData.vImageList.empty()) {
// We have an ImageList.
// Is this row in range?
if (plvItem->iItem >= 0 && plvItem->iItem < static_cast<int>(lvData.vImageList.size())) {
const int iImage = lvData.vImageList.at(plvItem->iItem);
if (iImage >= 0) {
// Set the ImageList index.
plvItem->iImage = iImage;
ret = true;
}
}
}
}
}
return ret;
}
/**
* ListView CustomDraw function.
* @param plvcd [in/out] NMLVCUSTOMDRAW
* @return Return value.
*/
inline int RP_ShellPropSheetExt_Private::ListView_CustomDraw(NMLVCUSTOMDRAW *plvcd)
{
int result = CDRF_DODEFAULT;
switch (plvcd->nmcd.dwDrawStage) {
case CDDS_PREPAINT:
// Request notifications for individual ListView items.
result = CDRF_NOTIFYITEMDRAW;
break;
case CDDS_ITEMPREPAINT: {
// Set the background color for alternating row colors.
if (plvcd->nmcd.dwItemSpec % 2) {
// NOTE: plvcd->clrTextBk is set to 0xFF000000 here,
// not the actual default background color.
// FIXME: On Windows 7:
// - Standard row colors are 19px high.
// - Alternate row colors are 17px high. (top and bottom lines ignored?)
plvcd->clrTextBk = colorAltRow;
result = CDRF_NEWFONT;
}
break;
}
default:
break;
}
return result;
}
/**
* WM_NOTIFY handler for the property sheet.
* @param hDlg Dialog window.
* @param pHdr NMHDR
* @return Return value.
*/
INT_PTR RP_ShellPropSheetExt_Private::DlgProc_WM_NOTIFY(HWND hDlg, NMHDR *pHdr)
{
INT_PTR ret = false;
switch (pHdr->code) {
case PSN_SETACTIVE:
if (lblIcon) {
lblIcon->startAnimTimer();
}
if (hBtnOptions) {
ShowWindow(hBtnOptions, SW_SHOW);
}
break;
case PSN_KILLACTIVE:
if (lblIcon) {
lblIcon->stopAnimTimer();
}
if (hBtnOptions) {
ShowWindow(hBtnOptions, SW_HIDE);
}
break;
#ifdef UNICODE
case NM_CLICK:
case NM_RETURN: {
// Check if this is a SysLink control.
// NOTE: SysLink control only supports Unicode.
// NOTE: Linear search...
const HWND hwndFrom = pHdr->hwndFrom;
const bool isSysLink = std::any_of(tabs.cbegin(), tabs.cend(), [hwndFrom](const tab& tab) {
return (tab.lblCredits == hwndFrom);
});
if (isSysLink) {
// It's a SysLink control.
// Open the URL.
PNMLINK pNMLink = reinterpret_cast<PNMLINK>(pHdr);
ShellExecute(nullptr, _T("open"), pNMLink->item.szUrl, nullptr, nullptr, SW_SHOW);
}
break;
}
#endif /* UNICODE */
case TCN_SELCHANGE: {
// Tab change. Make sure this is the correct WC_TABCONTROL.
if (tabWidget != nullptr && tabWidget == pHdr->hwndFrom) {
// Tab widget. Show the selected tab.
int newTabIndex = TabCtrl_GetCurSel(tabWidget);
ShowWindow(tabs[curTabIndex].hDlg, SW_HIDE);
curTabIndex = newTabIndex;
ShowWindow(tabs[newTabIndex].hDlg, SW_SHOW);
}
break;
}
case LVN_GETDISPINFO: {
// Get data for an LVS_OWNERDRAW ListView.
if ((pHdr->idFrom & 0xFC00) != IDC_RFT_LISTDATA(0))
break;
ret = ListView_GetDispInfo(reinterpret_cast<NMLVDISPINFO*>(pHdr));
break;
}
case NM_CUSTOMDRAW: {
// Custom drawing notification.
if ((pHdr->idFrom & 0xFC00) != IDC_RFT_LISTDATA(0))
break;
// NOTE: Since this is a DlgProc, we can't simply return
// the CDRF code. It has to be set as DWLP_MSGRESULT.
// References:
// - https://stackoverflow.com/questions/40549962/c-winapi-listview-nm-customdraw-not-getting-cdds-itemprepaint
// - https://stackoverflow.com/a/40552426
const int result = ListView_CustomDraw(reinterpret_cast<NMLVCUSTOMDRAW*>(pHdr));
SetWindowLongPtr(hDlg, DWLP_MSGRESULT, result);
ret = true;
break;
}
case LVN_ITEMCHANGING: {
// If the window is fully initialized,
// disable modification of checkboxes.
// Reference: https://groups.google.com/forum/embed/#!topic/microsoft.public.vc.mfc/e9cbkSsiImA
if (!isFullyInit)
break;
if ((pHdr->idFrom & 0xFC00) != IDC_RFT_LISTDATA(0))
break;
NMLISTVIEW *const pnmlv = reinterpret_cast<NMLISTVIEW*>(pHdr);
const unsigned int state = (pnmlv->uOldState ^ pnmlv->uNewState) & LVIS_STATEIMAGEMASK;
// Set result to true if the state difference is non-zero (i.e. it's changed).
SetWindowLongPtr(hDlg, DWLP_MSGRESULT, (state != 0));
ret = true;
break;
}
case MSGWN_CLOSED: {
// MessageWidget Close button was clicked.
adjustTabsForMessageWidgetVisibility(false);
ret = true;
break;
}
default:
break;
}
return ret;
}
/**
* WM_COMMAND handler for the property sheet.
* @param hDlg Dialog window.
* @param wParam
* @param lParam
* @return Return value.
*/
INT_PTR RP_ShellPropSheetExt_Private::DlgProc_WM_COMMAND(HWND hDlg, WPARAM wParam, LPARAM lParam)
{
((void)hDlg);
((void)lParam);
INT_PTR ret = false;
switch (HIWORD(wParam)) {
case CBN_SELCHANGE: {
// The user may be changing the selected language
// for RFT_STRING_MULTI.
if (LOWORD(wParam) != IDC_CBO_LANGUAGE)
break;
// NOTE: lParam also has the ComboBox HWND.
const uint32_t lc = sel_lc();
if (lc != 0) {
updateMulti(lc);
}
break;
}
default:
break;
}
return ret;
}
/**
* WM_PAINT handler for the property sheet.
* @param hDlg Dialog window.
* @return Return value.
*/
INT_PTR RP_ShellPropSheetExt_Private::DlgProc_WM_PAINT(HWND hDlg)
{
if (!lblBanner && !lblIcon) {
// Nothing to draw...
return false;
}
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hDlg, &ps);
// TODO: Check paint rectangles?
// TODO: Share the memory DC between lblBanner and lblIcon?
// Draw the banner.
if (lblBanner) {
lblBanner->draw(hdc);
}
// Draw the icon.
if (lblIcon) {
lblIcon->draw(hdc);
}
EndPaint(hDlg, &ps);
return true;
}
//
// FUNCTION: FilePropPageDlgProc
//
// PURPOSE: Processes messages for the property page.
//
INT_PTR CALLBACK RP_ShellPropSheetExt_Private::DlgProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
// Based on CppShellExtPropSheetHandler.
// https://code.msdn.microsoft.com/windowsapps/CppShellExtPropSheetHandler-d93b49b7
switch (uMsg) {
case WM_INITDIALOG: {
// Get the pointer to the property sheet page object. This is
// contained in the LPARAM of the PROPSHEETPAGE structure.
LPPROPSHEETPAGE pPage = reinterpret_cast<LPPROPSHEETPAGE>(lParam);
if (!pPage)
return true;
// Access the property sheet extension from property page.
RP_ShellPropSheetExt *const pExt = reinterpret_cast<RP_ShellPropSheetExt*>(pPage->lParam);
if (!pExt)
return true;
RP_ShellPropSheetExt_Private *const d = pExt->d_ptr;
// Store the D object pointer with this particular page dialog.
SetProp(hDlg, D_PTR_PROP, static_cast<HANDLE>(d));
// Save handles for later.
d->hDlgSheet = hDlg;
// Dialog initialization is postponed to WM_SHOWWINDOW,
// since some other extension (e.g. HashTab) may be
// resizing the dialog.
// NOTE: We're using WM_SHOWWINDOW instead of WM_SIZE
// because WM_SIZE isn't sent for block devices,
// e.g. CD-ROM drives.
return true;
}
// FIXME: FBI's age rating is cut off on Windows
// if we don't adjust for WM_SHOWWINDOW.
case WM_SHOWWINDOW: {
auto *const d = static_cast<RP_ShellPropSheetExt_Private*>(GetProp(hDlg, D_PTR_PROP));
if (!d) {
// No RP_ShellPropSheetExt_Private. Can't do anything...
return false;
}
if (d->isFullyInit) {
// Dialog is already initialized.
break;
}
// Open the RomData object.
RpFile *const file = new RpFile(d->filename, RpFile::FM_OPEN_READ_GZ);
if (!file->isOpen()) {
// Unable to open the file.
file->unref();
break;
}
d->romData = RomDataFactory::create(file);
file->unref();
if (!d->romData) {
// Unable to get a RomData object.
break;
} else if (!d->romData->isOpen()) {
// RomData is not open.
UNREF_AND_NULL_NOCHK(d->romData);
break;
}
// Load the images.
d->loadImages();
// Initialize the dialog.
d->initDialog();
// We can close the RomData's underlying IRpFile now.
d->romData->close();
// Create the "Options" button in the parent window.
d->createOptionsButton();
// Start the icon animation timer.
if (d->lblIcon) {
d->lblIcon->startAnimTimer();
}
// Continue normal processing.
break;
}
case WM_DESTROY: {
auto *const d = static_cast<RP_ShellPropSheetExt_Private*>(GetProp(hDlg, D_PTR_PROP));
if (d && d->lblIcon) {
// Stop the animation timer.
d->lblIcon->stopAnimTimer();
}
// FIXME: Remove D_PTR_PROP from child windows.
// NOTE: WM_DESTROY is sent *before* child windows are destroyed.
// WM_NCDESTROY is sent *after*.
// Remove the D_PTR_PROP property from the page.
// The D_PTR_PROP property stored the pointer to the
// RP_ShellPropSheetExt_Private object.
RemoveProp(hDlg, RP_ShellPropSheetExtPrivate::D_PTR_PROP);
return true;
}
case WM_NOTIFY: {
auto *const d = static_cast<RP_ShellPropSheetExt_Private*>(GetProp(hDlg, D_PTR_PROP));
if (!d) {
// No RP_ShellPropSheetExt_Private. Can't do anything...
return false;
}
return d->DlgProc_WM_NOTIFY(hDlg, reinterpret_cast<NMHDR*>(lParam));
}
case WM_COMMAND: {
auto *const d = static_cast<RP_ShellPropSheetExt_Private*>(GetProp(hDlg, D_PTR_PROP));
if (!d) {
// No RP_ShellPropSheetExt_Private. Can't do anything...
return false;
}
return d->DlgProc_WM_COMMAND(hDlg, wParam, lParam);
}
case WM_PAINT: {
auto *const d = static_cast<RP_ShellPropSheetExt_Private*>(GetProp(hDlg, D_PTR_PROP));
if (!d) {
// No RP_ShellPropSheetExt_Private. Can't do anything...
return false;
}
return d->DlgProc_WM_PAINT(hDlg);
}
case WM_SYSCOLORCHANGE:
case WM_THEMECHANGED: {
// Reload the images.
auto *const d = static_cast<RP_ShellPropSheetExt_Private*>(GetProp(hDlg, D_PTR_PROP));
if (!d) {
// No RP_ShellPropSheetExt_Private. Can't do anything...
return false;
}
// Did the background color change?
// NOTE: Assuming the main background color changed if
// the alternate row color changed.
COLORREF colorAltRow = LibWin32Common::getAltRowColor();
if (colorAltRow != d->colorAltRow) {
// Alternate row color changed.
d->colorAltRow = colorAltRow;
// Reload images with the new row color.
d->loadImages();
// Invalidate the banner and icon rectangles.
if (d->lblBanner) {
d->lblBanner->invalidateRect();
}
if (d->lblIcon) {
d->lblIcon->invalidateRect();
}
// TODO: Check for RFT_LISTDATA with icons and reinitialize
// the icons if the background color changed.
// Alternatively, maybe store them as ARGB32 bitmaps?
// That method works for ComboBoxEx...
}
// Update the fonts.
d->fontHandler.updateFonts(true);
break;
}
case WM_NCPAINT: {
// Update the monospaced font.
// NOTE: This should be WM_SETTINGCHANGE with
// SPI_GETFONTSMOOTHING or SPI_GETFONTSMOOTHINGTYPE,
// but that message isn't sent when previewing changes
// for ClearType. (It's sent when applying the changes.)
auto *const d = static_cast<RP_ShellPropSheetExt_Private*>(GetProp(hDlg, D_PTR_PROP));
if (d) {
// Update the fonts.
d->fontHandler.updateFonts();
}
break;
}
case WM_CTLCOLORSTATIC: {
auto *const d = static_cast<RP_ShellPropSheetExt_Private*>(GetProp(hDlg, D_PTR_PROP));
if (!d) {
// No RP_ShellPropSheetExt_Private. Can't do anything...
return false;
}
auto iter = d->setWarningControls.find(reinterpret_cast<HWND>(lParam));
if (iter != d->setWarningControls.end()) {
// Set the "Warning" color.
HDC hdc = reinterpret_cast<HDC>(wParam);
SetTextColor(hdc, RGB(255, 0, 0));
}
break;
}
case WM_WTSSESSION_CHANGE: {
auto *const d = static_cast<RP_ShellPropSheetExt_Private*>(GetProp(hDlg, D_PTR_PROP));
if (!d) {
// No RP_ShellPropSheetExt_Private. Can't do anything...
return false;
}
// If RDP was connected, disable ListView double-buffering.
// If console (or RemoteFX) was connected, enable ListView double-buffering.
switch (wParam) {
case WTS_CONSOLE_CONNECT:
std::for_each(d->hwndListViewControls.cbegin(), d->hwndListViewControls.cend(),
[](HWND hWnd) {
DWORD dwExStyle = ListView_GetExtendedListViewStyle(hWnd);
dwExStyle |= LVS_EX_DOUBLEBUFFER;
ListView_SetExtendedListViewStyle(hWnd, dwExStyle);
}
);
break;
case WTS_REMOTE_CONNECT:
std::for_each(d->hwndListViewControls.cbegin(), d->hwndListViewControls.cend(),
[](HWND hWnd) {
DWORD dwExStyle = ListView_GetExtendedListViewStyle(hWnd);
dwExStyle &= ~LVS_EX_DOUBLEBUFFER;
ListView_SetExtendedListViewStyle(hWnd, dwExStyle);
}
);
break;
default:
break;
}
break;
}
default:
break;
}
return false; // Let system deal with other messages
}
//
// FUNCTION: FilePropPageCallbackProc
//
// PURPOSE: Specifies an application-defined callback function that a property
// sheet calls when a page is created and when it is about to be
// destroyed. An application can use this function to perform
// initialization and cleanup operations for the page.
//
UINT CALLBACK RP_ShellPropSheetExt_Private::CallbackProc(HWND hWnd, UINT uMsg, LPPROPSHEETPAGE ppsp)
{
((void)hWnd); // TODO: Validate this?
switch (uMsg) {
case PSPCB_CREATE: {
// Must return true to enable the page to be created.
return true;
}
case PSPCB_RELEASE: {
// When the callback function receives the PSPCB_RELEASE notification,
// the ppsp parameter of the PropSheetPageProc contains a pointer to
// the PROPSHEETPAGE structure. The lParam member of the PROPSHEETPAGE
// structure contains the extension pointer which can be used to
// release the object.
// Release the property sheet extension object. This is called even
// if the property page was never actually displayed.
RP_ShellPropSheetExt *const pExt = reinterpret_cast<RP_ShellPropSheetExt*>(ppsp->lParam);
if (pExt) {
pExt->Release();
}
break;
}
default:
break;
}
return false;
}
/**
* Dialog procedure for subtabs.
* @param hDlg
* @param uMsg
* @param wParam
* @param lParam
*/
INT_PTR CALLBACK RP_ShellPropSheetExt_Private::SubtabDlgProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg) {
case WM_DESTROY: {
// Remove the D_PTR_PROP and TAB_PTR_PROP properties from the page.
// The D_PTR_PROP property stored the pointer to the
// RP_ShellPropSheetExt_Private object.
// The TAB_PTR_PROP property stored the pointer to the
// RP_ShellPropSheetExt_Private::tab object.
RemoveProp(hDlg, RP_ShellPropSheetExtPrivate::D_PTR_PROP);
RemoveProp(hDlg, RP_ShellPropSheetExtPrivate::TAB_PTR_PROP);
return TRUE;
}
case WM_NOTIFY: {
// Propagate NM_CUSTOMDRAW to the parent dialog.
const NMHDR *const pHdr = reinterpret_cast<const NMHDR*>(lParam);
switch (pHdr->code) {
case LVN_GETDISPINFO:
case NM_CUSTOMDRAW:
case LVN_ITEMCHANGING: {
// NOTE: Since this is a DlgProc, we can't simply return
// the CDRF code. It has to be set as DWLP_MSGRESULT.
// References:
// - https://stackoverflow.com/questions/40549962/c-winapi-listview-nm-customdraw-not-getting-cdds-itemprepaint
// - https://stackoverflow.com/a/40552426
INT_PTR result = SendMessage(GetParent(hDlg), uMsg, wParam, lParam);
SetWindowLongPtr(hDlg, DWLP_MSGRESULT, result);
return TRUE;
}
default:
break;
}
break;
}
case WM_VSCROLL: {
auto *const d = static_cast<RP_ShellPropSheetExt_Private*>(GetProp(hDlg, D_PTR_PROP));
auto *const tab = static_cast<RP_ShellPropSheetExt_Private::tab*>(GetProp(hDlg, TAB_PTR_PROP));
if (!d || !tab) {
// No RP_ShellPropSheetExt_Private or tab. Can't do anything...
break;
}
// Check the operation and scroll there.
int deltaY = 0;
switch (LOWORD(wParam)) {
case SB_TOP:
deltaY = -tab->scrollPos;
break;
case SB_BOTTOM:
deltaY = tab->curPt.y - tab->scrollPos;
break;
case SB_LINEUP:
deltaY = -d->lblDescHeight;
break;
case SB_LINEDOWN:
deltaY = d->lblDescHeight;
break;
case SB_PAGEUP:
deltaY = -d->dlgSize.cy;
break;
case SB_PAGEDOWN:
deltaY = d->dlgSize.cy;
break;
case SB_THUMBTRACK:
case SB_THUMBPOSITION:
deltaY = HIWORD(wParam) - tab->scrollPos;
break;
}
// Make sure this doesn't go out of range.
int scrollPos = tab->scrollPos + deltaY;
if (scrollPos < 0) {
deltaY -= scrollPos;
} else if (scrollPos + d->dlgSize.cy > tab->curPt.y) {
deltaY -= (scrollPos + 1) - (tab->curPt.y - d->dlgSize.cy);
}
tab->scrollPos += deltaY;
SetScrollPos(hDlg, SB_VERT, tab->scrollPos, TRUE);
ScrollWindow(hDlg, 0, -deltaY, nullptr, nullptr);
return TRUE;
}
}
// Dummy callback procedure that does nothing.
return DefSubclassProc(hDlg, uMsg, wParam, lParam);
}