Added the ability to hide/show entities in the outliner

This commit is contained in:
Justin Marshall
2026-05-06 15:04:39 -07:00
parent c1defbad40
commit 7c5ddb4336
3 changed files with 557 additions and 7 deletions
+367 -4
View File
@@ -42,6 +42,144 @@ 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()
@@ -53,6 +191,8 @@ BEGIN_MESSAGE_MAP(COutlinerDlg, CWnd)
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() {
@@ -133,7 +273,7 @@ int COutlinerDlg::OnCreate(LPCREATESTRUCT lpCreateStruct) {
m_tree.Create(
WS_CHILD | WS_VISIBLE | WS_TABSTOP |
TVS_HASBUTTONS | TVS_HASLINES | TVS_LINESATROOT |
TVS_SHOWSELALWAYS | TVS_DISABLEDRAGDROP,
TVS_SHOWSELALWAYS | TVS_DISABLEDRAGDROP | TVS_CHECKBOXES,
CRect(0, 0, 10, 10),
this,
IDC_OUTLINER_TREE
@@ -299,6 +439,15 @@ BOOL COutlinerDlg::PreTranslateMessage(MSG* pMsg) {
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;
@@ -349,7 +498,30 @@ void COutlinerDlg::Refresh(bool force) {
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();
@@ -383,6 +555,7 @@ void COutlinerDlg::Refresh(bool force) {
if (loopGroup == NULL) {
loopGroup = m_tree.InsertItem("Target loops / no root", TVI_ROOT, TVI_LAST);
m_tree.SetItemState(loopGroup, 0, TVIS_STATEIMAGEMASK);
insertedAny = true;
}
@@ -395,10 +568,14 @@ void COutlinerDlg::Refresh(bool force) {
}
if (!insertedAny) {
HTREEITEM emptyItem = NULL;
if (m_filterText.IsEmpty()) {
m_tree.InsertItem("<no entities>", TVI_ROOT, TVI_LAST);
emptyItem = m_tree.InsertItem("<no entities>", TVI_ROOT, TVI_LAST);
} else {
m_tree.InsertItem("<no matching entities>", TVI_ROOT, TVI_LAST);
emptyItem = m_tree.InsertItem("<no matching entities>", TVI_ROOT, TVI_LAST);
}
if (emptyItem != NULL) {
m_tree.SetItemState(emptyItem, 0, TVIS_STATEIMAGEMASK);
}
}
@@ -425,7 +602,7 @@ void COutlinerDlg::Refresh(bool force) {
m_lastWorldEntity = world_entity;
m_lastEntityCount = g_qeglobals.d_num_entities;
m_lastMapModified = mapModified;
m_lastMapName = currentmap;
m_lastMapName = Outliner_CurrentMapName();
}
void COutlinerDlg::BuildGraph() {
@@ -565,6 +742,10 @@ bool COutlinerDlg::IsTargetKey(const char* key) const {
return false;
}
if (idStr::Icmp(key, "next") == 0) {
return true;
}
return idStr::Icmpn(key, "target", 6) == 0;
}
@@ -723,6 +904,7 @@ HTREEITEM COutlinerDlg::InsertEntityRecursive(entity_t* ent, HTREEITEM parent, C
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) {
@@ -787,12 +969,170 @@ void COutlinerDlg::ExpandAll(HTREEITEM start) {
}
}
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);
@@ -806,12 +1146,23 @@ void COutlinerDlg::OnTreeRightClick(NMHDR* pNMHDR, LRESULT* pResult) {
}
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(
@@ -828,6 +1179,18 @@ void COutlinerDlg::OnTreeRightClick(NMHDR* pNMHDR, LRESULT* pResult) {
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;