mirror of
https://github.com/jmarshall23/DoomRTX.git
synced 2026-08-16 08:10:32 +02:00
931 lines
20 KiB
C++
931 lines
20 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"
|
|
|
|
IMPLEMENT_DYNAMIC(COutlinerDlg, CWnd)
|
|
|
|
static const COLORREF OUTLINER_BG = RGB(245, 247, 250);
|
|
static const COLORREF OUTLINER_PANEL_BG = RGB(255, 255, 255);
|
|
static const COLORREF OUTLINER_TEXT = RGB(32, 36, 42);
|
|
static const COLORREF OUTLINER_MUTED_TEXT = RGB(88, 96, 105);
|
|
|
|
static const 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;
|
|
|
|
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)
|
|
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),
|
|
(HBRUSH)(COLOR_BTNFACE + 1),
|
|
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,
|
|
CRect(0, 0, 10, 10),
|
|
this,
|
|
IDC_OUTLINER_TREE
|
|
);
|
|
m_tree.ModifyStyleEx(0, WS_EX_CLIENTEDGE, SWP_FRAMECHANGED);
|
|
m_tree.SetItemHeight(20);
|
|
|
|
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_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();
|
|
|
|
BuildGraph();
|
|
|
|
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);
|
|
insertedAny = true;
|
|
}
|
|
|
|
CPtrArray stack;
|
|
InsertEntityRecursive(ent, loopGroup, stack);
|
|
}
|
|
|
|
if (loopGroup != NULL) {
|
|
m_tree.Expand(loopGroup, TVE_EXPAND);
|
|
}
|
|
|
|
if (!insertedAny) {
|
|
if (m_filterText.IsEmpty()) {
|
|
m_tree.InsertItem("<no entities>", TVI_ROOT, TVI_LAST);
|
|
} else {
|
|
m_tree.InsertItem("<no matching entities>", TVI_ROOT, TVI_LAST);
|
|
}
|
|
}
|
|
|
|
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 = currentmap;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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));
|
|
|
|
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::OnTreeDblClick(NMHDR* pNMHDR, LRESULT* pResult) {
|
|
OpenEntityProperties(SelectedEntity());
|
|
*pResult = 0;
|
|
}
|
|
|
|
void COutlinerDlg::OnTreeRightClick(NMHDR* pNMHDR, LRESULT* pResult) {
|
|
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();
|
|
|
|
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, 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_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);
|
|
}
|