mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-06-16 05:13:58 -07:00
Some checks failed
checkrefs / lfscheck (push) Has been cancelled
checkrefs / checkrefs (push) Has been cancelled
lint / cppcheck (push) Has been cancelled
lint / copyright (push) Has been cancelled
lint / jenkinsfiles (push) Has been cancelled
pre-commit / build (push) Has been cancelled
This shifts the responsibility of updating the actual size more towards IGUIObject, and enables only ever doing it when the value is actually needed. This allows us to remove the delay of size changed notifications, since the value is now already recalculated as infrequently as possible anyways. All of that ensures that the actual size (returned by GetActualSize) is always up-to-date e.g. when reading it from the parent, which was previously broken. Fixes #8200
570 lines
15 KiB
C++
570 lines
15 KiB
C++
/* Copyright (C) 2026 Wildfire Games.
|
|
* This file is part of 0 A.D.
|
|
*
|
|
* 0 A.D. 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 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* 0 A.D. 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 0 A.D. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "precompiled.h"
|
|
|
|
#include "IGUIObject.h"
|
|
|
|
#include "gui/CGUI.h"
|
|
#include "gui/CGUISetting.h"
|
|
#include "gui/SGUIStyle.h"
|
|
#include "gui/Scripting/JSInterface_GUIProxy.h"
|
|
#include "js/Conversions.h"
|
|
#include "lib/debug.h"
|
|
#include "lib/secure_crt.h"
|
|
#include "maths/Size2D.h"
|
|
#include "maths/Vector2D.h"
|
|
#include "ps/CLogger.h"
|
|
#include "ps/Profiler2.h"
|
|
#include "scriptinterface/Object.h"
|
|
#include "scriptinterface/ScriptInterface.h"
|
|
#include "soundmanager/ISoundManager.h"
|
|
|
|
#include <algorithm>
|
|
#include <js/CallAndConstruct.h>
|
|
#include <js/ComparisonOperators.h>
|
|
#include <js/CompilationAndEvaluation.h>
|
|
#include <js/CompileOptions.h>
|
|
#include <js/GCAPI.h>
|
|
#include <js/GCVector.h>
|
|
#include <js/SourceText.h>
|
|
#include <js/TracingAPI.h>
|
|
#include <js/Value.h>
|
|
#include <jsapi.h>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <tuple>
|
|
#include <unordered_map>
|
|
#include <utility>
|
|
|
|
class JSObject;
|
|
namespace mozilla { union Utf8Unit; }
|
|
|
|
const CStr IGUIObject::EventNameMouseEnter = "MouseEnter";
|
|
const CStr IGUIObject::EventNameMouseMove = "MouseMove";
|
|
const CStr IGUIObject::EventNameMouseLeave = "MouseLeave";
|
|
|
|
IGUIObject::IGUIObject(CGUI& pGUI)
|
|
: m_pGUI(pGUI),
|
|
m_pParent(),
|
|
m_MouseHovering(),
|
|
m_LastClickTime(),
|
|
m_Enabled(this, "enabled", true),
|
|
m_Hidden(this, "hidden", false),
|
|
m_Size(this, "size"),
|
|
m_Style(this, "style"),
|
|
m_Hotkey(this, "hotkey"),
|
|
m_Z(this, "z"),
|
|
m_Absolute(this, "absolute", true),
|
|
m_Ghost(this, "ghost", false),
|
|
m_AspectRatio(this, "aspectratio"),
|
|
m_Tooltip(this, "tooltip"),
|
|
m_TooltipStyle(this, "tooltip_style")
|
|
{
|
|
}
|
|
|
|
IGUIObject::~IGUIObject()
|
|
{
|
|
if (!m_ScriptHandlers.empty())
|
|
JS_RemoveExtraGCRootsTracer(m_pGUI.GetScriptInterface()->GetGeneralJSContext(), Trace, this);
|
|
|
|
// m_Children is deleted along all other GUI Objects in the CGUI destructor
|
|
}
|
|
|
|
void IGUIObject::RegisterChild(IGUIObject* child)
|
|
{
|
|
child->SetParent(this);
|
|
m_Children.push_back(child);
|
|
}
|
|
|
|
void IGUIObject::UnregisterChild(IGUIObject* child)
|
|
{
|
|
std::vector<IGUIObject*>::iterator it = std::find(m_Children.begin(), m_Children.end(), child);
|
|
if (it != m_Children.end())
|
|
{
|
|
(*it)->m_pParent = nullptr;
|
|
m_Children.erase(it);
|
|
}
|
|
}
|
|
|
|
void IGUIObject::RegisterSetting(const CStr& Name, IGUISetting* setting)
|
|
{
|
|
if (SettingExists(Name))
|
|
LOGERROR("The setting '%s' already exists on the object '%s'!", Name.c_str(), GetPresentableName().c_str());
|
|
else
|
|
m_Settings.emplace(Name, setting);
|
|
}
|
|
|
|
void IGUIObject::ReregisterSetting(const CStr& Name, IGUISetting* setting)
|
|
{
|
|
if (!SettingExists(Name))
|
|
LOGERROR("The setting '%s' must already exist on the object '%s'!", Name.c_str(), GetPresentableName().c_str());
|
|
else
|
|
m_Settings.at(Name) = setting;
|
|
}
|
|
|
|
bool IGUIObject::SettingExists(const CStr& Setting) const
|
|
{
|
|
return m_Settings.find(Setting) != m_Settings.end();
|
|
}
|
|
|
|
bool IGUIObject::SetSettingFromString(const CStr& Setting, const CStrW& Value, const bool SendMessage)
|
|
{
|
|
const std::map<CStr, IGUISetting*>::iterator it = m_Settings.find(Setting);
|
|
if (it == m_Settings.end())
|
|
{
|
|
LOGERROR("GUI object '%s' has no property called '%s', can't set parse and set value '%s'", GetPresentableName().c_str(), Setting.c_str(), Value.ToUTF8().c_str());
|
|
return false;
|
|
}
|
|
return it->second->FromString(Value, SendMessage);
|
|
}
|
|
|
|
void IGUIObject::SettingChanged(const CStr& Setting, const bool SendMessage)
|
|
{
|
|
if (Setting == "size")
|
|
{
|
|
// Notify all children that they'll have to re-cache their actual size.
|
|
RecurseObject(nullptr, &IGUIObject::HandleSizeChanged);
|
|
}
|
|
else if (Setting == "hidden")
|
|
{
|
|
// Hiding an object requires us to reset it and all children
|
|
if (m_Hidden)
|
|
RecurseObject(nullptr, &IGUIObject::ResetStates);
|
|
}
|
|
else if (Setting == "style")
|
|
m_pGUI.SetObjectStyle(*this, m_Style);
|
|
|
|
if (SendMessage)
|
|
{
|
|
SGUIMessage msg(GUIM_SETTINGS_UPDATED, Setting);
|
|
HandleMessage(msg);
|
|
|
|
// inform to parents until get to the root object
|
|
// for now only size and hidden settings are bubbled up
|
|
if (GetParent() && (Setting == "size" || Setting == "hidden"))
|
|
{
|
|
EGUIMessageType type = Setting == "size" ? GUIM_CHILD_RESIZED : GUIM_CHILD_TOGGLE_VISIBILITY;
|
|
SGUIMessage msg(type, GetName());
|
|
msg.Skip(true);
|
|
|
|
IGUIObject* parent = GetParent();
|
|
while (parent)
|
|
{
|
|
parent->HandleMessage(msg);
|
|
|
|
if (!msg.skipped)
|
|
break;
|
|
|
|
parent = parent->GetParent();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool IGUIObject::IsMouseOver() const
|
|
{
|
|
if (m_VisibleArea)
|
|
return m_VisibleArea.PointInside(m_pGUI.GetMousePos());
|
|
|
|
return GetActualSize().PointInside(m_pGUI.GetMousePos());
|
|
}
|
|
|
|
void IGUIObject::UpdateMouseOver(IGUIObject* const& pMouseOver)
|
|
{
|
|
if (pMouseOver == this)
|
|
{
|
|
if (!m_MouseHovering)
|
|
SendMouseEvent(GUIM_MOUSE_ENTER,EventNameMouseEnter);
|
|
|
|
m_MouseHovering = true;
|
|
|
|
SendMouseEvent(GUIM_MOUSE_OVER, EventNameMouseMove);
|
|
}
|
|
else
|
|
{
|
|
if (m_MouseHovering)
|
|
{
|
|
m_MouseHovering = false;
|
|
SendMouseEvent(GUIM_MOUSE_LEAVE, EventNameMouseLeave);
|
|
}
|
|
}
|
|
}
|
|
|
|
void IGUIObject::ChooseMouseOverAndClosest(IGUIObject*& pObject)
|
|
{
|
|
if (!IsMouseOver())
|
|
return;
|
|
|
|
// Check if we've got competition at all
|
|
if (pObject == nullptr)
|
|
{
|
|
pObject = this;
|
|
return;
|
|
}
|
|
|
|
// Or if it's closer
|
|
if (GetBufferedZ() >= pObject->GetBufferedZ())
|
|
{
|
|
pObject = this;
|
|
return;
|
|
}
|
|
}
|
|
|
|
IGUIObject* IGUIObject::GetParent() const
|
|
{
|
|
// Important, we're not using GetParent() for these
|
|
// checks, that could screw it up
|
|
if (m_pParent && m_pParent->m_pParent == nullptr)
|
|
return nullptr;
|
|
|
|
return m_pParent;
|
|
}
|
|
|
|
void IGUIObject::ResetStates()
|
|
{
|
|
// Notify the gui that we aren't hovered anymore
|
|
UpdateMouseOver(nullptr);
|
|
}
|
|
|
|
void IGUIObject::HandleSizeChanged()
|
|
{
|
|
m_CachedActualSizeDirty = true;
|
|
}
|
|
|
|
const CRect& IGUIObject::GetActualSize() const
|
|
{
|
|
if (std::exchange(m_CachedActualSizeDirty, false))
|
|
RecalculateActualSize();
|
|
|
|
return m_CachedActualSize;
|
|
}
|
|
|
|
void IGUIObject::RecalculateActualSize() const
|
|
{
|
|
// If absolute="false" and the object has got a parent,
|
|
// use its cached size instead of the screen.
|
|
if (!m_Absolute && m_pParent && !IsRootObject())
|
|
m_CachedActualSize = m_Size->GetSize(m_pParent->GetActualSize());
|
|
else
|
|
m_CachedActualSize = m_Size->GetSize(CRect(m_pGUI.GetWindowSize()));
|
|
|
|
// In a few cases, GUI objects have to resize to fill the screen
|
|
// but maintain a constant aspect ratio.
|
|
// Adjust the size to be the max possible, centered in the original size:
|
|
if (m_AspectRatio)
|
|
{
|
|
if (m_CachedActualSize.GetWidth() > m_CachedActualSize.GetHeight() * m_AspectRatio)
|
|
{
|
|
float delta = m_CachedActualSize.GetWidth() - m_CachedActualSize.GetHeight() * m_AspectRatio;
|
|
m_CachedActualSize.left += delta/2.f;
|
|
m_CachedActualSize.right -= delta/2.f;
|
|
}
|
|
else
|
|
{
|
|
float delta = m_CachedActualSize.GetHeight() - m_CachedActualSize.GetWidth() / m_AspectRatio;
|
|
m_CachedActualSize.bottom -= delta/2.f;
|
|
m_CachedActualSize.top += delta/2.f;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool IGUIObject::ApplyStyle(const CStr& StyleName)
|
|
{
|
|
if (!m_pGUI.HasStyle(StyleName))
|
|
{
|
|
LOGERROR("IGUIObject: Trying to use style '%s' that doesn't exist.", StyleName.c_str());
|
|
return false;
|
|
}
|
|
|
|
// The default style may specify settings for any GUI object.
|
|
// Other styles are reported if they specify a Setting that does not exist,
|
|
// so that the XML author is informed and can correct the style.
|
|
|
|
for (const std::pair<const CStr, CStrW>& p : m_pGUI.GetStyle(StyleName).m_SettingsDefaults)
|
|
{
|
|
if (SettingExists(p.first))
|
|
m_Settings.at(p.first)->FromString(p.second, true);
|
|
else if (StyleName != "default")
|
|
LOGWARNING("GUI object has no setting \"%s\", but the style \"%s\" defines it", p.first, StyleName.c_str());
|
|
}
|
|
return true;
|
|
}
|
|
|
|
float IGUIObject::GetBufferedZ() const
|
|
{
|
|
if (m_Absolute)
|
|
return m_Z;
|
|
|
|
if (GetParent())
|
|
return GetParent()->GetBufferedZ() + m_Z;
|
|
|
|
// In philosophy, a parentless object shouldn't be able to have a relative sizing,
|
|
// but we'll accept it so that absolute can be used as default without a complaint.
|
|
// Also, you could consider those objects children to the screen resolution.
|
|
return m_Z;
|
|
}
|
|
|
|
void IGUIObject::RegisterScriptHandler(const CStr& eventName, const CStr& Code, CGUI& pGUI)
|
|
{
|
|
ScriptRequest rq(pGUI.GetScriptInterface());
|
|
|
|
const int paramCount = 1;
|
|
const char* paramNames[paramCount] = { "mouse" };
|
|
|
|
// Location to report errors from
|
|
CStr CodeName = GetName() + " " + eventName;
|
|
|
|
// Generate a unique name
|
|
static int x = 0;
|
|
char buf[64];
|
|
sprintf_s(buf, ARRAY_SIZE(buf), "__eventhandler%d (%s)", x++, eventName.c_str());
|
|
|
|
// TODO: this is essentially the same code as ScriptInterface::LoadScript (with a tweak for the argument).
|
|
JS::CompileOptions options(rq.cx);
|
|
options.setFileAndLine(CodeName.c_str(), 0);
|
|
options.setIsRunOnce(false);
|
|
options.setForceStrictMode();
|
|
|
|
JS::SourceText<mozilla::Utf8Unit> src;
|
|
ENSURE(src.init(rq.cx, Code.c_str(), Code.length(), JS::SourceOwnership::Borrowed));
|
|
JS::RootedObjectVector emptyScopeChain(rq.cx);
|
|
JS::RootedFunction func(rq.cx, JS::CompileFunction(rq.cx, emptyScopeChain, options, buf, paramCount, paramNames, src));
|
|
if (func == nullptr)
|
|
{
|
|
LOGERROR("RegisterScriptHandler: Failed to compile the script for %s", eventName.c_str());
|
|
return;
|
|
}
|
|
|
|
JS::RootedObject funcObj(rq.cx, JS_GetFunctionObject(func));
|
|
SetScriptHandler(eventName, funcObj);
|
|
}
|
|
|
|
void IGUIObject::SetScriptHandler(const CStr& eventName, JS::HandleObject Function)
|
|
{
|
|
if (m_ScriptHandlers.empty())
|
|
JS_AddExtraGCRootsTracer(m_pGUI.GetScriptInterface()->GetGeneralJSContext(), Trace, this);
|
|
|
|
m_ScriptHandlers[eventName] = JS::Heap<JSObject*>(Function);
|
|
|
|
if (std::find(m_pGUI.m_EventObjects[eventName].begin(), m_pGUI.m_EventObjects[eventName].end(), this) == m_pGUI.m_EventObjects[eventName].end())
|
|
m_pGUI.m_EventObjects[eventName].emplace_back(this);
|
|
}
|
|
|
|
void IGUIObject::UnsetScriptHandler(const CStr& eventName)
|
|
{
|
|
std::map<CStr, JS::Heap<JSObject*> >::iterator it = m_ScriptHandlers.find(eventName);
|
|
|
|
if (it == m_ScriptHandlers.end())
|
|
return;
|
|
|
|
m_ScriptHandlers.erase(it);
|
|
|
|
if (m_ScriptHandlers.empty())
|
|
JS_RemoveExtraGCRootsTracer(m_pGUI.GetScriptInterface()->GetGeneralJSContext(), Trace, this);
|
|
|
|
std::unordered_map<CStr, std::vector<IGUIObject*>>::iterator it2 = m_pGUI.m_EventObjects.find(eventName);
|
|
if (it2 == m_pGUI.m_EventObjects.end())
|
|
return;
|
|
|
|
std::vector<IGUIObject*>& handlers = it2->second;
|
|
handlers.erase(std::remove(handlers.begin(), handlers.end(), this), handlers.end());
|
|
|
|
if (handlers.empty())
|
|
m_pGUI.m_EventObjects.erase(it2);
|
|
}
|
|
|
|
InReaction IGUIObject::SendEvent(EGUIMessageType type, const CStr& eventName)
|
|
{
|
|
PROFILE2_EVENT("gui event");
|
|
PROFILE2_ATTR("type: %s", eventName.c_str());
|
|
PROFILE2_ATTR("object: %s", m_Name.c_str());
|
|
|
|
SGUIMessage msg(type);
|
|
HandleMessage(msg);
|
|
|
|
ScriptEvent(eventName);
|
|
|
|
return msg.skipped ? IN_PASS : IN_HANDLED;
|
|
}
|
|
|
|
InReaction IGUIObject::SendMouseEvent(EGUIMessageType type, const CStr& eventName)
|
|
{
|
|
PROFILE2_EVENT("gui mouse event");
|
|
PROFILE2_ATTR("type: %s", eventName.c_str());
|
|
PROFILE2_ATTR("object: %s", m_Name.c_str());
|
|
|
|
SGUIMessage msg(type);
|
|
if (type == GUIM_MOUSE_WHEEL_UP || type == GUIM_MOUSE_WHEEL_DOWN || type == GUIM_MOUSE_WHEEL_LEFT || type == GUIM_MOUSE_WHEEL_RIGHT)
|
|
msg.Skip();
|
|
HandleMessage(msg);
|
|
|
|
ScriptRequest rq(m_pGUI.GetScriptInterface());
|
|
|
|
// Set up the 'mouse' parameter
|
|
JS::RootedValue mouse(rq.cx);
|
|
|
|
const CVector2D& mousePos = m_pGUI.GetMousePos();
|
|
|
|
std::map<CStr, JS::Heap<JSObject*> >::iterator it = m_ScriptHandlers.find(eventName);
|
|
if (it != m_ScriptHandlers.end())
|
|
{
|
|
Script::CreateObject(
|
|
rq,
|
|
&mouse,
|
|
"x", mousePos.X,
|
|
"y", mousePos.Y,
|
|
"buttons", m_pGUI.GetMouseButtons());
|
|
JS::RootedValueVector paramData(rq.cx);
|
|
std::ignore = paramData.append(mouse);
|
|
ScriptEvent(eventName, paramData);
|
|
|
|
if (type == GUIM_MOUSE_WHEEL_UP || type == GUIM_MOUSE_WHEEL_DOWN || type == GUIM_MOUSE_WHEEL_LEFT || type == GUIM_MOUSE_WHEEL_RIGHT)
|
|
msg.Skip(false);
|
|
}
|
|
|
|
// inform to parents until get to the root object
|
|
// for now only wheel events are inform to parents
|
|
if ((type == GUIM_MOUSE_WHEEL_UP || type == GUIM_MOUSE_WHEEL_DOWN || type == GUIM_MOUSE_WHEEL_LEFT || type == GUIM_MOUSE_WHEEL_RIGHT) && msg.skipped)
|
|
{
|
|
if (GetParent())
|
|
msg.Skip(GetParent()->SendMouseEvent(type, eventName) == IN_PASS);
|
|
else
|
|
msg.Skip(false);
|
|
}
|
|
|
|
return msg.skipped ? IN_PASS : IN_HANDLED;
|
|
}
|
|
|
|
bool IGUIObject::ScriptEvent(const CStr& eventName,
|
|
const JS::HandleValueArray& paramData /* = JS::HandleValueArray::empty() */)
|
|
{
|
|
std::map<CStr, JS::Heap<JSObject*> >::iterator it = m_ScriptHandlers.find(eventName);
|
|
if (it == m_ScriptHandlers.end())
|
|
return false;
|
|
|
|
ScriptRequest rq(m_pGUI.GetScriptInterface());
|
|
JS::RootedObject obj(rq.cx, GetJSObject());
|
|
JS::RootedValue handlerVal(rq.cx, JS::ObjectValue(*it->second));
|
|
JS::RootedValue result(rq.cx);
|
|
|
|
if (!JS_CallFunctionValue(rq.cx, obj, handlerVal, paramData, &result))
|
|
{
|
|
LOGERROR("Errors executing script event \"%s\"", eventName.c_str());
|
|
ScriptException::CatchPending(rq);
|
|
return false;
|
|
}
|
|
return JS::ToBoolean(result);
|
|
}
|
|
|
|
JSObject* IGUIObject::GetJSObject()
|
|
{
|
|
// Cache the object when somebody first asks for it, because otherwise
|
|
// we end up doing far too much object allocation.
|
|
if (!m_JSObject)
|
|
CreateJSObject();
|
|
|
|
return m_JSObject->Get();
|
|
}
|
|
|
|
bool IGUIObject::IsEnabled() const
|
|
{
|
|
return m_Enabled;
|
|
}
|
|
|
|
bool IGUIObject::IsHidden() const
|
|
{
|
|
return m_Hidden;
|
|
}
|
|
|
|
bool IGUIObject::IsHiddenOrGhost() const
|
|
{
|
|
return m_Hidden || m_Ghost;
|
|
}
|
|
|
|
void IGUIObject::PlaySound(const CStrW& soundPath) const
|
|
{
|
|
if (g_SoundManager && !soundPath.empty())
|
|
g_SoundManager->PlayAsUI(soundPath.c_str(), false);
|
|
}
|
|
|
|
CStr IGUIObject::GetPresentableName() const
|
|
{
|
|
// __internal(), must be at least 13 letters to be able to be
|
|
// an internal name
|
|
if (m_Name.length() <= 12)
|
|
return m_Name;
|
|
|
|
if (std::string_view{m_Name}.substr(0, 10) == "__internal")
|
|
return CStr("[unnamed object]");
|
|
else
|
|
return m_Name;
|
|
}
|
|
|
|
void IGUIObject::SetFocus()
|
|
{
|
|
m_pGUI.SetFocusedObject(this);
|
|
}
|
|
|
|
void IGUIObject::ReleaseFocus()
|
|
{
|
|
m_pGUI.SetFocusedObject(nullptr);
|
|
}
|
|
|
|
bool IGUIObject::IsFocused() const
|
|
{
|
|
return m_pGUI.GetFocusedObject() == this;
|
|
}
|
|
|
|
bool IGUIObject::IsBaseObject() const
|
|
{
|
|
return this == m_pGUI.GetBaseObject();
|
|
}
|
|
|
|
bool IGUIObject::IsRootObject() const
|
|
{
|
|
return m_pParent == m_pGUI.GetBaseObject();
|
|
}
|
|
|
|
void IGUIObject::TraceMember(JSTracer* trc)
|
|
{
|
|
// Please ensure to adapt the Tracer enabling and disabling in accordance with the GC things traced!
|
|
|
|
for (std::pair<const CStr, JS::Heap<JSObject*>>& handler : m_ScriptHandlers)
|
|
JS::TraceEdge(trc, &handler.second, "IGUIObject::m_ScriptHandlers");
|
|
}
|
|
|
|
void IGUIObject::DrawInArea(CCanvas2D& canvas, CRect& area)
|
|
{
|
|
bool isInsideBoundaries = false;
|
|
RecurseObject(nullptr, &IGUIObject::SetIsInsideBoundaries, isInsideBoundaries);
|
|
if (!area.IntersectWith(GetActualSize()))
|
|
return;
|
|
|
|
CRect intersection = area.Intersection(GetActualSize());
|
|
|
|
m_VisibleArea = intersection;
|
|
|
|
Draw(canvas);
|
|
|
|
isInsideBoundaries = true;
|
|
RecurseObject(nullptr, &IGUIObject::SetIsInsideBoundaries, isInsideBoundaries);
|
|
}
|
|
|
|
bool IGUIObject::IsHiddenOrGhostOrOutOfBoundaries() const {
|
|
return !m_IsInsideBoundaries || IsHiddenOrGhost();
|
|
}
|