Editor theme and bug fixes.

This commit is contained in:
Justin Marshall
2026-05-15 10:32:24 -07:00
parent ea14ecb595
commit f6b489b2ff
18 changed files with 5972 additions and 455 deletions
+213 -6
View File
@@ -29,13 +29,212 @@ along with IceTech Source Code. If not, see <http://www.gnu.org/licenses/>.
#include "Radiant.h"
#include "OutlinerDlg.h"
#include "InspectorDialog.h"
#include <commctrl.h>
#pragma comment(lib, "comctl32.lib")
IMPLEMENT_DYNAMIC(COutlinerDlg, CWnd)
static const COLORREF OUTLINER_BG = RGB(245, 247, 250);
static const COLORREF OUTLINER_PANEL_BG = RGB(255, 255, 255);
static const COLORREF OUTLINER_TEXT = RGB(32, 36, 42);
static const COLORREF OUTLINER_MUTED_TEXT = RGB(88, 96, 105);
static const COLORREF OUTLINER_BG = RGB(17, 19, 23);
static const COLORREF OUTLINER_PANEL_BG = RGB(14, 16, 20);
static const COLORREF OUTLINER_TEXT = RGB(226, 232, 240);
static const COLORREF OUTLINER_MUTED_TEXT = RGB(151, 163, 184);
static const COLORREF OUTLINER_FIELD_BG = RGB(14, 16, 20);
static const COLORREF OUTLINER_BORDER = RGB(55, 62, 72);
static const COLORREF OUTLINER_ACCENT = RGB(87, 166, 255);
static const char* OUTLINER_TREE_OLDPROC = "IceTech.Outliner.TreeOldProc";
static void Outliner_ApplyNativeDarkTheme(HWND hWnd) {
if (!hWnd) {
return;
}
typedef HRESULT (WINAPI *SetWindowThemeProc)(HWND, LPCWSTR, LPCWSTR);
typedef BOOL (WINAPI *AllowDarkModeForWindowProc)(HWND, BOOL);
static HMODULE hUxTheme = ::LoadLibraryA("uxtheme.dll");
static SetWindowThemeProc pSetWindowTheme = hUxTheme ? (SetWindowThemeProc)::GetProcAddress(hUxTheme, "SetWindowTheme") : NULL;
static AllowDarkModeForWindowProc pAllowDarkModeForWindow = hUxTheme ? (AllowDarkModeForWindowProc)::GetProcAddress(hUxTheme, MAKEINTRESOURCEA(133)) : NULL;
if (pAllowDarkModeForWindow) {
pAllowDarkModeForWindow(hWnd, TRUE);
}
if (pSetWindowTheme) {
pSetWindowTheme(hWnd, L"DarkMode_Explorer", NULL);
}
::SendMessage(hWnd, WM_THEMECHANGED, 0, 0);
}
static void Outliner_FillRect(HDC hDC, const RECT& rc, COLORREF color) {
HBRUSH brush = ::CreateSolidBrush(color);
::FillRect(hDC, &rc, brush);
::DeleteObject(brush);
}
static void Outliner_FrameRect(HDC hDC, const RECT& rc, COLORREF color) {
HBRUSH brush = ::CreateSolidBrush(color);
::FrameRect(hDC, &rc, brush);
::DeleteObject(brush);
}
static HBITMAP Outliner_CreateDarkCheckBitmap(bool checked) {
HDC screenDC = ::GetDC(NULL);
HDC memDC = ::CreateCompatibleDC(screenDC);
HBITMAP bitmap = ::CreateCompatibleBitmap(screenDC, 16, 16);
HBITMAP oldBitmap = (HBITMAP)::SelectObject(memDC, bitmap);
RECT rc = { 0, 0, 16, 16 };
Outliner_FillRect(memDC, rc, RGB(255, 0, 255));
RECT box = { 2, 2, 14, 14 };
Outliner_FillRect(memDC, box, OUTLINER_FIELD_BG);
Outliner_FrameRect(memDC, box, checked ? OUTLINER_ACCENT : OUTLINER_BORDER);
if (checked) {
HPEN pen = ::CreatePen(PS_SOLID, 2, OUTLINER_ACCENT);
HPEN oldPen = (HPEN)::SelectObject(memDC, pen);
::MoveToEx(memDC, 5, 8, NULL);
::LineTo(memDC, 7, 11);
::LineTo(memDC, 12, 4);
::SelectObject(memDC, oldPen);
::DeleteObject(pen);
}
::SelectObject(memDC, oldBitmap);
::DeleteDC(memDC);
::ReleaseDC(NULL, screenDC);
return bitmap;
}
static HIMAGELIST Outliner_GetDarkCheckImageList() {
static HIMAGELIST imageList = NULL;
if (imageList) {
return imageList;
}
imageList = ::ImageList_Create(16, 16, ILC_COLOR24 | ILC_MASK, 2, 0);
if (!imageList) {
return NULL;
}
HBITMAP uncheckedBitmap = Outliner_CreateDarkCheckBitmap(false);
HBITMAP checkedBitmap = Outliner_CreateDarkCheckBitmap(true);
::ImageList_AddMasked(imageList, uncheckedBitmap, RGB(255, 0, 255));
::ImageList_AddMasked(imageList, checkedBitmap, RGB(255, 0, 255));
::DeleteObject(uncheckedBitmap);
::DeleteObject(checkedBitmap);
return imageList;
}
static void Outliner_ApplyDarkTreeImages(HWND hTree) {
HIMAGELIST imageList = Outliner_GetDarkCheckImageList();
if (hTree && imageList) {
::SendMessage(hTree, TVM_SETIMAGELIST, TVSIL_STATE, (LPARAM)imageList);
}
}
static bool Outliner_TreeItemHasStateImage(HWND hTree, HTREEITEM item) {
if (!hTree || !item) {
return false;
}
TVITEM tvi;
memset(&tvi, 0, sizeof(tvi));
tvi.mask = TVIF_STATE;
tvi.hItem = item;
tvi.stateMask = TVIS_STATEIMAGEMASK;
if (!TreeView_GetItem(hTree, &tvi)) {
return false;
}
return (tvi.state & TVIS_STATEIMAGEMASK) != 0;
}
static bool Outliner_TreePointInCheckZone(HWND hTree, HTREEITEM item, const POINT& point, UINT flags) {
if (!item || !Outliner_TreeItemHasStateImage(hTree, item)) {
return false;
}
if (flags & TVHT_ONITEMSTATEICON) {
return true;
}
RECT label;
if (!TreeView_GetItemRect(hTree, item, &label, TRUE)) {
return false;
}
RECT row;
if (!TreeView_GetItemRect(hTree, item, &row, FALSE)) {
row = label;
}
// Make the checkbox easier to hit than the stock 16px state image. The
// actual checkbox remains on the left, but clicks in a wider leading gutter
// are treated as checkbox clicks instead of requiring pixel-perfect aim.
RECT checkZone;
checkZone.left = max(0, label.left - 30);
checkZone.right = label.left + 10;
checkZone.top = row.top;
checkZone.bottom = row.bottom;
return ::PtInRect(&checkZone, point) ? true : false;
}
static LRESULT CALLBACK Outliner_TreeProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
WNDPROC oldProc = (WNDPROC)::GetPropA(hWnd, OUTLINER_TREE_OLDPROC);
if (!oldProc) {
return ::DefWindowProc(hWnd, uMsg, wParam, lParam);
}
switch (uMsg) {
case WM_LBUTTONDOWN:
{
POINT point;
point.x = (short)LOWORD(lParam);
point.y = (short)HIWORD(lParam);
TVHITTESTINFO hit;
memset(&hit, 0, sizeof(hit));
hit.pt = point;
HTREEITEM item = TreeView_HitTest(hWnd, &hit);
if (item && Outliner_TreePointInCheckZone(hWnd, item, point, hit.flags)) {
BOOL checked = TreeView_GetCheckState(hWnd, item) ? TRUE : FALSE;
TreeView_SelectItem(hWnd, item);
TreeView_SetCheckState(hWnd, item, checked ? FALSE : TRUE);
HWND parent = ::GetParent(hWnd);
if (parent) {
::SendMessage(parent, COutlinerDlg::WM_OUTLINER_APPLY_CHECK, (WPARAM)item, 0);
}
::InvalidateRect(hWnd, NULL, FALSE);
return 0;
}
break;
}
case WM_NCDESTROY:
{
::SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)oldProc);
::RemovePropA(hWnd, OUTLINER_TREE_OLDPROC);
return ::CallWindowProc(oldProc, hWnd, uMsg, wParam, lParam);
}
}
return ::CallWindowProc(oldProc, hWnd, uMsg, wParam, lParam);
}
static void Outliner_SubclassTreeForCheckClicks(HWND hTree) {
if (!hTree || ::GetPropA(hTree, OUTLINER_TREE_OLDPROC)) {
return;
}
WNDPROC oldProc = (WNDPROC)::GetWindowLongPtr(hTree, GWLP_WNDPROC);
::SetPropA(hTree, OUTLINER_TREE_OLDPROC, (HANDLE)oldProc);
::SetWindowLongPtr(hTree, GWLP_WNDPROC, (LONG_PTR)Outliner_TreeProc);
}
static HBRUSH Outliner_BackgroundBrush() {
static HBRUSH hBrush = ::CreateSolidBrush(OUTLINER_BG);
return hBrush;
}
static const int OUTLINER_MARGIN = 6;
static const int OUTLINER_FILTER_LABEL_W = 42;
@@ -222,7 +421,7 @@ BOOL COutlinerDlg::Create(CWnd* pParentWnd) {
CString className = AfxRegisterWndClass(
CS_DBLCLKS,
::LoadCursor(NULL, IDC_ARROW),
(HBRUSH)(COLOR_BTNFACE + 1),
Outliner_BackgroundBrush(),
NULL
);
@@ -278,8 +477,16 @@ int COutlinerDlg::OnCreate(LPCREATESTRUCT lpCreateStruct) {
this,
IDC_OUTLINER_TREE
);
m_tree.ModifyStyleEx(0, WS_EX_CLIENTEDGE, SWP_FRAMECHANGED);
m_filter.ModifyStyleEx(WS_EX_CLIENTEDGE, 0, SWP_FRAMECHANGED);
m_tree.ModifyStyleEx(WS_EX_CLIENTEDGE, 0, SWP_FRAMECHANGED);
m_tree.SetItemHeight(20);
m_tree.SetBkColor(OUTLINER_PANEL_BG);
m_tree.SetTextColor(OUTLINER_TEXT);
Outliner_ApplyNativeDarkTheme(GetSafeHwnd());
Outliner_ApplyNativeDarkTheme(m_filter.GetSafeHwnd());
Outliner_ApplyNativeDarkTheme(m_tree.GetSafeHwnd());
Outliner_ApplyDarkTreeImages(m_tree.GetSafeHwnd());
Outliner_SubclassTreeForCheckClicks(m_tree.GetSafeHwnd());
ApplyFont();