/* CGUI */ #include "precompiled.h" #include <string> #include <stdarg.h> #include "lib/res/graphics/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 "CList.h" #include "CDropDown.h" #include "CProgressBar.h" #include "CTooltip.h" #include "MiniMap.h" #include "ps/XML/Xeromyces.h" #include "ps/Font.h" #include "ps/Pyrogenesis.h" #include "lib/input.h" #include "lib/bits.h" // TODO Gee: Whatever include CRect/CPos/CSize #include "ps/Overlay.h" #include "ps/Profile.h" #include "scripting/ScriptingHost.h" #include "ps/Hotkey.h" #include "ps/Globals.h" #include "lib/timer.h" #include "lib/sysdep/sysdep.h" // namespaces used using namespace std; const double SELECT_DBLCLICK_RATE = 0.5; #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, }; //------------------------------------------------------------------- // 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 //------------------------------------------------------------------- InReaction gui_handler(const SDL_Event_* ev) { PROFILE( "GUI event handler" ); return g_GUI.HandleEvent(ev); } InReaction CGUI::HandleEvent(const SDL_Event_* ev) { InReaction ret = IN_PASS; if (ev->ev.type == SDL_GUIHOTKEYPRESS) { const CStr& objectName = *(CStr*) ev->ev.user.data1; 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->ev.type == SDL_MOUSEMOTION) { // Yes the mouse position is stored as float to avoid // constant conversions when operating in a // float-based environment. m_MousePos = CPos((float)ev->ev.motion.x, (float)ev->ev.motion.y); GUI<SGUIMessage>::RecurseObject(GUIRR_HIDDEN | GUIRR_GHOST, m_BaseObject, &IGUIObject::HandleMessage, SGUIMessage(GUIM_MOUSE_MOTION)); } // Update m_MouseButtons. (BUTTONUP is handled later.) else if (ev->ev.type == SDL_MOUSEBUTTONDOWN) { switch (ev->ev.button.button) { case SDL_BUTTON_LEFT: case SDL_BUTTON_RIGHT: case SDL_BUTTON_MIDDLE: m_MouseButtons |= BIT(ev->ev.button.button); break; default: break; } } // 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 { PROFILE( "mouse events" ); // TODO Gee: Optimizations needed! // these two recursive function are quite overhead heavy. // pNearest will after this point at the hovered object, possibly NULL GUI<IGUIObject*>::RecurseObject(GUIRR_HIDDEN | GUIRR_GHOST, m_BaseObject, &IGUIObject::ChooseMouseOverAndClosest, pNearest); // Is placed in the UpdateMouseOver function //if (ev->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<IGUIObject*>::RecurseObject(GUIRR_HIDDEN | GUIRR_GHOST, m_BaseObject, &IGUIObject::UpdateMouseOver, pNearest); if (ev->ev.type == SDL_MOUSEBUTTONDOWN) { switch (ev->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 ret = IN_HANDLED; } else if (m_FocusedObject) { m_FocusedObject->HandleMessage(SGUIMessage(GUIM_LOST_FOCUS)); //if (m_FocusedObject-> TODO SelfishFocus? m_FocusedObject = 0; } break; case SDL_BUTTON_WHEELDOWN: // wheel down if (pNearest) { pNearest->HandleMessage(SGUIMessage(GUIM_MOUSE_WHEEL_DOWN)); pNearest->ScriptEvent("mousewheeldown"); ret = IN_HANDLED; } break; case SDL_BUTTON_WHEELUP: // wheel up if (pNearest) { pNearest->HandleMessage(SGUIMessage(GUIM_MOUSE_WHEEL_UP)); pNearest->ScriptEvent("mousewheelup"); ret = IN_HANDLED; } break; default: break; } } else if (ev->ev.type == SDL_MOUSEBUTTONUP) { switch (ev->ev.button.button) { case SDL_BUTTON_LEFT: if (pNearest) { double timeElapsed = get_time() -pNearest->m_LastClickTime[SDL_BUTTON_LEFT]; pNearest->m_LastClickTime[SDL_BUTTON_LEFT] = get_time(); //Double click? if (timeElapsed < SELECT_DBLCLICK_RATE) { pNearest->HandleMessage(SGUIMessage(GUIM_MOUSE_DBLCLICK_LEFT)); pNearest->ScriptEvent("mouseleftdoubleclick"); } else { pNearest->HandleMessage(SGUIMessage(GUIM_MOUSE_RELEASE_LEFT)); pNearest->ScriptEvent("mouseleftrelease"); } ret = IN_HANDLED; } break; case SDL_BUTTON_RIGHT: if (pNearest) { double timeElapsed = get_time() -pNearest->m_LastClickTime[SDL_BUTTON_RIGHT]; pNearest->m_LastClickTime[SDL_BUTTON_RIGHT] = get_time(); //Double click? if (timeElapsed < SELECT_DBLCLICK_RATE) { pNearest->HandleMessage(SGUIMessage(GUIM_MOUSE_DBLCLICK_RIGHT)); //pNearest->ScriptEvent("mouserightdoubleclick"); } else { pNearest->HandleMessage(SGUIMessage(GUIM_MOUSE_RELEASE_RIGHT)); //pNearest->ScriptEvent("mouserightrelease"); } ret = IN_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) { UNUSED2(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->ev.type == SDL_MOUSEBUTTONUP) { switch (ev->ev.button.button) { case SDL_BUTTON_LEFT: case SDL_BUTTON_RIGHT: case SDL_BUTTON_MIDDLE: m_MouseButtons &= ~BIT(ev->ev.button.button); break; default: break; } } // Handle keys for input boxes if (GetFocusedObject()) { if ( (ev->ev.type == SDL_KEYDOWN && ev->ev.key.keysym.sym != SDLK_ESCAPE && !g_keys[SDLK_LCTRL] && !g_keys[SDLK_RCTRL] && !g_keys[SDLK_LALT] && !g_keys[SDLK_RALT]) || ev->ev.type == SDL_HOTKEYDOWN ) { ret = GetFocusedObject()->ManuallyHandleEvent(ev); } // else will return IN_PASS because we never used the button. } return ret; } void CGUI::TickObjects() { CStr action = "tick"; GUI<CStr>::RecurseObject(0, m_BaseObject, &IGUIObject::ScriptEvent, action); // Also update tooltips: // TODO: Efficiency IGUIObject* pNearest = NULL; GUI<IGUIObject*>::RecurseObject(GUIRR_HIDDEN | GUIRR_GHOST, m_BaseObject, &IGUIObject::ChooseMouseOverAndClosest, pNearest); m_Tooltip.Update(pNearest, m_MousePos, this); } void CGUI::SendEventToAll(const CStr& EventName) { // janwas 2006-03-03: spoke with Ykkrosh about EventName case. // when registering, case is converted to lower - this avoids surprise // if someone were to get the case wrong and then not notice their // handler is never called. however, until now, the other end // (sending events here) wasn't converting to lower case, // leading to a similar problem. // now fixed; case is irrelevant since all are converted to lower. GUI<CStr>::RecurseObject(0, m_BaseObject, &IGUIObject::ScriptEvent, EventName.LowerCase()); } //------------------------------------------------------------------- // Constructor / Destructor //------------------------------------------------------------------- CGUI::CGUI() : m_MouseButtons(0), m_FocusedObject(NULL), m_InternalNameNumber(0) { 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); debug_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); // The following line was commented out, I don't know if that's me or not, or // for what reason, but I'm gonna uncomment, if anything breaks, just let me // know, or if it wasn't I that commented it out, do let me know why. // -- Gee 20-07-2005 AddObjectType("list", &CList::ConstructObject); // AddObjectType("dropdown", &CDropDown::ConstructObject); } void CGUI::Process() { /* // TODO Gee: check if m_pInput is valid, otherwise return /// debug_assert(m_pInput); // Pre-process all objects try { GUI<EGUIMessage>::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<IGUIObject*>::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<IGUIObject*>::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<EGUIMessage>::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(); guiLoadIdentity(); 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) { UNUSED2(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& UNUSED(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) { UNUSED2(e); debug_warn("CGUI::Destroy error"); // TODO Gee: Handle } delete it->second; } for (std::map<CStr, CGUISprite>::iterator it2 = m_Sprites.begin(); it2 != m_Sprites.end(); ++it2) for (std::vector<SGUIImage>::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<CGUI*>::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<SGUIMessage>::RecurseObject(0, pObject, &IGUIObject::HandleMessage, SGUIMessage(GUIM_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.swap(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<SGenerateTextImage> 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<CStr>::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 = std::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_SpriteName, BufferZone, icon.m_CellID); // Check if image is the lowest thing. Text.m_Size.cy = std::max(Text.m_Size.cy, Image.m_YTo); Images[j].push_back(Image); Text.m_SpriteCalls.push_back(SpriteCall); } } } pos_last_img = std::max(pos_last_img, i); x += Feedback.m_Size.cx; prelim_line_height = std::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<SGenerateTextImage>::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 = std::max(y, it->m_YFrom); union_to = std::min(y+prelim_line_height, it->m_YTo); // The union is not empty if (union_to > union_from) { if (j == From) width_range[From] = std::max(width_range[From], it->m_Indentation); else width_range[To] = std::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 = std::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<SGUIText::STextCall>::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 = std::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 = std::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, const CRect &clipping) { // 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); if (clipping != CRect()) { double eq[4][4] = { { 0.0, 1.0, 0.0, -clipping.top }, { 1.0, 0.0, 0.0, -clipping.left }, { 0.0, -1.0, 0.0, clipping.bottom }, { -1.0, 0.0, 0.0, clipping.right } }; for (int i=0; i<4; ++i) { glClipPlane(GL_CLIP_PLANE0+i, eq[i]); glEnable(GL_CLIP_PLANE0+i); } } CFont* font = NULL; CStr LastFontName; for (vector<SGUIText::STextCall>::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); glColor4fv(color.FloatArray()); glwprintf(L"%ls", it->m_String.c_str()); // "%ls" is necessary in case m_String contains % symbols glPopMatrix(); } if (font) delete font; for (list<SGUIText::SSpriteCall>::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 -- if (clipping != CRect()) { for (int i=0; i<4; ++i) glDisable(GL_CLIP_PLANE0+i); } glDisable(GL_TEXTURE_2D); // -- GL } bool CGUI::GetPreDefinedColor(const CStr& name, CColor &Output) { if (m_PreDefinedColors.count(name) == 0) { return false; } else { Output = m_PreDefinedColors[name]; return true; } } void CGUI::ReportParseError(const char *str, ...) { va_list argp; char buffer[512]; memset(buffer,0,sizeof(buffer)); va_start(argp, str); sys_vsnprintf(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& e) { LOG(ERROR, LOG_CATEGORY, "Errors loading GUI file %s (%s)", Filename.c_str(), e.getCode()); 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 <object> or <script> elements XMBElementList children = Element.GetChildNodes(); for (int i=0; i<children.Count; ++i) { //debug_printf("Object %d\n", i); XMBElement child = children.Item(i); if (child.GetNodeName() == el_script) // Execute the inline script Xeromyces_ReadScript(child, pFile); else // Read in this whole object into the GUI Xeromyces_ReadObject(child, pFile, m_BaseObject); } } void CGUI::Xeromyces_ReadRootSprites(XMBElement Element, CXeromyces* pFile) { // Iterate main children // they should all be <sprite> elements XMBElementList children = Element.GetChildNodes(); for (int i=0; i<children.Count; ++i) { XMBElement child = children.Item(i); // Read in this whole object into the GUI Xeromyces_ReadSprite(child, pFile); } } void CGUI::Xeromyces_ReadRootStyles(XMBElement Element, CXeromyces* pFile) { // Iterate main children // they should all be <styles> elements XMBElementList children = Element.GetChildNodes(); for (int i=0; i<children.Count; ++i) { XMBElement child = children.Item(i); // Read in this whole object into the GUI Xeromyces_ReadStyle(child, pFile); } } void CGUI::Xeromyces_ReadRootSetup(XMBElement Element, CXeromyces* pFile) { // Iterate main children // they should all be <icon>, <scrollbar> or <tooltip>. XMBElementList children = Element.GetChildNodes(); for (int i=0; i<children.Count; ++i) { XMBElement child = children.Item(i); // Read in this whole object into the GUI CStr name (pFile->GetElementString(child.GetNodeName())); if (name == "scrollbar") { Xeromyces_ReadScrollBarStyle(child, pFile); } else if (name == "icon") { Xeromyces_ReadIcon(child, pFile); } else if (name == "tooltip") { Xeromyces_ReadTooltip(child, pFile); } else if (name == "color") { Xeromyces_ReadColor(child, pFile); } else { debug_warn("Invalid data - DTD shouldn't allow this"); } } } void CGUI::Xeromyces_ReadObject(XMBElement Element, CXeromyces* pFile, IGUIObject *pParent) { debug_assert(pParent); int i; // Our object we are going to create IGUIObject *object = NULL; XMBAttributeList attributes = Element.GetAttributes(); // Well first of all we need to determine the type CStr type (attributes.GetNamedItem(pFile->GetAttributeID("type"))); // Construct object from specified type // henceforth, we need to do a rollback before aborting. // i.e. releasing this object object = ConstructObject(type); if (!object) { // Report error that object was unsuccessfully loaded ReportParseError("Unrecognized type \"%s\"", type.c_str()); return; } // Cache some IDs for element attribute names, to avoid string comparisons #define ELMT(x) int elmt_##x = pFile->GetElementID(#x) #define ATTR(x) int attr_##x = pFile->GetAttributeID(#x) ELMT(object); ELMT(action); ATTR(style); ATTR(type); ATTR(name); ATTR(hotkey); ATTR(z); ATTR(on); ATTR(file); // // Read Style and set defaults // // If the setting "style" is set, try loading that setting. // // Always load default (if it's available) first! // CStr argStyle (attributes.GetNamedItem(attr_style)); if (m_Styles.count("default") == 1) object->LoadStyle(*this, "default"); if (! argStyle.empty()) { // additional check if (m_Styles.count(argStyle) == 0) { ReportParseError("Trying to use style '%s' that doesn't exist.", argStyle.c_str()); } else object->LoadStyle(*this, argStyle); } // // Read Attributes // bool NameSet = false; bool ManuallySetZ = false; // if z has been manually set, this turn true CStr hotkeyTag; // Now we can iterate all attributes and store for (i=0; i<attributes.Count; ++i) { XMBAttribute attr = attributes.Item(i); // If value is "null", then it is equivalent as never being entered if ((CStr)attr.Value == "null") continue; // Ignore "type" and "style", we've already checked it if (attr.Name == attr_type || attr.Name == attr_style) continue; // Also the name needs some special attention if (attr.Name == attr_name) { object->SetName((CStr)attr.Value); NameSet = true; continue; } // Wire up the hotkey tag, if it has one if (attr.Name == attr_hotkey) hotkeyTag = attr.Value; if (attr.Name == attr_z) ManuallySetZ = true; // Try setting the value if (object->SetSetting(pFile->GetAttributeString(attr.Name), (CStr)attr.Value, true) != PS_OK) { ReportParseError("(object: %s) Can't set \"%s\" to \"%s\"", object->GetPresentableName().c_str(), pFile->GetAttributeString(attr.Name).c_str(), CStr(attr.Value).c_str()); // This is not a fatal error } } // Check if name isn't set, generate an internal name in that case. if (!NameSet) { object->SetName(CStr("__internal(") + CStr(m_InternalNameNumber) + CStr(")")); ++m_InternalNameNumber; } // Attempt to register the hotkey tag, if one was provided if (! hotkeyTag.empty()) HotkeyRegisterGuiObject(object->GetName(), hotkeyTag); CStrW caption (Element.GetText()); if (! caption.empty()) { // Set the setting caption to this object->SetSetting("caption", caption, true); // There is no harm if the object didn't have a "caption" } // // Read Children // // Iterate children XMBElementList children = Element.GetChildNodes(); for (i=0; i<children.Count; ++i) { // Get node XMBElement child = children.Item(i); // Check what name the elements got int element_name = child.GetNodeName(); if (element_name == elmt_object) { // Call this function on the child Xeromyces_ReadObject(child, pFile, object); } else if (element_name == elmt_action) { // Scripted <action> element // Check for a 'file' parameter CStr file (child.GetAttributes().GetNamedItem(attr_file)); CStr code; // If there is a file, open it and use it as the code if (! file.empty()) { CVFSFile scriptfile; if (scriptfile.Load(file) != PSRETURN_OK) { LOG(ERROR, LOG_CATEGORY, "Error opening action file '%s'", file.c_str()); throw PSERROR_GUI_JSOpenFailed(); } code = scriptfile.GetAsString(); } // Read the inline code (concatenating to the file code, if both are specified) code += (CStr)child.GetText(); CStr action = (CStr)child.GetAttributes().GetNamedItem(attr_on); object->RegisterScriptHandler(action.LowerCase(), code, this); } else { // Try making the object read the tag. if (!object->HandleAdditionalChildren(child, pFile)) { LOG(ERROR, LOG_CATEGORY, "(object: %s) Reading unknown children for its type"); } } } // // Check if Z wasn't manually set // if (!ManuallySetZ) { // Set it automatically to 10 plus its parents if (pParent==NULL) { debug_warn("CGUI::Xeromyces_ReadObject error"); // TODO Gee: Report error } else { bool absolute; GUI<bool>::GetSetting(object, "absolute", absolute); // If the object is absolute, we'll have to get the parent's Z buffered, // and add to that! if (absolute) { GUI<float>::SetSetting(object, "z", pParent->GetBufferedZ() + 10.f, true); } else // If the object is relative, then we'll just store Z as "10" { GUI<float>::SetSetting(object, "z", 10.f, true); } } } // // Input Child // try { if (pParent == m_BaseObject) AddObject(object); else pParent->AddChild(object); } catch (PS_RESULT e) { ReportParseError(e); } } void CGUI::Xeromyces_ReadScript(XMBElement Element, CXeromyces* pFile) { // Check for a 'file' parameter CStr file (Element.GetAttributes().GetNamedItem( pFile->GetAttributeID("file") )); // If there is a file specified, open and execute it if (! file.empty()) g_ScriptingHost.RunScript(file, m_ScriptObject); // Execute inline scripts CStr code (Element.GetText()); if (! code.empty()) g_ScriptingHost.RunMemScript(code.c_str(), code.length(), "Some XML file", Element.GetLineNumber(), m_ScriptObject); } void CGUI::Xeromyces_ReadSprite(XMBElement Element, CXeromyces* pFile) { // Sprite object we're adding CGUISprite sprite; // and what will be its reference name CStr name; // // Read Attributes // // Get name, we know it exists because of DTD requirements name = Element.GetAttributes().GetNamedItem( pFile->GetAttributeID("name") ); if (m_Sprites.find(name) != m_Sprites.end()) LOG(WARNING, LOG_CATEGORY, "Sprite name '%s' used more than once; first definition will be discarded", (const char*)name); // // Read Children (the images) // SGUIImageEffects* effects = NULL; // Iterate children XMBElementList children = Element.GetChildNodes(); for (int i=0; i<children.Count; ++i) { // Get node XMBElement child = children.Item(i); CStr ElementName (pFile->GetElementString(child.GetNodeName())); if (ElementName == "image") { Xeromyces_ReadImage(child, pFile, sprite); } else if (ElementName == "effect") { debug_assert(! effects); // DTD should only allow one effect per sprite effects = new SGUIImageEffects; Xeromyces_ReadEffects(child, pFile, *effects); } else { debug_warn("Invalid data - DTD shouldn't allow this"); } } // Apply the effects to every image (unless the image overrides it with // different effects) if (effects) for (std::vector<SGUIImage>::iterator it = sprite.m_Images.begin(); it != sprite.m_Images.end(); ++it) if (! it->m_Effects) it->m_Effects = new SGUIImageEffects(*effects); // do a copy just so it can be deleted correctly later delete effects; // // Add Sprite // m_Sprites[name] = sprite; } void CGUI::Xeromyces_ReadImage(XMBElement Element, CXeromyces* pFile, CGUISprite &parent) { // Image object we're adding SGUIImage image; // Set defaults (or maybe do that in DTD?) image.m_TextureSize = CClientArea("0 0 100% 100%"); image.m_Size = CClientArea("0 0 100% 100%"); // TODO Gee: Setup defaults here (or maybe they are in the SGUIImage ctor) // // Read Attributes // // Now we can iterate all attributes and store XMBAttributeList attributes = Element.GetAttributes(); for (int i=0; i<attributes.Count; ++i) { XMBAttribute attr = attributes.Item(i); CStr attr_name (pFile->GetAttributeString(attr.Name)); CStr attr_value (attr.Value); if (attr_name == "texture") { CStr TexFilename ("art/textures/ui/"); TexFilename += attr_value; image.m_TextureName = TexFilename; } else if (attr_name == "size") { CClientArea ca; if (!GUI<CClientArea>::ParseString(attr_value, ca)) ReportParseError("Error parsing '%s' (\"%s\")", attr_name.c_str(), attr_value.c_str()); else image.m_Size = ca; } else if (attr_name == "texture_size") { CClientArea ca; if (!GUI<CClientArea>::ParseString(attr_value, ca)) ReportParseError("Error parsing '%s' (\"%s\")", attr_name.c_str(), attr_value.c_str()); else image.m_TextureSize = ca; } else if (attr_name == "real_texture_placement") { CRect rect; if (!GUI<CRect>::ParseString(attr_value, rect)) ReportParseError("Error parsing '%s' (\"%s\")", attr_name.c_str(), attr_value.c_str()); else image.m_TexturePlacementInFile = rect; } else if (attr_name == "cell_size") { CSize size; if (!GUI<CSize>::ParseString(attr_value, size)) ReportParseError("Error parsing '%s' (\"%s\")", attr_name.c_str(), attr_value.c_str()); else image.m_CellSize = size; } else if (attr_name == "z_level") { float z_level; if (!GUI<float>::ParseString(attr_value, z_level)) ReportParseError("Error parsing '%s' (\"%s\")", attr_name.c_str(), attr_value.c_str()); else image.m_DeltaZ = z_level/100.f; } else if (attr_name == "backcolor") { CColor color; if (!GUI<CColor>::ParseString(attr_value, color)) ReportParseError("Error parsing '%s' (\"%s\")", attr_name.c_str(), attr_value.c_str()); else image.m_BackColor = color; } else if (attr_name == "bordercolor") { CColor color; if (!GUI<CColor>::ParseString(attr_value, color)) ReportParseError("Error parsing '%s' (\"%s\")", attr_name.c_str(), attr_value.c_str()); else image.m_BorderColor = color; } else if (attr_name == "border") { bool b; if (!GUI<bool>::ParseString(attr_value, b)) ReportParseError("Error parsing '%s' (\"%s\")", attr_name.c_str(), attr_value.c_str()); else image.m_Border = b; } else { debug_warn("Invalid data - DTD shouldn't allow this"); } } // Look for effects XMBElementList children = Element.GetChildNodes(); for (int i=0; i<children.Count; ++i) { XMBElement child = children.Item(i); CStr ElementName (pFile->GetElementString(child.GetNodeName())); if (ElementName == "effect") { debug_assert(! image.m_Effects); // DTD should only allow one effect per sprite image.m_Effects = new SGUIImageEffects; Xeromyces_ReadEffects(child, pFile, *image.m_Effects); } else { debug_warn("Invalid data - DTD shouldn't allow this"); } } // // Input // parent.AddImage(image); } void CGUI::Xeromyces_ReadEffects(XMBElement Element, CXeromyces* pFile, SGUIImageEffects &effects) { XMBAttributeList attributes = Element.GetAttributes(); for (int i=0; i<attributes.Count; ++i) { XMBAttribute attr = attributes.Item(i); CStr attr_name (pFile->GetAttributeString(attr.Name)); CStr attr_value (attr.Value); #define COLOR(xml, mem, alpha) \ if (attr_name == xml) \ { \ CColor color; \ if (!GUI<int>::ParseColor(attr_value, color, alpha)) \ ReportParseError("Error parsing '%s' (\"%s\")", attr_name.c_str(), attr_value.c_str()); \ else effects.m_##mem = color; \ } \ else #define BOOL(xml, mem) \ if (attr_name == xml) \ { \ effects.m_##mem = true; \ } \ else COLOR("add_color", AddColor, 0.f) COLOR("multiply_color", MultiplyColor, 255.f) BOOL("grayscale", Greyscale) { debug_warn("Invalid data - DTD shouldn't allow this"); } } } void CGUI::Xeromyces_ReadStyle(XMBElement Element, CXeromyces* pFile) { // style object we're adding SGUIStyle style; CStr name; // // Read Attributes // // Now we can iterate all attributes and store XMBAttributeList attributes = Element.GetAttributes(); for (int i=0; i<attributes.Count; ++i) { XMBAttribute attr = attributes.Item(i); CStr attr_name (pFile->GetAttributeString(attr.Name)); CStr attr_value (attr.Value); // The "name" setting is actually the name of the style // and not a new default if (attr_name == "name") name = attr_value; else style.m_SettingsDefaults[attr_name] = attr_value; } // // Add to CGUI // m_Styles[name] = style; } void CGUI::Xeromyces_ReadScrollBarStyle(XMBElement Element, CXeromyces* pFile) { // style object we're adding SGUIScrollBarStyle scrollbar; CStr name; // // Read Attributes // // Now we can iterate all attributes and store XMBAttributeList attributes = Element.GetAttributes(); for (int i=0; i<attributes.Count; ++i) { XMBAttribute attr = attributes.Item(i); CStr attr_name = pFile->GetAttributeString(attr.Name); CStr attr_value (attr.Value); if (attr_value == "null") continue; if (attr_name == "name") name = attr_value; else if (attr_name == "width") { float f; if (!GUI<float>::ParseString(attr_value, f)) { ReportParseError("Error parsing '%s' (\"%s\")", attr_name.c_str(), attr_value.c_str()); } scrollbar.m_Width = f; } else if (attr_name == "minimum_bar_size") { float f; if (!GUI<float>::ParseString(attr_value, f)) { ReportParseError("Error parsing '%s' (\"%s\")", attr_name.c_str(), attr_value.c_str()); } scrollbar.m_MinimumBarSize = f; } else if (attr_name == "sprite_button_top") scrollbar.m_SpriteButtonTop = attr_value; else if (attr_name == "sprite_button_top_pressed") scrollbar.m_SpriteButtonTopPressed = attr_value; else if (attr_name == "sprite_button_top_disabled") scrollbar.m_SpriteButtonTopDisabled = attr_value; else if (attr_name == "sprite_button_top_over") scrollbar.m_SpriteButtonTopOver = attr_value; else if (attr_name == "sprite_button_bottom") scrollbar.m_SpriteButtonBottom = attr_value; else if (attr_name == "sprite_button_bottom_pressed") scrollbar.m_SpriteButtonBottomPressed = attr_value; else if (attr_name == "sprite_button_bottom_disabled") scrollbar.m_SpriteButtonBottomDisabled = attr_value; else if (attr_name == "sprite_button_bottom_over") scrollbar.m_SpriteButtonBottomOver = attr_value; else if (attr_name == "sprite_back_vertical") scrollbar.m_SpriteBackVertical = attr_value; else if (attr_name == "sprite_bar_vertical") scrollbar.m_SpriteBarVertical = attr_value; else if (attr_name == "sprite_bar_vertical_over") scrollbar.m_SpriteBarVerticalOver = attr_value; else if (attr_name == "sprite_bar_vertical_pressed") scrollbar.m_SpriteBarVerticalPressed = attr_value; } // // Add to CGUI // m_ScrollBarStyles[name] = scrollbar; } void CGUI::Xeromyces_ReadIcon(XMBElement Element, CXeromyces* pFile) { // Icon we're adding SGUIIcon icon; CStr name; XMBAttributeList attributes = Element.GetAttributes(); for (int i=0; i<attributes.Count; ++i) { XMBAttribute attr = attributes.Item(i); CStr attr_name (pFile->GetAttributeString(attr.Name)); CStr attr_value (attr.Value); if (attr_value == "null") continue; if (attr_name == "name") name = attr_value; else if (attr_name == "sprite") icon.m_SpriteName = attr_value; else if (attr_name == "size") { CSize size; if (!GUI<CSize>::ParseString(attr_value, size)) ReportParseError("Error parsing '%s' (\"%s\") inside <icon>.", attr_name.c_str(), attr_value.c_str()); icon.m_Size = size; } else if (attr_name == "cell_id") { int cell_id; if (!GUI<int>::ParseString(attr_value, cell_id)) ReportParseError("Error parsing '%s' (\"%s\") inside <icon>.", attr_name.c_str(), attr_value.c_str()); icon.m_CellID = cell_id; } else { debug_warn("Invalid data - DTD shouldn't allow this"); } } m_Icons[name] = icon; } void CGUI::Xeromyces_ReadTooltip(XMBElement Element, CXeromyces* pFile) { // Read the tooltip, and store it as a specially-named object IGUIObject* object = new CTooltip; XMBAttributeList attributes = Element.GetAttributes(); for (int i=0; i<attributes.Count; ++i) { XMBAttribute attr = attributes.Item(i); CStr attr_name (pFile->GetAttributeString(attr.Name)); CStr attr_value (attr.Value); if (attr_name == "name") { object->SetName("__tooltip_" + attr_value); } else { object->SetSetting(attr_name, attr_value); } } AddObject(object); } // Reads Custom Color void CGUI::Xeromyces_ReadColor(XMBElement Element, CXeromyces* pFile) { // Read the color and stor in m_PreDefinedColors XMBAttributeList attributes = Element.GetAttributes(); //IGUIObject* object = new CTooltip; CColor color; CStr name = attributes.GetNamedItem(pFile->GetAttributeID("name")); // Try parsing value CStr value (Element.GetText()); if (! value.empty()) { // Try setting color to value if (!color.ParseString(value, 255.f)) { ReportParseError("Unable to create custom color '%s'. Invalid color syntax.", name.c_str()); } else { // input color m_PreDefinedColors[name] = color; } } }