mirror of
https://github.com/jmarshall23/DoomRTX.git
synced 2026-08-12 16:21:04 +02:00
1501 lines
36 KiB
C++
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);
|
|
}
|