Files
DoomRTX/neo/engine/tools/radiant/OutlinerDlg.cpp
T
2026-05-15 10:32:24 -07:00

1501 lines
36 KiB
C++

/*
===========================================================================
IceTech GPL Source Code
Copyright (C) 2026 Justin Marshall
This file is part of the IceTech GPL Source Code (?IceTech Source Code?).
IceTech Source Code is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
IceTech Source Code is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with IceTech Source Code. If not, see <http://www.gnu.org/licenses/>.
===========================================================================
*/
#include "precompiled.h"
#pragma hdrstop
#include "qe3.h"
#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(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;
static const int OUTLINER_FILTER_H = 22;
static const int OUTLINER_GAP = 6;
static CPtrArray s_outlinerHiddenEntities;
static CString s_outlinerVisibilityMapName;
static entity_t* s_outlinerVisibilityWorldEntity = NULL;
static entity_t* s_outlinerVisibilityFirstEntity = NULL;
static int s_outlinerVisibilityEntityCount = -1;
static int s_outlinerVisibilityMapModified = -999999;
static bool s_outlinerVisibilityInitialized = false;
static const char* Outliner_CurrentMapName() {
return currentmap ? currentmap : "";
}
static entity_t* Outliner_FirstMapEntity() {
if (entities.next != NULL && entities.next != &entities) {
return entities.next;
}
return NULL;
}
static bool Outliner_ArrayContainsPtr(const CPtrArray& array, entity_t* ent) {
for (int i = 0; i < array.GetSize(); i++) {
if ((entity_t*)array.GetAt(i) == ent) {
return true;
}
}
return false;
}
static void Outliner_ClearHiddenEntities() {
s_outlinerHiddenEntities.RemoveAll();
}
static void Outliner_SyncVisibilityMapIdentity() {
s_outlinerVisibilityInitialized = true;
s_outlinerVisibilityWorldEntity = world_entity;
s_outlinerVisibilityFirstEntity = Outliner_FirstMapEntity();
s_outlinerVisibilityEntityCount = g_qeglobals.d_num_entities;
s_outlinerVisibilityMapModified = mapModified;
s_outlinerVisibilityMapName = Outliner_CurrentMapName();
}
void Outliner_ResetEntityVisibility() {
Outliner_ClearHiddenEntities();
Outliner_SyncVisibilityMapIdentity();
}
static void Outliner_EnsureVisibilityMap() {
const char* mapName = Outliner_CurrentMapName();
const entity_t* firstEntity = Outliner_FirstMapEntity();
bool resetVisibility = false;
if (!s_outlinerVisibilityInitialized) {
resetVisibility = true;
}
else if (s_outlinerVisibilityWorldEntity != world_entity) {
resetVisibility = true;
}
else if (s_outlinerVisibilityMapName.CompareNoCase(mapName) != 0) {
resetVisibility = true;
}
else if (mapModified < s_outlinerVisibilityMapModified) {
// Loading or creating a map normally resets mapModified. Treat that as
// a new editor visibility session so every entity starts checked.
resetVisibility = true;
}
else if (mapModified <= 0 && s_outlinerVisibilityFirstEntity != firstEntity) {
// Some map/new-map paths reuse the same worldspawn pointer and leave
// currentmap empty. If the list head changed while the map is clean,
// this is almost certainly a freshly loaded/new map, not an edit.
resetVisibility = true;
}
if (resetVisibility) {
Outliner_ClearHiddenEntities();
}
Outliner_SyncVisibilityMapIdentity();
}
static int Outliner_FindHiddenEntity(entity_t* ent) {
for (int i = 0; i < s_outlinerHiddenEntities.GetSize(); i++) {
if ((entity_t*)s_outlinerHiddenEntities.GetAt(i) == ent) {
return i;
}
}
return -1;
}
static void Outliner_PruneHiddenEntities(const CPtrArray& currentEntities) {
Outliner_EnsureVisibilityMap();
for (int i = s_outlinerHiddenEntities.GetSize() - 1; i >= 0; i--) {
entity_t* ent = (entity_t*)s_outlinerHiddenEntities.GetAt(i);
if (!Outliner_ArrayContainsPtr(currentEntities, ent)) {
s_outlinerHiddenEntities.RemoveAt(i);
}
}
}
bool Outliner_IsEntityVisible(entity_t* ent) {
if (ent == NULL) {
return true;
}
Outliner_EnsureVisibilityMap();
return Outliner_FindHiddenEntity(ent) < 0;
}
bool Outliner_HasHiddenEntities() {
Outliner_EnsureVisibilityMap();
return s_outlinerHiddenEntities.GetSize() > 0;
}
bool Outliner_IsEntityLinkVisible(entity_t* source, entity_t* target) {
return Outliner_IsEntityVisible(source) && Outliner_IsEntityVisible(target);
}
void Outliner_SetEntityVisible(entity_t* ent, bool visible) {
if (ent == NULL) {
return;
}
Outliner_EnsureVisibilityMap();
const int index = Outliner_FindHiddenEntity(ent);
if (visible) {
if (index >= 0) {
s_outlinerHiddenEntities.RemoveAt(index);
}
}
else {
if (index < 0) {
s_outlinerHiddenEntities.Add(ent);
}
}
}
BEGIN_MESSAGE_MAP(COutlinerDlg, CWnd)
ON_WM_CREATE()
ON_WM_SIZE()
ON_WM_DESTROY()
ON_WM_TIMER()
ON_WM_PAINT()
ON_WM_ERASEBKGND()
ON_WM_CTLCOLOR()
ON_EN_CHANGE(COutlinerDlg::IDC_OUTLINER_FILTER, OnFilterChanged)
ON_NOTIFY(NM_DBLCLK, COutlinerDlg::IDC_OUTLINER_TREE, OnTreeDblClick)
ON_NOTIFY(NM_RCLICK, COutlinerDlg::IDC_OUTLINER_TREE, OnTreeRightClick)
ON_NOTIFY(NM_CLICK, COutlinerDlg::IDC_OUTLINER_TREE, OnTreeClick)
ON_MESSAGE(COutlinerDlg::WM_OUTLINER_APPLY_CHECK, OnApplyTreeCheck)
END_MESSAGE_MAP()
COutlinerDlg::COutlinerDlg() {
m_lastMapModified = -999999;
m_lastEntityCount = -1;
m_lastWorldEntity = NULL;
}
COutlinerDlg::~COutlinerDlg() {
ClearItemData();
if (m_font.GetSafeHandle()) {
m_font.DeleteObject();
}
if (m_bgBrush.GetSafeHandle()) {
m_bgBrush.DeleteObject();
}
if (m_editBrush.GetSafeHandle()) {
m_editBrush.DeleteObject();
}
if (m_staticBrush.GetSafeHandle()) {
m_staticBrush.DeleteObject();
}
}
BOOL COutlinerDlg::Create(CWnd* pParentWnd) {
CString className = AfxRegisterWndClass(
CS_DBLCLKS,
::LoadCursor(NULL, IDC_ARROW),
Outliner_BackgroundBrush(),
NULL
);
return CWnd::CreateEx(
WS_EX_CONTROLPARENT,
className,
"Outliner",
WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN | WS_CLIPSIBLINGS,
CRect(5, 5, 10, 10),
pParentWnd,
IDC_OUTLINER_ROOT
);
}
int COutlinerDlg::OnCreate(LPCREATESTRUCT lpCreateStruct) {
if (CWnd::OnCreate(lpCreateStruct) == -1) {
return -1;
}
if (!m_bgBrush.GetSafeHandle()) {
m_bgBrush.CreateSolidBrush(OUTLINER_BG);
}
if (!m_editBrush.GetSafeHandle()) {
m_editBrush.CreateSolidBrush(OUTLINER_PANEL_BG);
}
if (!m_staticBrush.GetSafeHandle()) {
m_staticBrush.CreateSolidBrush(OUTLINER_BG);
}
m_filterLabel.Create(
"Filter:",
WS_CHILD | WS_VISIBLE | SS_LEFT | SS_CENTERIMAGE,
CRect(0, 0, 10, 10),
this,
IDC_OUTLINER_FILTER_LABEL
);
m_filter.CreateEx(
WS_EX_CLIENTEDGE,
"EDIT",
"",
WS_CHILD | WS_VISIBLE | WS_TABSTOP | ES_AUTOHSCROLL,
CRect(0, 0, 10, 10),
this,
IDC_OUTLINER_FILTER
);
m_tree.Create(
WS_CHILD | WS_VISIBLE | WS_TABSTOP |
TVS_HASBUTTONS | TVS_HASLINES | TVS_LINESATROOT |
TVS_SHOWSELALWAYS | TVS_DISABLEDRAGDROP | TVS_CHECKBOXES,
CRect(0, 0, 10, 10),
this,
IDC_OUTLINER_TREE
);
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();
SetTimer(OUTLINER_REFRESH_TIMER, 750, NULL);
Refresh(true);
return 0;
}
void COutlinerDlg::ApplyFont() {
if (!m_font.GetSafeHandle()) {
LOGFONT lf;
memset(&lf, 0, sizeof(lf));
HDC hDC = ::GetDC(GetSafeHwnd());
const int dpiY = hDC ? GetDeviceCaps(hDC, LOGPIXELSY) : 96;
if (hDC) {
::ReleaseDC(GetSafeHwnd(), hDC);
}
lf.lfHeight = -MulDiv(9, dpiY, 72);
lf.lfWeight = FW_NORMAL;
lf.lfQuality = CLEARTYPE_QUALITY;
strncpy(lf.lfFaceName, "Segoe UI", sizeof(lf.lfFaceName) - 1);
lf.lfFaceName[sizeof(lf.lfFaceName) - 1] = '\0';
m_font.CreateFontIndirect(&lf);
}
if (m_font.GetSafeHandle()) {
SetFont(&m_font, FALSE);
m_filterLabel.SetFont(&m_font, FALSE);
m_filter.SetFont(&m_font, FALSE);
m_tree.SetFont(&m_font, FALSE);
}
}
void COutlinerDlg::OnSize(UINT nType, int cx, int cy) {
CWnd::OnSize(nType, cx, cy);
if (!m_filter.GetSafeHwnd() || !m_tree.GetSafeHwnd()) {
return;
}
const int left = OUTLINER_MARGIN;
const int top = OUTLINER_MARGIN;
const int right = (cx - OUTLINER_MARGIN > left) ? (cx - OUTLINER_MARGIN) : left;
const int filterTop = top;
const int filterBottom = filterTop + OUTLINER_FILTER_H;
m_filterLabel.SetWindowPos(
NULL,
left,
filterTop,
OUTLINER_FILTER_LABEL_W,
OUTLINER_FILTER_H,
SWP_NOZORDER | SWP_NOACTIVATE
);
m_filter.SetWindowPos(
NULL,
left + OUTLINER_FILTER_LABEL_W + OUTLINER_GAP,
filterTop,
(right > (left + OUTLINER_FILTER_LABEL_W + OUTLINER_GAP)) ? (right - (left + OUTLINER_FILTER_LABEL_W + OUTLINER_GAP)) : 0,
OUTLINER_FILTER_H,
SWP_NOZORDER | SWP_NOACTIVATE
);
const int treeTop = filterBottom + OUTLINER_GAP;
m_tree.SetWindowPos(
NULL,
left,
treeTop,
(right > left) ? (right - left) : 0,
(cy > treeTop + OUTLINER_MARGIN) ? (cy - treeTop - OUTLINER_MARGIN) : 0,
SWP_NOZORDER | SWP_NOACTIVATE
);
}
void COutlinerDlg::OnDestroy() {
KillTimer(OUTLINER_REFRESH_TIMER);
if (m_tree.GetSafeHwnd()) {
m_tree.DeleteAllItems();
}
ClearItemData();
CWnd::OnDestroy();
}
void COutlinerDlg::OnTimer(UINT_PTR nIDEvent) {
if (nIDEvent == OUTLINER_REFRESH_TIMER) {
RefreshIfNeeded();
return;
}
CWnd::OnTimer(nIDEvent);
}
void COutlinerDlg::OnPaint() {
CPaintDC dc(this);
CRect rc;
GetClientRect(&rc);
dc.FillSolidRect(&rc, OUTLINER_BG);
}
BOOL COutlinerDlg::OnEraseBkgnd(CDC* pDC) {
CRect rc;
GetClientRect(&rc);
pDC->FillSolidRect(&rc, OUTLINER_BG);
return TRUE;
}
HBRUSH COutlinerDlg::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor) {
HBRUSH hbr = CWnd::OnCtlColor(pDC, pWnd, nCtlColor);
if (!pDC) {
return hbr;
}
pDC->SetTextColor(OUTLINER_TEXT);
switch (nCtlColor) {
case CTLCOLOR_STATIC:
pDC->SetBkColor(OUTLINER_BG);
pDC->SetTextColor(OUTLINER_MUTED_TEXT);
return (HBRUSH)m_staticBrush.GetSafeHandle();
case CTLCOLOR_EDIT:
pDC->SetBkColor(OUTLINER_PANEL_BG);
pDC->SetTextColor(OUTLINER_TEXT);
return (HBRUSH)m_editBrush.GetSafeHandle();
default:
break;
}
return hbr;
}
void COutlinerDlg::OnFilterChanged() {
if (!m_filter.GetSafeHwnd()) {
return;
}
m_filter.GetWindowText(m_filterText);
m_filterText.TrimLeft();
m_filterText.TrimRight();
Refresh(true);
}
BOOL COutlinerDlg::PreTranslateMessage(MSG* pMsg) {
if (pMsg->message == WM_KEYDOWN) {
if (pMsg->wParam == VK_RETURN && pMsg->hwnd == m_tree.GetSafeHwnd()) {
OpenEntityProperties(SelectedEntity());
return TRUE;
}
if (pMsg->wParam == VK_SPACE && pMsg->hwnd == m_tree.GetSafeHwnd()) {
entity_t* ent = SelectedEntity();
if (ent != NULL) {
ToggleEntityVisibility(ent, false);
return TRUE;
}
}
if (pMsg->wParam == VK_F5) {
Refresh(true);
return TRUE;
}
}
return CWnd::PreTranslateMessage(pMsg);
}
void COutlinerDlg::RefreshIfNeeded() {
if (NeedsRefresh()) {
Refresh(true);
}
}
bool COutlinerDlg::NeedsRefresh() const {
if (!m_tree.GetSafeHwnd()) {
return false;
}
if (m_lastWorldEntity != world_entity) {
return true;
}
if (m_lastEntityCount != g_qeglobals.d_num_entities) {
return true;
}
if (m_lastMapModified != mapModified) {
return true;
}
if (m_lastMapName.CompareNoCase(currentmap) != 0) {
return true;
}
return false;
}
void COutlinerDlg::Refresh(bool force) {
if (!m_tree.GetSafeHwnd()) {
return;
}
if (!force && !NeedsRefresh()) {
return;
}
entity_t* oldSelection = SelectedEntity();
const char* mapName = Outliner_CurrentMapName();
const bool hadPreviousRefresh = (m_lastMapModified != -999999);
const bool mapIdentityChanged =
(!s_outlinerVisibilityInitialized) ||
(m_lastWorldEntity != world_entity) ||
(m_lastMapName.CompareNoCase(mapName) != 0) ||
(hadPreviousRefresh && mapModified < m_lastMapModified) ||
(hadPreviousRefresh && mapModified <= 0 && s_outlinerVisibilityFirstEntity != Outliner_FirstMapEntity());
// File->New in this editor can leave the same empty map name around.
// When that happens, make the new map start from a clean all-visible state
// instead of inheriting hidden pointers from the previous contents.
const bool likelyNewBlankMap =
(s_outlinerHiddenEntities.GetSize() > 0) &&
(m_lastEntityCount > 1) &&
(g_qeglobals.d_num_entities <= 1) &&
(mapName[0] == '\0');
if (mapIdentityChanged || likelyNewBlankMap) {
Outliner_ResetEntityVisibility();
}
BuildGraph();
Outliner_PruneHiddenEntities(m_entities);
m_tree.SetRedraw(FALSE);
m_tree.DeleteAllItems();
ClearItemData();
bool insertedAny = false;
for (int i = 0; i < m_entities.GetSize(); i++) {
if (m_incoming.GetAt(i) == 0) {
CPtrArray stack;
HTREEITEM item = InsertEntityRecursive((entity_t*)m_entities.GetAt(i), TVI_ROOT, stack);
if (item != NULL) {
insertedAny = true;
}
}
}
HTREEITEM loopGroup = NULL;
for (int i = 0; i < m_entities.GetSize(); i++) {
if (m_covered.GetAt(i)) {
continue;
}
entity_t* ent = (entity_t*)m_entities.GetAt(i);
if (!m_filterText.IsEmpty()) {
CPtrArray filterStack;
if (!EntityOrDescendantMatches(ent, filterStack)) {
continue;
}
}
if (loopGroup == NULL) {
loopGroup = m_tree.InsertItem("Target loops / no root", TVI_ROOT, TVI_LAST);
m_tree.SetItemState(loopGroup, 0, TVIS_STATEIMAGEMASK);
insertedAny = true;
}
CPtrArray stack;
InsertEntityRecursive(ent, loopGroup, stack);
}
if (loopGroup != NULL) {
m_tree.Expand(loopGroup, TVE_EXPAND);
}
if (!insertedAny) {
HTREEITEM emptyItem = NULL;
if (m_filterText.IsEmpty()) {
emptyItem = m_tree.InsertItem("<no entities>", TVI_ROOT, TVI_LAST);
} else {
emptyItem = m_tree.InsertItem("<no matching entities>", TVI_ROOT, TVI_LAST);
}
if (emptyItem != NULL) {
m_tree.SetItemState(emptyItem, 0, TVIS_STATEIMAGEMASK);
}
}
if (!m_filterText.IsEmpty()) {
ExpandAll(m_tree.GetRootItem());
} else {
HTREEITEM root = m_tree.GetRootItem();
while (root != NULL) {
m_tree.Expand(root, TVE_EXPAND);
root = m_tree.GetNextSiblingItem(root);
}
}
if (oldSelection != NULL) {
HTREEITEM selectedItem = FindTreeItemForEntity(oldSelection, m_tree.GetRootItem());
if (selectedItem != NULL) {
m_tree.SelectItem(selectedItem);
}
}
m_tree.SetRedraw(TRUE);
m_tree.Invalidate();
m_lastWorldEntity = world_entity;
m_lastEntityCount = g_qeglobals.d_num_entities;
m_lastMapModified = mapModified;
m_lastMapName = Outliner_CurrentMapName();
}
void COutlinerDlg::BuildGraph() {
m_entities.RemoveAll();
m_incoming.RemoveAll();
m_covered.RemoveAll();
m_edges.RemoveAll();
if (world_entity != NULL) {
AddEntityNode(world_entity);
}
if (entities.next != NULL) {
for (entity_t* ent = entities.next; ent != &entities; ent = ent->next) {
AddEntityNode(ent);
}
}
for (int i = 0; i < m_entities.GetSize(); i++) {
entity_t* source = (entity_t*)m_entities.GetAt(i);
if (source == NULL) {
continue;
}
const int numKeys = source->epairs.GetNumKeyVals();
for (int keyIndex = 0; keyIndex < numKeys; keyIndex++) {
const idKeyValue* kv = source->epairs.GetKeyVal(keyIndex);
if (kv == NULL) {
continue;
}
if (!IsTargetKey(kv->GetKey().c_str())) {
continue;
}
const char* targetName = kv->GetValue().c_str();
if (targetName == NULL || targetName[0] == '\0') {
continue;
}
const int targetIndex = FindNodeByName(targetName);
if (targetIndex < 0) {
continue;
}
entity_t* target = (entity_t*)m_entities.GetAt(targetIndex);
AddEdge(source, target);
}
}
}
void COutlinerDlg::AddEntityNode(entity_t* ent) {
if (ent == NULL) {
return;
}
if (FindNode(ent) >= 0) {
return;
}
m_entities.Add(ent);
m_incoming.Add(0);
m_covered.Add(FALSE);
}
void COutlinerDlg::AddEdge(entity_t* source, entity_t* target) {
if (source == NULL || target == NULL) {
return;
}
if (HasEdge(source, target)) {
return;
}
outlinerEdge_t edge;
edge.source = source;
edge.target = target;
m_edges.Add(edge);
const int targetIndex = FindNode(target);
if (targetIndex >= 0) {
m_incoming.SetAt(targetIndex, m_incoming.GetAt(targetIndex) + 1);
}
}
bool COutlinerDlg::HasEdge(entity_t* source, entity_t* target) const {
for (int i = 0; i < m_edges.GetSize(); i++) {
const outlinerEdge_t& edge = m_edges.GetAt(i);
if (edge.source == source && edge.target == target) {
return true;
}
}
return false;
}
int COutlinerDlg::FindNode(entity_t* ent) const {
for (int i = 0; i < m_entities.GetSize(); i++) {
if ((entity_t*)m_entities.GetAt(i) == ent) {
return i;
}
}
return -1;
}
int COutlinerDlg::FindNodeByName(const char* name) const {
if (name == NULL || name[0] == '\0') {
return -1;
}
for (int i = 0; i < m_entities.GetSize(); i++) {
entity_t* ent = (entity_t*)m_entities.GetAt(i);
if (ent == NULL) {
continue;
}
const char* entName = ValueForKey(ent, "name");
if (entName != NULL && entName[0] != '\0' && idStr::Icmp(entName, name) == 0) {
return i;
}
// Support old/ported maps that still use targetname, but do not require it.
const char* targetName = ValueForKey(ent, "targetname");
if (targetName != NULL && targetName[0] != '\0' && idStr::Icmp(targetName, name) == 0) {
return i;
}
}
return -1;
}
bool COutlinerDlg::IsTargetKey(const char* key) const {
if (key == NULL || key[0] == '\0') {
return false;
}
if (idStr::Icmp(key, "targetname") == 0) {
return false;
}
if (idStr::Icmp(key, "next") == 0) {
return true;
}
return idStr::Icmpn(key, "target", 6) == 0;
}
void COutlinerDlg::ClearItemData() {
for (int i = 0; i < m_itemData.GetSize(); i++) {
outlinerItemData_t* data = (outlinerItemData_t*)m_itemData.GetAt(i);
delete data;
}
m_itemData.RemoveAll();
}
COutlinerDlg::outlinerItemData_t* COutlinerDlg::AllocItemData(entity_t* ent, bool cycleRef) {
outlinerItemData_t* data = new outlinerItemData_t;
data->entity = ent;
data->cycleRef = cycleRef;
m_itemData.Add(data);
return data;
}
entity_t* COutlinerDlg::EntityFromItem(HTREEITEM item) const {
if (item == NULL || !m_tree.GetSafeHwnd()) {
return NULL;
}
outlinerItemData_t* data = (outlinerItemData_t*)m_tree.GetItemData(item);
if (data == NULL) {
return NULL;
}
return data->entity;
}
entity_t* COutlinerDlg::SelectedEntity() const {
if (!m_tree.GetSafeHwnd()) {
return NULL;
}
return EntityFromItem(m_tree.GetSelectedItem());
}
CString COutlinerDlg::EntityDisplayName(entity_t* ent) const {
CString text;
if (ent == NULL) {
return "<null entity>";
}
const char* classname = ValueForKey(ent, "classname");
const char* name = ValueForKey(ent, "name");
if (ent == world_entity) {
text = "worldspawn";
return text;
}
if (name != NULL && name[0] != '\0') {
if (classname != NULL && classname[0] != '\0') {
text.Format("%s [%s]", name, classname);
} else {
text = name;
}
} else if (classname != NULL && classname[0] != '\0') {
text.Format("<unnamed> [%s]", classname);
} else {
text.Format("<entity %d>", ent->entityId);
}
return text;
}
bool COutlinerDlg::EntityMatchesFilter(entity_t* ent) const {
if (m_filterText.IsEmpty()) {
return true;
}
if (ent == NULL) {
return false;
}
CString haystack;
const char* name = ValueForKey(ent, "name");
if (name != NULL && name[0] != '\0') {
haystack = name;
} else if (ent == world_entity) {
haystack = "worldspawn";
} else {
const char* classname = ValueForKey(ent, "classname");
haystack = (classname != NULL) ? classname : "";
}
haystack.MakeLower();
CString needle = m_filterText;
needle.MakeLower();
return haystack.Find(needle) >= 0;
}
bool COutlinerDlg::EntityOrDescendantMatches(entity_t* ent, CPtrArray& stack) const {
if (m_filterText.IsEmpty()) {
return true;
}
if (EntityMatchesFilter(ent)) {
return true;
}
if (ArrayContains(stack, ent)) {
return false;
}
stack.Add(ent);
bool matches = false;
for (int i = 0; i < m_edges.GetSize(); i++) {
const outlinerEdge_t& edge = m_edges.GetAt(i);
if (edge.source != ent) {
continue;
}
if (EntityOrDescendantMatches(edge.target, stack)) {
matches = true;
break;
}
}
stack.RemoveAt(stack.GetSize() - 1);
return matches;
}
bool COutlinerDlg::ArrayContains(const CPtrArray& array, entity_t* ent) const {
for (int i = 0; i < array.GetSize(); i++) {
if ((entity_t*)array.GetAt(i) == ent) {
return true;
}
}
return false;
}
HTREEITEM COutlinerDlg::InsertEntityRecursive(entity_t* ent, HTREEITEM parent, CPtrArray& stack) {
if (ent == NULL) {
return NULL;
}
if (!m_filterText.IsEmpty()) {
CPtrArray filterStack;
if (!EntityOrDescendantMatches(ent, filterStack)) {
return NULL;
}
}
const bool cycleRef = ArrayContains(stack, ent);
CString label = EntityDisplayName(ent);
if (cycleRef) {
label += " (cycle)";
}
HTREEITEM item = m_tree.InsertItem(label, parent, TVI_LAST);
m_tree.SetItemData(item, (DWORD_PTR)AllocItemData(ent, cycleRef));
m_tree.SetCheck(item, Outliner_IsEntityVisible(ent) ? TRUE : FALSE);
const int nodeIndex = FindNode(ent);
if (nodeIndex >= 0) {
m_covered.SetAt(nodeIndex, TRUE);
}
if (cycleRef) {
return item;
}
stack.Add(ent);
for (int i = 0; i < m_edges.GetSize(); i++) {
const outlinerEdge_t& edge = m_edges.GetAt(i);
if (edge.source != ent) {
continue;
}
InsertEntityRecursive(edge.target, item, stack);
}
stack.RemoveAt(stack.GetSize() - 1);
if (!m_filterText.IsEmpty()) {
m_tree.Expand(item, TVE_EXPAND);
}
return item;
}
HTREEITEM COutlinerDlg::FindTreeItemForEntity(entity_t* ent, HTREEITEM start) const {
HTREEITEM item = start;
while (item != NULL) {
if (EntityFromItem(item) == ent) {
return item;
}
HTREEITEM child = m_tree.GetChildItem(item);
if (child != NULL) {
HTREEITEM found = FindTreeItemForEntity(ent, child);
if (found != NULL) {
return found;
}
}
item = m_tree.GetNextSiblingItem(item);
}
return NULL;
}
void COutlinerDlg::ExpandAll(HTREEITEM start) {
HTREEITEM item = start;
while (item != NULL) {
m_tree.Expand(item, TVE_EXPAND);
HTREEITEM child = m_tree.GetChildItem(item);
if (child != NULL) {
ExpandAll(child);
}
item = m_tree.GetNextSiblingItem(item);
}
}
void COutlinerDlg::OnTreeClick(NMHDR* pNMHDR, LRESULT* pResult) {
CPoint point;
::GetCursorPos(&point);
m_tree.ScreenToClient(&point);
UINT flags = 0;
HTREEITEM item = m_tree.HitTest(point, &flags);
if (item != NULL && (flags & TVHT_ONITEMSTATEICON) && EntityFromItem(item) != NULL) {
m_tree.SelectItem(item);
// Let the tree control finish changing the checkbox before we read it.
PostMessage(WM_OUTLINER_APPLY_CHECK, (WPARAM)item, 0);
}
*pResult = 0;
}
LRESULT COutlinerDlg::OnApplyTreeCheck(WPARAM wParam, LPARAM lParam) {
if (!m_tree.GetSafeHwnd()) {
return 0;
}
HTREEITEM item = (HTREEITEM)wParam;
if (item == NULL) {
return 0;
}
entity_t* ent = EntityFromItem(item);
if (ent == NULL) {
return 0;
}
const bool visible = m_tree.GetCheck(item) != FALSE;
Outliner_SetEntityVisible(ent, visible);
ApplyVisibilityChange();
return 0;
}
void COutlinerDlg::CollectEntityAndChildren(entity_t* ent, CPtrArray& out, CPtrArray& stack) const {
if (ent == NULL) {
return;
}
if (ArrayContains(out, ent)) {
return;
}
out.Add(ent);
if (ArrayContains(stack, ent)) {
return;
}
stack.Add(ent);
for (int i = 0; i < m_edges.GetSize(); i++) {
const outlinerEdge_t& edge = m_edges.GetAt(i);
if (edge.source != ent) {
continue;
}
CollectEntityAndChildren(edge.target, out, stack);
}
stack.RemoveAt(stack.GetSize() - 1);
}
void COutlinerDlg::SetEntityVisibility(entity_t* ent, bool visible, bool includeChildren) {
if (ent == NULL) {
return;
}
if (includeChildren) {
CPtrArray entitiesToChange;
CPtrArray stack;
CollectEntityAndChildren(ent, entitiesToChange, stack);
for (int i = 0; i < entitiesToChange.GetSize(); i++) {
Outliner_SetEntityVisible((entity_t*)entitiesToChange.GetAt(i), visible);
}
}
else {
Outliner_SetEntityVisible(ent, visible);
}
ApplyVisibilityChange();
}
void COutlinerDlg::ToggleEntityVisibility(entity_t* ent, bool includeChildren) {
if (ent == NULL) {
return;
}
const bool newVisible = !Outliner_IsEntityVisible(ent);
SetEntityVisibility(ent, newVisible, includeChildren);
}
void COutlinerDlg::ShowOnlyEntityAndChildren(entity_t* ent) {
if (ent == NULL) {
return;
}
CPtrArray keepVisible;
CPtrArray stack;
CollectEntityAndChildren(ent, keepVisible, stack);
for (int i = 0; i < m_entities.GetSize(); i++) {
entity_t* candidate = (entity_t*)m_entities.GetAt(i);
if (i == 0)
{
Outliner_SetEntityVisible(candidate, true);
continue;
}
Outliner_SetEntityVisible(candidate, ArrayContains(keepVisible, candidate));
}
ApplyVisibilityChange();
}
void COutlinerDlg::ShowAllEntities() {
Outliner_ResetEntityVisibility();
ApplyVisibilityChange();
}
void COutlinerDlg::UpdateAllCheckmarks(HTREEITEM start) {
HTREEITEM item = start;
while (item != NULL) {
entity_t* ent = EntityFromItem(item);
if (ent != NULL) {
m_tree.SetCheck(item, Outliner_IsEntityVisible(ent) ? TRUE : FALSE);
}
HTREEITEM child = m_tree.GetChildItem(item);
if (child != NULL) {
UpdateAllCheckmarks(child);
}
item = m_tree.GetNextSiblingItem(item);
}
}
void COutlinerDlg::ApplyVisibilityChange() {
if (m_tree.GetSafeHwnd()) {
m_tree.SetRedraw(FALSE);
UpdateAllCheckmarks(m_tree.GetRootItem());
m_tree.SetRedraw(TRUE);
m_tree.Invalidate();
}
Sys_UpdateWindows(W_ALL);
}
void COutlinerDlg::OnTreeDblClick(NMHDR* pNMHDR, LRESULT* pResult) {
OpenEntityProperties(SelectedEntity());
*pResult = 0;
}
void COutlinerDlg::OnTreeRightClick(NMHDR* pNMHDR, LRESULT* pResult) {
RefreshIfNeeded();
CPoint screenPoint;
::GetCursorPos(&screenPoint);
CPoint treePoint = screenPoint;
m_tree.ScreenToClient(&treePoint);
UINT flags = 0;
HTREEITEM item = m_tree.HitTest(treePoint, &flags);
if (item != NULL) {
m_tree.SelectItem(item);
}
entity_t* ent = SelectedEntity();
const bool entVisible = Outliner_IsEntityVisible(ent);
CString toggleSelfText;
CString toggleChildrenText;
toggleSelfText.Format("%s this entity", entVisible ? "Hide" : "Show");
toggleChildrenText.Format("%s this entity + children", entVisible ? "Hide" : "Show");
CMenu menu;
menu.CreatePopupMenu();
menu.AppendMenu(MF_STRING | (ent ? 0 : MF_GRAYED), ID_OUTLINER_OPEN_PROPERTIES, "Open entity properties");
menu.AppendMenu(MF_STRING | (ent ? 0 : MF_GRAYED), ID_OUTLINER_MOVE_TO_ENTITY, "Move to entity");
menu.AppendMenu(MF_SEPARATOR);
menu.AppendMenu(MF_STRING | (ent ? 0 : MF_GRAYED), ID_OUTLINER_TOGGLE_VISIBILITY, toggleSelfText);
menu.AppendMenu(MF_STRING | (ent ? 0 : MF_GRAYED), ID_OUTLINER_TOGGLE_VISIBILITY_CHILDREN, toggleChildrenText);
menu.AppendMenu(MF_STRING | (ent ? 0 : MF_GRAYED), ID_OUTLINER_SHOW_ONLY_THIS_AND_CHILDREN, "Show only this entity + children");
menu.AppendMenu(MF_STRING, ID_OUTLINER_SHOW_ALL, "Show all entities");
menu.AppendMenu(MF_SEPARATOR);
menu.AppendMenu(MF_STRING, ID_OUTLINER_REFRESH, "Refresh");
const int command = menu.TrackPopupMenu(
TPM_LEFTALIGN | TPM_RIGHTBUTTON | TPM_RETURNCMD,
screenPoint.x,
screenPoint.y,
this
);
switch (command) {
case ID_OUTLINER_OPEN_PROPERTIES:
OpenEntityProperties(ent);
break;
case ID_OUTLINER_MOVE_TO_ENTITY:
MoveToEntity(ent);
break;
case ID_OUTLINER_TOGGLE_VISIBILITY:
ToggleEntityVisibility(ent, false);
break;
case ID_OUTLINER_TOGGLE_VISIBILITY_CHILDREN:
ToggleEntityVisibility(ent, true);
break;
case ID_OUTLINER_SHOW_ONLY_THIS_AND_CHILDREN:
ShowOnlyEntityAndChildren(ent);
break;
case ID_OUTLINER_SHOW_ALL:
ShowAllEntities();
break;
case ID_OUTLINER_REFRESH:
Refresh(true);
break;
default:
break;
}
*pResult = 0;
}
void COutlinerDlg::SelectEntityInMap(entity_t* ent, bool updateInspector) {
if (ent == NULL) {
return;
}
Select_Deselect();
if (ent != world_entity) {
brush_t* firstBrush = ent->brushes.onext;
if (firstBrush != NULL && firstBrush != &ent->brushes) {
Select_Brush(firstBrush, true, true);
}
}
if (updateInspector && g_Inspectors != NULL) {
if (ent->eclass != NULL) {
g_Inspectors->UpdateEntitySel(ent->eclass);
}
g_Inspectors->entityDlg.SetEditEntity(ent);
g_Inspectors->UpdateSelectedEntity();
}
Sys_UpdateWindows(W_ALL);
}
void COutlinerDlg::OpenEntityProperties(entity_t* ent) {
if (ent == NULL || g_Inspectors == NULL) {
return;
}
SelectEntityInMap(ent, true);
g_Inspectors->SetMode(W_ENTITY);
}
bool COutlinerDlg::GetEntityCenter(entity_t* ent, idVec3& center) const {
if (ent == NULL) {
return false;
}
if (GetVectorForKey(ent, "origin", center)) {
return true;
}
idBounds bounds;
bounds.Clear();
bool haveBounds = false;
for (brush_t* b = ent->brushes.onext; b != NULL && b != &ent->brushes; b = b->onext) {
bounds.AddPoint(b->mins);
bounds.AddPoint(b->maxs);
haveBounds = true;
}
if (haveBounds) {
center = bounds.GetCenter();
return true;
}
center.Zero();
return false;
}
void COutlinerDlg::MoveToEntity(entity_t* ent) {
if (ent == NULL || g_pParentWnd == NULL) {
return;
}
idVec3 center;
if (!GetEntityCenter(ent, center)) {
return;
}
SelectEntityInMap(ent, false);
if (g_pParentWnd->GetCamera() != NULL) {
g_pParentWnd->GetCamera()->Camera().origin = center;
}
if (g_pParentWnd->GetXYWnd() != NULL) {
g_pParentWnd->GetXYWnd()->SetOrigin(center);
}
if (g_pParentWnd->GetXZWnd() != NULL) {
g_pParentWnd->GetXZWnd()->SetOrigin(center);
}
if (g_pParentWnd->GetYZWnd() != NULL) {
g_pParentWnd->GetYZWnd()->SetOrigin(center);
}
Sys_UpdateWindows(W_ALL);
}