Index: ps/trunk/source/gui/GUITooltip.cpp =================================================================== --- ps/trunk/source/gui/GUITooltip.cpp (revision 1553) +++ ps/trunk/source/gui/GUITooltip.cpp (revision 1554) @@ -1,230 +1,282 @@ #include "precompiled.h" #include "GUITooltip.h" #include "lib/timer.h" #include "IGUIObject.h" #include "CGUI.h" #include "ps/CLogger.h" /* Tooltips: When holding the mouse stationary over an object for some amount of time, the tooltip is displayed. If the mouse moves off that object, the tooltip disappears. If the mouse re-enters an object within a short time, the new tooltip is displayed immediately. (This lets you run the mouse across a series of buttons, without waiting ages for the text to pop up every time.) See Visual Studio's toolbar buttons for an example. Implemented as a state machine: (where "*" lines are checked constantly, and "<" lines are handled on entry to that state) IN MOTION * If the mouse stops, check whether it should have a tooltip and move to 'STATIONARY, NO TOOLTIP' or 'STATIONARY, TOOLIP' + * If the mouse enters an object with a tooltip delay of 0, switch to 'SHOWING' STATIONARY, NO TOOLTIP * If the mouse moves, switch to 'IN MOTION' STATIONARY, TOOLTIP < Set target time = now + tooltip time * If the mouse moves, switch to 'IN MOTION' * If now > target time, switch to 'SHOWING' SHOWING < Start displaying the tooltip * If the mouse leaves the object, check whether the new object has a tooltip and switch to 'SHOWING' or 'COOLING' COOLING (since I can't think of a better name) < Stop displaying the tooltip < Set target time = now + cooldown time * If the mouse has moved and is over a tooltipped object, switch to 'SHOWING' * If now > target time, switch to 'STATIONARY, NO TOOLTIP' */ enum { ST_IN_MOTION, ST_STATIONARY_NO_TOOLTIP, ST_STATIONARY_TOOLTIP, ST_SHOWING, ST_COOLING }; GUITooltip::GUITooltip() : m_State(ST_IN_MOTION), m_PreviousObject(NULL), m_PreviousTooltipName(NULL) { } const double CooldownTime = 0.25; // TODO: Don't hard-code this value -static bool GetTooltip(IGUIObject* obj, CStr* &style) +static bool GetTooltip(IGUIObject* obj, CStr &style) { - if (obj && obj->SettingExists("tooltip-style")) + if (obj && obj->SettingExists("tooltip-style") + && GUI::GetSetting(obj, "tooltip-style", style) == PS_OK) { - // Use GetSettingPointer to avoid unnecessary string-copying. - // (The tooltip code is only run once per frame, but efficiency - // would be nice anyway.) - if (GUI::GetSettingPointer(obj, "tooltip-style", style) == PS_OK - && style->Length()) - - return true; + if (style.Length() == 0) + style = "default"; + return true; } return false; } -// Urgh - this is only a method because it needs to access HandleMessage (which -// is 'protected'), so it needs to be friendable (and so not a static function) -void GUITooltip::ShowTooltip(IGUIObject* obj, CPos pos, CStr& style, CGUI* gui) +static void ShowTooltip(IGUIObject* obj, CPos pos, CStr& style, CGUI* gui) { - IGUIObject* tooltipobj = gui->FindObjectByName(style); + assert(obj); + + // Get the object referenced by 'tooltip-style' + IGUIObject* tooltipobj = gui->FindObjectByName("__tooltip_"+style); if (! tooltipobj) { - LOG_ONCE(ERROR, "gui", "Cannot find tooltip object named '%s'", (const char*)style); + LOG_ONCE(ERROR, "gui", "Cannot find tooltip named '%s'", (const char*)style); return; } - // Unhide the object - GUI::SetSetting(tooltipobj, "hidden", false); - assert(obj); + IGUIObject* usedobj = tooltipobj; // object actually used to display the tooltip in + + CStr usedObjectName; + if (GUI::GetSetting(tooltipobj, "use-object", usedObjectName) == PS_OK + && usedObjectName.Length()) + { + usedobj = gui->FindObjectByName(usedObjectName); + if (! usedobj) + { + LOG_ONCE(ERROR, "gui", "Cannot find object named '%s' used by tooltip '%s'", (const char*)usedObjectName, (const char*)style); + return; + } - // These shouldn't fail: + // Unhide the object. (If it had use-object and hide-object="true", + // still unhide it, because the used object might be hidden by default) + GUI::SetSetting(usedobj, "hidden", false); + } + else + { + // Unhide the object + GUI::SetSetting(usedobj, "hidden", false); + + // Store mouse position inside the CTooltip + if (GUI::SetSetting(usedobj, "_mousepos", pos) != PS_OK) + debug_warn("Failed to set tooltip mouse position"); + } // Retrieve object's 'tooltip' setting CStr text; if (GUI::GetSetting(obj, "tooltip", text) != PS_OK) - debug_warn("Failed to retrieve tooltip text"); + debug_warn("Failed to retrieve tooltip text"); // shouldn't fail // Set tooltip's caption - if (tooltipobj->SetSetting("caption", text) != PS_OK) - debug_warn("Failed to set tooltip caption"); - - // Store mouse position inside the tooltip - if (GUI::SetSetting(tooltipobj, "_mousepos", pos) != PS_OK) - debug_warn("Failed to set tooltip mouse position"); + if (usedobj->SetSetting("caption", text) != PS_OK) + debug_warn("Failed to set tooltip caption"); // shouldn't fail // Make the tooltip object regenerate its text - tooltipobj->HandleMessage(SGUIMessage(GUIM_SETTINGS_UPDATED, "caption")); + usedobj->HandleMessage(SGUIMessage(GUIM_SETTINGS_UPDATED, "caption")); } static void HideTooltip(CStr& style, CGUI* gui) { - IGUIObject* tooltipobj = gui->FindObjectByName(style); + IGUIObject* tooltipobj = gui->FindObjectByName("__tooltip_"+style); if (! tooltipobj) { - LOG_ONCE(ERROR, "gui", "Cannot find tooltip object named '%s'", (const char*)style); + LOG_ONCE(ERROR, "gui", "Cannot find tooltip named '%s'", (const char*)style); return; } - GUI::SetSetting(tooltipobj, "hidden", true); + + CStr usedObjectName; + if (GUI::GetSetting(tooltipobj, "use-object", usedObjectName) == PS_OK + && usedObjectName.Length()) + { + IGUIObject* usedobj = gui->FindObjectByName(usedObjectName); + if (! usedobj) + { + LOG_ONCE(ERROR, "gui", "Cannot find object named '%s' used by tooltip '%s'", (const char*)usedObjectName, (const char*)style); + return; + } + + // Clear the caption + usedobj->SetSetting("caption", ""); + usedobj->HandleMessage(SGUIMessage(GUIM_SETTINGS_UPDATED, "caption")); + + + bool hideobject = true; + GUI::GetSetting(tooltipobj, "hide-object", hideobject); + + // If hide-object was enabled, hide it + if (hideobject) + GUI::SetSetting(usedobj, "hidden", true); + } + else + { + GUI::SetSetting(tooltipobj, "hidden", true); + } + } -static int GetTooltipTime(CStr& style, CGUI* gui) +static int GetTooltipDelay(CStr& style, CGUI* gui) { - int time = 500; // default value (in msec) + int delay = 500; // default value (in msec) - IGUIObject* tooltipobj = gui->FindObjectByName(style); + IGUIObject* tooltipobj = gui->FindObjectByName("__tooltip_"+style); if (! tooltipobj) { LOG_ONCE(ERROR, "gui", "Cannot find tooltip object named '%s'", (const char*)style); - return time; + return delay; } - GUI::GetSetting(tooltipobj, "time", time); - return time; + GUI::GetSetting(tooltipobj, "delay", delay); + return delay; } void GUITooltip::Update(IGUIObject* Nearest, CPos MousePos, CGUI* GUI) { + // Called once per frame, so efficiency isn't vital + + double now = get_time(); - CStr* style = NULL; + CStr style; int nextstate = -1; switch (m_State) { case ST_IN_MOTION: if (MousePos == m_PreviousMousePos) { if (GetTooltip(Nearest, style)) nextstate = ST_STATIONARY_TOOLTIP; else nextstate = ST_STATIONARY_NO_TOOLTIP; } + else + { + // Check for movement onto a zero-delayed tooltip + if (GetTooltip(Nearest, style) && GetTooltipDelay(style, GUI)==0) + + nextstate = ST_SHOWING; + } break; case ST_STATIONARY_NO_TOOLTIP: if (MousePos != m_PreviousMousePos) nextstate = ST_IN_MOTION; break; case ST_STATIONARY_TOOLTIP: if (MousePos != m_PreviousMousePos) nextstate = ST_IN_MOTION; else if (now >= m_Time) { // Make sure the tooltip still exists if (GetTooltip(Nearest, style)) nextstate = ST_SHOWING; else { // Failed to retrieve style - the object has probably been // altered, so just restart the process nextstate = ST_IN_MOTION; } } break; case ST_SHOWING: if (Nearest != m_PreviousObject) { if (GetTooltip(Nearest, style)) nextstate = ST_SHOWING; else nextstate = ST_COOLING; } break; case ST_COOLING: - if (now >= m_Time) - nextstate = ST_IN_MOTION; - else if (Nearest != m_PreviousObject && GetTooltip(Nearest, style)) + if (GetTooltip(Nearest, style)) nextstate = ST_SHOWING; + else if (now >= m_Time) + nextstate = ST_IN_MOTION; break; } - // Handle state-entry code + // Handle state-entry code: if (nextstate != -1) { switch (nextstate) { case ST_STATIONARY_TOOLTIP: - m_Time = now + (double)GetTooltipTime(*style, GUI) / 1000.; + m_Time = now + (double)GetTooltipDelay(style, GUI) / 1000.; break; case ST_SHOWING: - // show tooltip - ShowTooltip(Nearest, MousePos, *style, GUI); - m_PreviousTooltipName = *style; + ShowTooltip(Nearest, MousePos, style, GUI); + m_PreviousTooltipName = style; break; case ST_COOLING: - // hide the tooltip HideTooltip(m_PreviousTooltipName, GUI); m_Time = now + CooldownTime; break; } m_State = nextstate; } m_PreviousMousePos = MousePos; m_PreviousObject = Nearest; } \ No newline at end of file Index: ps/trunk/source/gui/CGUI.cpp =================================================================== --- ps/trunk/source/gui/CGUI.cpp (revision 1553) +++ ps/trunk/source/gui/CGUI.cpp (revision 1554) @@ -1,1764 +1,1763 @@ /* CGUI by Gustav Larsson gee@pyro.nu */ #include "precompiled.h" #include #include #include #include "lib/res/unifont.h" #include "GUI.h" // Types - when including them into the engine. #include "CButton.h" #include "CImage.h" #include "CText.h" #include "CCheckBox.h" #include "CRadioButton.h" #include "CInput.h" #include "CProgressBar.h" #include "CTooltip.h" #include "MiniMap.h" #include "ps/Xeromyces.h" #include "ps/Font.h" #include "Pyrogenesis.h" #include "input.h" #include "OverlayText.h" // TODO Gee: Whatever include CRect/CPos/CSize #include "Overlay.h" #include "scripting/ScriptingHost.h" #include "Hotkey.h" // namespaces used using namespace std; #include "ps/CLogger.h" #define LOG_CATEGORY "gui" // Class for global JavaScript object JSClass GUIClass = { "GUIClass", 0, JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, JS_PropertyStub, JS_EnumerateStub, JS_ResolveStub, JS_ConvertStub, JS_FinalizeStub, }; // Globals used. extern int g_xres, g_yres; extern bool keys[SDLK_LAST]; //------------------------------------------------------------------- // called from main loop when (input) events are received. // event is passed to other handlers if false is returned. // trampoline: we don't want to make the implementation (in CGUI) static //------------------------------------------------------------------- int gui_handler(const SDL_Event* ev) { return g_GUI.HandleEvent(ev); } int CGUI::HandleEvent(const SDL_Event* ev) { int ret = EV_PASS; if (ev->type == SDL_GUIHOTKEYPRESS) { const CStr& objectName = *(CStr*) ev->user.code; IGUIObject* object = FindObjectByName(objectName); if (! object) { LOG(ERROR, LOG_CATEGORY, "Cannot find hotkeyed object '%s'", objectName.c_str()); } else { object->HandleMessage( SGUIMessage( GUIM_PRESSED ) ); object->ScriptEvent("press"); } } else if (ev->type == SDL_MOUSEMOTION) { // Yes the mouse position is stored as float to avoid // constant conversations when operating in a // float-based environment. m_MousePos = CPos((float)ev->motion.x, (float)ev->motion.y); GUI::RecurseObject(GUIRR_HIDDEN | GUIRR_GHOST, m_BaseObject, &IGUIObject::HandleMessage, SGUIMessage(GUIM_MOUSE_MOTION)); } // Update m_MouseButtons. (BUTTONUP is handled later.) else if (ev->type == SDL_MOUSEBUTTONDOWN) { // (0,1,2) = (LMB,RMB,MMB) if (ev->button.button < 3) m_MouseButtons |= (1 << ev->button.button); } // JW: (pre|post)process omitted; what're they for? why would we need any special button_released handling? // Only one object can be hovered IGUIObject *pNearest = NULL; // TODO Gee: (2004-09-08) Big TODO, don't do the below if the SDL_Event is something like a keypress! try { // TODO Gee: Optimizations needed! // these two recursive function are quite overhead heavy. // pNearest will after this point at the hovered object, possibly NULL GUI::RecurseObject(GUIRR_HIDDEN | GUIRR_GHOST, m_BaseObject, &IGUIObject::ChooseMouseOverAndClosest, pNearest); if (ev->type == SDL_MOUSEMOTION && pNearest) pNearest->ScriptEvent("mousemove"); // Now we'll call UpdateMouseOver on *all* objects, // we'll input the one hovered, and they will each // update their own data and send messages accordingly GUI::RecurseObject(GUIRR_HIDDEN | GUIRR_GHOST, m_BaseObject, &IGUIObject::UpdateMouseOver, pNearest); if (ev->type == SDL_MOUSEBUTTONDOWN) { switch (ev->button.button) { case SDL_BUTTON_LEFT: if (pNearest) { if (pNearest != m_FocusedObject) { // Update focused object if (m_FocusedObject) m_FocusedObject->HandleMessage(SGUIMessage(GUIM_LOST_FOCUS)); m_FocusedObject = pNearest; m_FocusedObject->HandleMessage(SGUIMessage(GUIM_GOT_FOCUS)); } pNearest->HandleMessage(SGUIMessage(GUIM_MOUSE_PRESS_LEFT)); pNearest->ScriptEvent("mouseleftpress"); // Block event, so things on the map (behind the GUI) won't be pressed LOG(ERROR, LOG_CATEGORY, "Left click blocked"); ret = EV_HANDLED; } break; case SDL_BUTTON_WHEELDOWN: // wheel down if (pNearest) { pNearest->HandleMessage(SGUIMessage(GUIM_MOUSE_WHEEL_DOWN)); pNearest->ScriptEvent("mousewheeldown"); ret = EV_HANDLED; } break; case SDL_BUTTON_WHEELUP: // wheel up if (pNearest) { pNearest->HandleMessage(SGUIMessage(GUIM_MOUSE_WHEEL_UP)); pNearest->ScriptEvent("mousewheelup"); ret = EV_HANDLED; } break; default: break; } } else if (ev->type == SDL_MOUSEBUTTONUP) { switch (ev->button.button) { case SDL_BUTTON_LEFT: if (pNearest) { pNearest->HandleMessage(SGUIMessage(GUIM_MOUSE_RELEASE_LEFT)); pNearest->ScriptEvent("mouseleftrelease"); ret = EV_HANDLED; } break; } // Reset all states on all visible objects GUI<>::RecurseObject(GUIRR_HIDDEN, m_BaseObject, &IGUIObject::ResetStates); // It will have reset the mouse over of the current hovered, so we'll // have to restore that if (pNearest) pNearest->m_MouseHovering = true; } } catch (PS_RESULT e) { UNUSED(e); debug_warn("CGUI::HandleEvent error"); // TODO Gee: Handle } // JW: what's the difference between mPress and mDown? what's the code below responsible for? /* // Generally if just mouse is clicked if (m_pInput->mDown(NEMM_BUTTON1) && pNearest) { pNearest->HandleMessage(GUIM_MOUSE_DOWN_LEFT); } */ // BUTTONUP's effect on m_MouseButtons is handled after // everything else, so that e.g. 'press' handlers (activated // on button up) see which mouse button had been pressed. if (ev->type == SDL_MOUSEBUTTONUP) { // (0,1,2) = (LMB,RMB,MMB) if (ev->button.button < 3) m_MouseButtons &= ~(1 << ev->button.button); } // Handle keys for input boxes if (GetFocusedObject() && ev->type == SDL_KEYDOWN) { if( (ev->key.keysym.sym != SDLK_ESCAPE ) && !keys[SDLK_LCTRL] && !keys[SDLK_RCTRL] && !keys[SDLK_LALT] && !keys[SDLK_RALT]) { ret = GetFocusedObject()->ManuallyHandleEvent(ev); } // else will return EV_PASS because we never used the button. } return ret; } void CGUI::TickObjects() { CStr action = "tick"; GUI::RecurseObject(0, m_BaseObject, &IGUIObject::ScriptEvent, action); // Also update tooltips: // TODO: Efficiency IGUIObject* pNearest = NULL; GUI::RecurseObject(GUIRR_HIDDEN | GUIRR_GHOST, m_BaseObject, &IGUIObject::ChooseMouseOverAndClosest, pNearest); m_Tooltip.Update(pNearest, m_MousePos, this); } //------------------------------------------------------------------- // Constructor / Destructor //------------------------------------------------------------------- CGUI::CGUI() : m_InternalNameNumber(0), m_MouseButtons(0), m_FocusedObject(NULL) { m_BaseObject = new CGUIDummyObject; m_BaseObject->SetGUI(this); // Construct the parent object for all GUI JavaScript things m_ScriptObject = JS_NewObject(g_ScriptingHost.getContext(), &GUIClass, NULL, NULL); assert(m_ScriptObject != NULL); // How should it handle errors? JS_AddRoot(g_ScriptingHost.getContext(), &m_ScriptObject); // This will make this invisible, not add //m_BaseObject->SetName(BASE_OBJECT_NAME); } CGUI::~CGUI() { if (m_BaseObject) delete m_BaseObject; if (m_ScriptObject) // Let it be garbage-collected JS_RemoveRoot(g_ScriptingHost.getContext(), &m_ScriptObject); } //------------------------------------------------------------------- // Functions //------------------------------------------------------------------- IGUIObject *CGUI::ConstructObject(const CStr& str) { if (m_ObjectTypes.count(str) > 0) return (*m_ObjectTypes[str])(); else { // Error reporting will be handled with the NULL return. return NULL; } } void CGUI::Initialize() { // Add base types! // You can also add types outside the GUI to extend the flexibility of the GUI. // Pyrogenesis though will have all the object types inserted from here. AddObjectType("empty", &CGUIDummyObject::ConstructObject); AddObjectType("button", &CButton::ConstructObject); AddObjectType("image", &CImage::ConstructObject); AddObjectType("text", &CText::ConstructObject); AddObjectType("checkbox", &CCheckBox::ConstructObject); AddObjectType("radiobutton", &CRadioButton::ConstructObject); AddObjectType("progressbar", &CProgressBar::ConstructObject); AddObjectType("minimap", &CMiniMap::ConstructObject); AddObjectType("input", &CInput::ConstructObject); } void CGUI::Process() { /* // TODO Gee: check if m_pInput is valid, otherwise return /// assert(m_pInput); // Pre-process all objects try { GUI::RecurseObject(0, m_BaseObject, &IGUIObject::HandleMessage, GUIM_PREPROCESS); } catch (PS_RESULT e) { return; } // Check mouse over try { // Only one object can be hovered // check which one it is, if any ! IGUIObject *pNearest = NULL; GUI::RecurseObject(GUIRR_HIDDEN, m_BaseObject, &IGUIObject::ChooseMouseOverAndClosest, pNearest); // Now we'll call UpdateMouseOver on *all* objects, // we'll input the one hovered, and they will each // update their own data and send messages accordingly GUI::RecurseObject(GUIRR_HIDDEN, m_BaseObject, &IGUIObject::UpdateMouseOver, pNearest); // If pressed if (m_pInput->mPress(NEMM_BUTTON1) && pNearest) { pNearest->HandleMessage(GUIM_MOUSE_PRESS_LEFT); } else // If released if (m_pInput->mRelease(NEMM_BUTTON1) && pNearest) { pNearest->HandleMessage(GUIM_MOUSE_RELEASE_LEFT); } // Generally if just mouse is clicked if (m_pInput->mDown(NEMM_BUTTON1) && pNearest) { pNearest->HandleMessage(GUIM_MOUSE_DOWN_LEFT); } } catch (PS_RESULT e) { return; } // Post-process all objects try { GUI::RecurseObject(0, m_BaseObject, &IGUIObject::HandleMessage, GUIM_POSTPROCESS); } catch (PS_RESULT e) { return; } */ } void CGUI::Draw() { // Clear the depth buffer, so the GUI is // drawn on top of everything else glClear(GL_DEPTH_BUFFER_BIT); glPushMatrix(); glLoadIdentity(); // Adapt (origio) to being in top left corner and down // just like the mouse position glTranslatef(0.0f, (GLfloat)g_yres, -1000.0f); glScalef(1.0f, -1.f, 1.0f); try { // Recurse IGUIObject::Draw() with restriction: hidden // meaning all hidden objects won't call Draw (nor will it recurse its children) GUI<>::RecurseObject(GUIRR_HIDDEN, m_BaseObject, &IGUIObject::Draw); } catch (PS_RESULT e) { UNUSED(e); glPopMatrix(); // TODO Gee: Report error. debug_warn("CGUI::Draw error"); return; } glPopMatrix(); } void CGUI::DrawSprite(CGUISpriteInstance& Sprite, int CellID, const float &Z, const CRect &Rect, const CRect &Clipping) { // If the sprite doesn't exist (name == ""), don't bother drawing anything if (Sprite.IsEmpty()) return; // TODO: Clipping? glPushMatrix(); glTranslatef(0.0f, 0.0f, Z); Sprite.Draw(Rect, CellID, m_Sprites); glPopMatrix(); } void CGUI::Destroy() { // We can use the map to delete all // now we don't want to cancel all if one Destroy fails map_pObjects::iterator it; for (it = m_pAllObjects.begin(); it != m_pAllObjects.end(); ++it) { try { it->second->Destroy(); } catch (PS_RESULT e) { UNUSED(e); debug_warn("CGUI::Destroy error"); // TODO Gee: Handle } delete it->second; } for (std::map::iterator it2 = m_Sprites.begin(); it2 != m_Sprites.end(); ++it2) for (std::vector::iterator it3 = it2->second.m_Images.begin(); it3 != it2->second.m_Images.end(); ++it3) delete it3->m_Effects; // Clear all m_pAllObjects.clear(); m_Sprites.clear(); m_Icons.clear(); } void CGUI::UpdateResolution() { // Update ALL cached GUI<>::RecurseObject(0, m_BaseObject, &IGUIObject::UpdateCachedSize ); } void CGUI::AddObject(IGUIObject* pObject) { try { // Add CGUI pointer GUI::RecurseObject(0, pObject, &IGUIObject::SetGUI, this); // Add child to base object m_BaseObject->AddChild(pObject); // can throw // Cache tree GUI<>::RecurseObject(0, pObject, &IGUIObject::UpdateCachedSize); // Loaded GUI::RecurseObject(0, pObject, &IGUIObject::HandleMessage, SGUIMessage(GUIM_LOAD)); GUI::RecurseObject(0, pObject, &IGUIObject::ScriptEvent, "load"); } catch (PS_RESULT e) { throw e; } } void CGUI::UpdateObjects() { // We'll fill a temporary map until we know everything // succeeded map_pObjects AllObjects; try { // Fill freshly GUI< map_pObjects >::RecurseObject(0, m_BaseObject, &IGUIObject::AddToPointersMap, AllObjects ); } catch (PS_RESULT e) { // Throw the same error throw e; } // Else actually update the real one m_pAllObjects = AllObjects; } bool CGUI::ObjectExists(const CStr& Name) const { return m_pAllObjects.count(Name) != 0; } IGUIObject* CGUI::FindObjectByName(const CStr& Name) const { map_pObjects::const_iterator it = m_pAllObjects.find(Name); if (it == m_pAllObjects.end()) return NULL; else return it->second; } // private struct used only in GenerateText(...) struct SGenerateTextImage { float m_YFrom, // The image's starting location in Y m_YTo, // The image's end location in Y m_Indentation; // The image width in other words // Some help functions // TODO Gee: CRect => CPoint ? void SetupSpriteCall(const bool Left, SGUIText::SSpriteCall &SpriteCall, const float width, const float y, const CSize &Size, const CStr &TextureName, const float BufferZone, const int CellID) { // TODO Gee: Temp hardcoded values SpriteCall.m_Area.top = y+BufferZone; SpriteCall.m_Area.bottom = y+BufferZone + Size.cy; if (Left) { SpriteCall.m_Area.left = BufferZone; SpriteCall.m_Area.right = Size.cx+BufferZone; } else { SpriteCall.m_Area.left = width-BufferZone - Size.cx; SpriteCall.m_Area.right = width-BufferZone; } SpriteCall.m_CellID = CellID; SpriteCall.m_Sprite = TextureName; m_YFrom = SpriteCall.m_Area.top-BufferZone; m_YTo = SpriteCall.m_Area.bottom+BufferZone; m_Indentation = Size.cx+BufferZone*2; } }; SGUIText CGUI::GenerateText(const CGUIString &string, const CStr& Font, const float &Width, const float &BufferZone, const IGUIObject *pObject) { SGUIText Text; // object we're generating if (string.m_Words.size() == 0) return Text; float x=BufferZone, y=BufferZone; // drawing pointer int from=0; bool done=false; bool FirstLine = true; // Necessary because text in the first line is shorter // (it doesn't count the line spacing) // Images on the left or the right side. vector Images[2]; int pos_last_img=-1; // Position in the string where last img (either left or right) were encountered. // in order to avoid duplicate processing. // Easier to read. bool WordWrapping = (Width != 0); // Go through string word by word for (int i=0; i<(int)string.m_Words.size()-1 && !done; ++i) { // Pre-process each line one time, so we know which floating images // will be added for that line. // Generated stuff is stored in Feedback. CGUIString::SFeedback Feedback; // Preliminary line_height, used for word-wrapping with floating images. float prelim_line_height=0.f; // Width and height of all text calls generated. string.GenerateTextCall(Feedback, Font, string.m_Words[i], string.m_Words[i+1], FirstLine); // Loop through our images queues, to see if images has been added. // Check if this has already been processed. // Also, floating images are only applicable if Word-Wrapping is on if (WordWrapping && i > pos_last_img) { // Loop left/right for (int j=0; j<2; ++j) { for (vector::const_iterator it = Feedback.m_Images[j].begin(); it != Feedback.m_Images[j].end(); ++it) { SGUIText::SSpriteCall SpriteCall; SGenerateTextImage Image; // Y is if no other floating images is above, y. Else it is placed // after the last image, like a stack downwards. float _y; if (Images[j].size() > 0) _y = MAX(y, Images[j].back().m_YTo); else _y = y; // Get Size from Icon database SGUIIcon icon = GetIcon(*it); CSize size = icon.m_Size; Image.SetupSpriteCall((j==CGUIString::SFeedback::Left), SpriteCall, Width, _y, size, icon.m_TextureName, BufferZone, icon.m_CellID); // Check if image is the lowest thing. Text.m_Size.cy = MAX(Text.m_Size.cy, Image.m_YTo); Images[j].push_back(Image); Text.m_SpriteCalls.push_back(SpriteCall); } } } pos_last_img = MAX(pos_last_img, i); x += Feedback.m_Size.cx; prelim_line_height = MAX(prelim_line_height, Feedback.m_Size.cy); // If Width is 0, then there's no word-wrapping, disable NewLine. if ((WordWrapping && (x > Width-BufferZone || Feedback.m_NewLine)) || i == (int)string.m_Words.size()-2) { // Change 'from' to 'i', but first keep a copy of its value. int temp_from = from; from = i; static const int From=0, To=1; //int width_from=0, width_to=width; float width_range[2]; width_range[From] = BufferZone; width_range[To] = Width - BufferZone; // Floating images are only applicable if word-wrapping is enabled. if (WordWrapping) { // Decide width of the line. We need to iterate our floating images. // this won't be exact because we're assuming the line_height // will be as our preliminary calculation said. But that may change, // although we'd have to add a couple of more loops to try straightening // this problem out, and it is very unlikely to happen noticeably if one // structures his text in a stylistically pure fashion. Even if not, it // is still quite unlikely it will happen. // Loop through left and right side, from and to. for (int j=0; j<2; ++j) { for (vector::const_iterator it = Images[j].begin(); it != Images[j].end(); ++it) { // We're working with two intervals here, the image's and the line height's. // let's find the union of these two. float union_from, union_to; union_from = MAX(y, it->m_YFrom); union_to = MIN(y+prelim_line_height, it->m_YTo); // The union is not empty if (union_to > union_from) { if (j == From) width_range[From] = MAX(width_range[From], it->m_Indentation); else width_range[To] = MIN(width_range[To], Width - it->m_Indentation); } } } } // Reset X for the next loop x = width_range[From]; // Now we'll do another loop to figure out the height of // the line (the height of the largest character). This // couldn't be determined in the first loop (main loop) // because it didn't regard images, so we don't know // if all characters processed, will actually be involved // in that line. float line_height=0.f; for (int j=temp_from; j<=i; ++j) { // We don't want to use Feedback now, so we'll have to use // another. CGUIString::SFeedback Feedback2; // Don't attach object, it'll suppress the errors // we want them to be reported in the final GenerateTextCall() // so that we don't get duplicates. string.GenerateTextCall(Feedback2, Font, string.m_Words[j], string.m_Words[j+1], FirstLine); // Append X value. x += Feedback2.m_Size.cx; if (WordWrapping && x > width_range[To] && j!=temp_from && !Feedback2.m_NewLine) break; // Let line_height be the maximum m_Height we encounter. line_height = MAX(line_height, Feedback2.m_Size.cy); if (WordWrapping && Feedback2.m_NewLine) break; } // Reset x once more x = width_range[From]; // Move down, because font drawing starts from the baseline y += line_height; // Do the real processing now for (int j=temp_from; j<=i; ++j) { // We don't want to use Feedback now, so we'll have to use // another one. CGUIString::SFeedback Feedback2; // Defaults string.GenerateTextCall(Feedback2, Font, string.m_Words[j], string.m_Words[j+1], FirstLine, pObject); // Iterate all and set X/Y values // Since X values are not set, we need to make an internal // iteration with an increment that will append the internal // x, that is what x_pointer is for. float x_pointer=0.f; vector::iterator it; for (it = Feedback2.m_TextCalls.begin(); it != Feedback2.m_TextCalls.end(); ++it) { it->m_Pos = CPos(x + x_pointer, y); x_pointer += it->m_Size.cx; if (it->m_pSpriteCall) { it->m_pSpriteCall->m_Area += it->m_Pos - CSize(0,it->m_pSpriteCall->m_Area.GetHeight()); } } // Append X value. x += Feedback2.m_Size.cx; Text.m_Size.cx = MAX(Text.m_Size.cx, x+BufferZone); // The first word overrides the width limit, what we // do, in those cases, are just drawing that word even // though it'll extend the object. if (WordWrapping) // only if word-wrapping is applicable { if (Feedback2.m_NewLine) { from = j+1; // Sprite call can exist within only a newline segment, // therefore we need this. Text.m_SpriteCalls.insert(Text.m_SpriteCalls.end(), Feedback2.m_SpriteCalls.begin(), Feedback2.m_SpriteCalls.end()); break; } else if (x > width_range[To] && j==temp_from) { from = j+1; // do not break, since we want it to be added to m_TextCalls } else if (x > width_range[To]) { from = j; break; } } // Add the whole Feedback2.m_TextCalls to our m_TextCalls. Text.m_TextCalls.insert(Text.m_TextCalls.end(), Feedback2.m_TextCalls.begin(), Feedback2.m_TextCalls.end()); Text.m_SpriteCalls.insert(Text.m_SpriteCalls.end(), Feedback2.m_SpriteCalls.begin(), Feedback2.m_SpriteCalls.end()); if (j == (int)string.m_Words.size()-2) done = true; } // Reset X x = 0.f; // Update height of all Text.m_Size.cy = MAX(Text.m_Size.cy, y+BufferZone); FirstLine = false; // Now if we entered as from = i, then we want // i being one minus that, so that it will become // the same i in the next loop. The difference is that // we're on a new line now. i = from-1; } } return Text; } void CGUI::DrawText(SGUIText &Text, const CColor &DefaultColor, const CPos &pos, const float &z) { // TODO Gee: All these really necessary? Some // are defaults and if you changed them // the opposite value at the end of the functions, // some things won't be drawn correctly. glEnable(GL_TEXTURE_2D); glDisable(GL_CULL_FACE); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); glEnable(GL_BLEND); glDisable(GL_ALPHA_TEST); glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); CFont* font = NULL; CStr LastFontName; for (vector::const_iterator it = Text.m_TextCalls.begin(); it != Text.m_TextCalls.end(); ++it) { // If this is just a placeholder for a sprite call, continue if (it->m_pSpriteCall) continue; // Switch fonts when necessary, but remember the last one used if (it->m_Font != LastFontName) { delete font; font = new CFont(it->m_Font); font->Bind(); LastFontName = it->m_Font; } CColor color = it->m_UseCustomColor ? it->m_Color : DefaultColor; glPushMatrix(); // TODO Gee: (2004-09-04) Why are font corrupted if inputted float value? glTranslatef((GLfloat)int(pos.x+it->m_Pos.x), (GLfloat)int(pos.y+it->m_Pos.y), z); glColor4f(color.r, color.g, color.b, color.a); glwprintf(L"%ls", it->m_String.c_str()); // "%ls" is necessary in case m_String contains % symbols glPopMatrix(); } delete font; for (list::iterator it=Text.m_SpriteCalls.begin(); it!=Text.m_SpriteCalls.end(); ++it) { DrawSprite(it->m_Sprite, it->m_CellID, z, it->m_Area + pos); } // TODO To whom it may concern: Thing were not reset, so // I added this line, modify if incorrect -- glDisable(GL_TEXTURE_2D); // -- GL } void CGUI::ReportParseError(const char *str, ...) { va_list argp; char buffer[512]; memset(buffer,0,sizeof(buffer)); va_start(argp, str); vsnprintf2(buffer, sizeof(buffer), str, argp); va_end(argp); // Print header if (m_Errors==0) { LOG(ERROR, LOG_CATEGORY, "*** GUI Tree Creation Errors:"); } // Important, set ParseError to true ++m_Errors; LOG(ERROR, LOG_CATEGORY, buffer); } /** * @callgraph */ void CGUI::LoadXMLFile(const string &Filename) { // Reset parse error // we can later check if this has increased m_Errors = 0; CXeromyces XeroFile; if (XeroFile.Load(Filename.c_str()) != PSRETURN_OK) // Fail silently return; XMBElement node = XeroFile.getRoot(); // Check root element's (node) name so we know what kind of // data we'll be expecting CStr root_name (XeroFile.getElementString(node.getNodeName())); try { if (root_name == "objects") { Xeromyces_ReadRootObjects(node, &XeroFile); // Re-cache all values so these gets cached too. //UpdateResolution(); } else if (root_name == "sprites") { Xeromyces_ReadRootSprites(node, &XeroFile); } else if (root_name == "styles") { Xeromyces_ReadRootStyles(node, &XeroFile); } else if (root_name == "setup") { Xeromyces_ReadRootSetup(node, &XeroFile); } else { debug_warn("CGUI::LoadXMLFile error"); // TODO Gee: Output in log } } catch (PSERROR_GUI) { LOG(ERROR, LOG_CATEGORY, "Errors loading GUI file %s", Filename.c_str()); return; } // Now report if any other errors occured if (m_Errors > 0) { /// g_console.submit("echo GUI Tree Creation Reports %d errors", m_Errors); } } //=================================================================== // XML Reading Xeromyces Specific Sub-Routines //=================================================================== void CGUI::Xeromyces_ReadRootObjects(XMBElement Element, CXeromyces* pFile) { int el_script = pFile->getElementID("script"); // Iterate main children // they should all be or