/* =========================================================================== 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 . =========================================================================== */ #include "precompiled.h" #pragma hdrstop #include "qe3.h" #include "Radiant.h" #include "OutlinerDlg.h" #include "InspectorDialog.h" #include #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 idEditorEntity* s_outlinerVisibilityWorldEntity = NULL; static idEditorEntity* 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 idEditorEntity* Outliner_FirstMapEntity() { if (entities.next != NULL && entities.next != &entities) { return entities.next; } return NULL; } static bool Outliner_ArrayContainsPtr(const CPtrArray& array, idEditorEntity* ent) { for (int i = 0; i < array.GetSize(); i++) { if ((idEditorEntity*)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 idEditorEntity* 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(idEditorEntity* ent) { for (int i = 0; i < s_outlinerHiddenEntities.GetSize(); i++) { if ((idEditorEntity*)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--) { idEditorEntity* ent = (idEditorEntity*)s_outlinerHiddenEntities.GetAt(i); if (!Outliner_ArrayContainsPtr(currentEntities, ent)) { s_outlinerHiddenEntities.RemoveAt(i); } } } bool Outliner_IsEntityVisible(idEditorEntity* 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(idEditorEntity* source, idEditorEntity* target) { return Outliner_IsEntityVisible(source) && Outliner_IsEntityVisible(target); } void Outliner_SetEntityVisible(idEditorEntity* 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()) { idEditorEntity* 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; } idEditorEntity* 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((idEditorEntity*)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; } idEditorEntity* ent = (idEditorEntity*)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("", TVI_ROOT, TVI_LAST); } else { emptyItem = m_tree.InsertItem("", 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 (idEditorEntity* ent = entities.next; ent != &entities; ent = ent->next) { AddEntityNode(ent); } } for (int i = 0; i < m_entities.GetSize(); i++) { idEditorEntity* source = (idEditorEntity*)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; } idEditorEntity* target = (idEditorEntity*)m_entities.GetAt(targetIndex); AddEdge(source, target); } } } void COutlinerDlg::AddEntityNode(idEditorEntity* 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(idEditorEntity* source, idEditorEntity* 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(idEditorEntity* source, idEditorEntity* 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(idEditorEntity* ent) const { for (int i = 0; i < m_entities.GetSize(); i++) { if ((idEditorEntity*)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++) { idEditorEntity* ent = (idEditorEntity*)m_entities.GetAt(i); if (ent == NULL) { continue; } const char* entName = ent->ValueForKey("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 = ent->ValueForKey("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(idEditorEntity* ent, bool cycleRef) { outlinerItemData_t* data = new outlinerItemData_t; data->entity = ent; data->cycleRef = cycleRef; m_itemData.Add(data); return data; } idEditorEntity* 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; } idEditorEntity* COutlinerDlg::SelectedEntity() const { if (!m_tree.GetSafeHwnd()) { return NULL; } return EntityFromItem(m_tree.GetSelectedItem()); } CString COutlinerDlg::EntityDisplayName(idEditorEntity* ent) const { CString text; if (ent == NULL) { return ""; } const char* classname = ent->ValueForKey("classname"); const char* name = ent->ValueForKey("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(" [%s]", classname); } else { text.Format("", ent->entityId); } return text; } bool COutlinerDlg::EntityMatchesFilter(idEditorEntity* ent) const { if (m_filterText.IsEmpty()) { return true; } if (ent == NULL) { return false; } CString haystack; const char* name = ent->ValueForKey("name"); if (name != NULL && name[0] != '\0') { haystack = name; } else if (ent == world_entity) { haystack = "worldspawn"; } else { const char* classname = ent->ValueForKey("classname"); haystack = (classname != NULL) ? classname : ""; } haystack.MakeLower(); CString needle = m_filterText; needle.MakeLower(); return haystack.Find(needle) >= 0; } bool COutlinerDlg::EntityOrDescendantMatches(idEditorEntity* 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, idEditorEntity* ent) const { for (int i = 0; i < array.GetSize(); i++) { if ((idEditorEntity*)array.GetAt(i) == ent) { return true; } } return false; } HTREEITEM COutlinerDlg::InsertEntityRecursive(idEditorEntity* 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(idEditorEntity* 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; } idEditorEntity* 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(idEditorEntity* 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(idEditorEntity* 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((idEditorEntity*)entitiesToChange.GetAt(i), visible); } } else { Outliner_SetEntityVisible(ent, visible); } ApplyVisibilityChange(); } void COutlinerDlg::ToggleEntityVisibility(idEditorEntity* ent, bool includeChildren) { if (ent == NULL) { return; } const bool newVisible = !Outliner_IsEntityVisible(ent); SetEntityVisibility(ent, newVisible, includeChildren); } void COutlinerDlg::ShowOnlyEntityAndChildren(idEditorEntity* ent) { if (ent == NULL) { return; } CPtrArray keepVisible; CPtrArray stack; CollectEntityAndChildren(ent, keepVisible, stack); for (int i = 0; i < m_entities.GetSize(); i++) { idEditorEntity* candidate = (idEditorEntity*)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) { idEditorEntity* 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); } idEditorEntity* 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(idEditorEntity* ent, bool updateInspector) { if (ent == NULL) { return; } Select_Deselect(); if (ent != world_entity) { idEditorBrush* 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(idEditorEntity* ent) { if (ent == NULL || g_Inspectors == NULL) { return; } SelectEntityInMap(ent, true); g_Inspectors->SetMode(W_ENTITY); } bool COutlinerDlg::GetEntityCenter(idEditorEntity* ent, idVec3& center) const { if (ent == NULL) { return false; } if (ent->GetVectorForKey("origin", center)) { return true; } idBounds bounds; bounds.Clear(); bool haveBounds = false; for (idEditorBrush* 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(idEditorEntity* 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); }