Mercurial > projects > mde
view mde/gui/WidgetManager.d @ 129:ad91de8867a0
Added a widget size printing debug function. Fixed a bug in SwitchWidget.
author | Diggory Hardy <diggory.hardy@gmail.com> |
---|---|
date | Thu, 15 Jan 2009 16:52:46 +0000 |
parents | 41582439a42b |
children | 9cff74f68b84 |
line wrap: on
line source
/* LICENSE BLOCK Part of mde: a Modular D game-oriented Engine Copyright © 2007-2008 Diggory Hardy This program 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. This program 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 this program. If not, see <http://www.gnu.org/licenses/>. */ /************************************************************************************************* * The gui manager class base. * * This contains most of the code required by a window manager, but does not interact with a screen * or get user input. Rendering is handled separately by the renderer anyway. *************************************************************************************************/ module mde.gui.WidgetManager; import mde.gui.WidgetDataSet; import mde.gui.widget.Ifaces; import mde.gui.exception; import imde = mde.imde; import mde.lookup.Options; // miscOpts.L10n callback import mde.content.Content; import Items = mde.content.Items; // loadTranslation debug import mde.content.miscContent; // Debug menu import mde.file.mergetag.Reader; import mde.file.mergetag.Writer; import mde.setup.paths; // Widgets to create: import mde.gui.widget.layout; import mde.gui.widget.miscWidgets; import mde.gui.widget.TextWidget; import mde.gui.widget.miscContent; import mde.gui.widget.Floating; import mde.gui.widget.PopupMenu; import tango.core.sync.Mutex; import tango.util.log.Log : Log, Logger; import tango.util.container.SortedMap; private Logger logger; static this () { logger = Log.getLogger ("mde.gui.WidgetManager"); } /************************************************************************************************* * Contains the code for loading and saving an entire gui (more than one may exist), but not the * code for drawing it or handling user input. * * This abstract class exists solely for separating out some of the functionality. *************************************************************************************************/ abstract scope class AWidgetManager : IWidgetManager { /** Construct a new widget loader. * * params: * fileName = Name of file specifying the gui, excluding path and extension. */ protected this (char[] file) { mutex = new Mutex; // Used on functions intended to be called from outside the gui package. fileName = file; miscOpts.L10n.addCallback (&reloadStrings); clickCallbacks = new typeof(clickCallbacks); motionCallbacks = new typeof(motionCallbacks); debug { auto lWS = new EventContent ("logWidgetSize"); lWS.addCallback (&logWidgetSize); imde.menu.append (lWS); } } /* Load the widgets' data from the file specified to the CTOR. * * params: * allDesigns = Load all sections */ private void loadData (bool allDesigns = false) { if (allLoaded || (defaultDesign !is null && allDesigns == false)) return; // test if already loaded // Set up a reader scope IReader reader; try { reader = confDir.makeMTReader (fileName, PRIORITY.HIGH_LOW, null, true); // Read from the HEADER: // Get the renderer char[]* p = "Renderer" in reader.dataset.header._charA; if (p is null || *p is null) { logger.warn ("No renderer specified: using \"Simple\""); rendName = "Simple"; } else rendName = *p; // Get which section to use p = "Design" in reader.dataset.header._charA; if (p is null || *p is null) { logger.warn ("No gui design specified: trying \"Default\""); defaultDesign = "Default"; } else defaultDesign = *p; // Read the body: // Load the chosen design reader.dataSecCreator = delegate mt.IDataSection(mt.ID id) { WidgetDataSet* p = id in data; if (p is null) { data[id] = new WidgetDataSet; return *(id in data); } return *p; }; if (allDesigns) { reader.read; allLoaded = true; } else reader.read([defaultDesign]); } catch (NoFileException) { logger.error ("Unable to load GUI: no config file: "~fileName); // just return: not a fatal error (so long as the game can run without a GUI!) } catch (Exception e) { logger.error ("Unable to load GUI: errors parsing config file ("~confDir.getFileName(fileName,PRIORITY.HIGH_LOW)~"):"); logger.error (e.msg); throw new GuiException ("Failure parsing config file"); } Items.loadTranslation (); } /** Load the gui from some design. * * If a design was previously loaded, its changes are saved first. * * Params: * name = Design to load. If null, the default will be loaded. */ void loadDesign (char[] name = null) { if (changes !is null) // A design was previously loaded save; // own lock mutex.lock; scope(exit) mutex.unlock; // Load data (loadData tests if it's already loaded first): if (name is null) { loadData (false); name = defaultDesign; } else loadData (true); // Get data: auto p = name in data; while (p is null) { if (name == defaultDesign) throw new GuiException ("Unable to load [specified or] default design"); name = defaultDesign; // try again with the default p = name in data; } curData = *p; // Get/create a changes section: if (changesDS is null) changesDS = new mt.DataSet; mt.IDataSection* q = name in changesDS.sec; if (!q || ((changes = cast(WidgetDataChanges) *q) is null)) { changes = new WidgetDataChanges (curData); changesDS.sec[name] = changes; } // Create the widgets: createRootWidget; } /** Save changes, if any exist. * * Is run when the manager is destroyed, but could be run at other times too. */ void save () { preSave; mutex.lock; scope(exit) mutex.unlock; // Make all widgets save any changed data: child.saveChanges; if (changes.noChanges) return; if (loadUserFile) { // merge entries from user file into current changes try { scope IReader reader = confDir.makeMTReader ( fileName, PRIORITY.HIGH_ONLY, changesDS, true); // Create if necessary, only corresponding to existing designs read: reader.dataSecCreator = delegate mt.IDataSection(mt.ID id) { WidgetDataSet* p = id in data; if (p is null) throw new Exception ("File has changed since it was loaded!"); return new WidgetDataChanges (*p); }; reader.read; } catch (NoFileException) { // No user file exists; not an error. } catch (Exception e) { logger.error ("Error reading "~confDir.getFileName(fileName,PRIORITY.HIGH_ONLY)~" prior to saving:"); logger.error (e.msg); logger.error ("Overwriting the file."); // Continue... } loadUserFile = false; // don't need to do it again } try { // Save IWriter writer; writer = confDir.makeMTWriter (fileName, changesDS); writer.write; } catch (Exception e) { logger.error ("Saving to "~confDir.getFileName(fileName,PRIORITY.HIGH_ONLY)~" failed:"); logger.error (e.msg); // No point in throwing since it doesn't affect anything else. } } /** Get the names of all designs available. */ char[][] designs() { synchronized(mutex) { loadData (true); return data.keys; } } /** Called when translation strings have been reloaded. */ void reloadStrings (Content) { Items.loadTranslation; child.setup (++setupN, 2); child.setWidth (w, -1); child.setHeight (h, -1); child.setPosition (0,0); requestRedraw; } //BEGIN IParentWidget methods // If call reaches the widget manager there isn't any recursion. //NOTE: should be override final void recursionCheck (widgetID) {} override void minWChange (IChildWidget widget, wdim nmw) { debug assert (widget is child, "WM.mSC (code error)"); mw = nmw; if (w < nmw) { child.setWidth (nmw, -1); w = nmw; } child.setPosition (0,0); requestRedraw; } override void minHChange (IChildWidget widget, wdim nmh) { debug assert (widget is child, "WM.mSC (code error)"); mh = nmh; if (h < nmh) { child.setHeight (nmh, -1); h = nmh; } child.setPosition (0,0); requestRedraw; } //END IParentWidget methods //BEGIN IWidgetManager methods override IChildWidget makeWidget (IParentWidget parent, widgetID id, IContent content = null) { debug assert (parent, "makeWidget: parent is null (code error)"); debug scope (failure) logger.warn ("Creating widget \""~id~"\" failed."); WidgetData data = curData[id]; if (data.ints.length < 1) { logger.error ("No int data; creating a debug widget"); data.ints = [WIDGET_TYPE.Debug]; } int type = data.ints[0]; // type is first element of data try { // Statically programmed binary search on type, returning a new widget or calling a // function: //pragma (msg, binarySearch ("type", WIDGETS)); mixin (binarySearch ("type", WIDGETS)); // Not returned a new widget: logger.error ("Bad widget type: {}; creating a debug widget instead",type); } catch (Exception e) { logger.error ("Error creating widget: {}; creating a debug widget instead.", e.msg); } return new DebugWidget (this, this, id, data); } override WidgetData widgetData (widgetID id) { return curData[id]; } override void widgetData (widgetID id, WidgetData d) { changes[id] = d; // also updates WidgetDataSet in data. } override wdims dimData (widgetID id) { return curData.dims (id); } override void dimData (widgetID id, wdims d) { changes.setDims(id, d); // also updates WidgetDataSet in data. } // These methods are only intended for use within the gui package. They are not necessarily // thread-safe. IRenderer renderer () { assert (rend !is null, "WidgetManager.renderer: rend is null"); return rend; } void addPopup (IChildWidget parnt, IChildWidget widg, int flags = 0) { debug assert (parnt && widg, "addPopup: null widget"); if (popups.length >= popupsMem.length) popupsMem.length = popupsMem.length * 2 + 2; with (popupsMem[popups.length]) { parent = parnt; widget = widg; w = widg.width; h = widg.height; if (flags & 1) { y = parent.yPos; if (y+h > this.h) y += parent.height - h; x = parent.xPos + parent.width; if (x+w > this.w) x = parent.xPos - w; } else { x = parent.xPos; // align on left edge if (x+w > this.w) x += parent.width - w; // align on right edge y = parent.yPos + parent.height; // place below if (y+h > this.h) y = parent.yPos - h; // place above } widget.setPosition (x, y); } popups = popupsMem[0..popups.length+1]; requestRedraw; } void removePopup (IChildWidget parnt) { foreach_reverse (i,popup; popups) { if (popup.parent is parnt) popups = popups[0..i]; } requestRedraw; } void requestRedraw () { imde.mainSchedule.request(imde.SCHEDULE.DRAW); } void addClickCallback (bool delegate(wdabs, wdabs, ubyte, bool) dg) { clickCallbacks[dg.ptr] = dg; } void addMotionCallback (void delegate(wdabs, wdabs) dg) { motionCallbacks[dg.ptr] = dg; } void removeCallbacks (void* frame) { clickCallbacks.removeKey(frame); motionCallbacks.removeKey(frame); } //END IWidgetManager methods debug void logWidgetSize (Content) { logger.trace ("Current size: {,4},{,4}; minimal: {,4},{,4} - WidgetManager", w,h, mw,mh); child.logWidgetSize; } protected: /** Second stage of loading the widgets. * * loadDesign handles the data; this method needs to: * --- * // 1. Create the root widget: * child = makeWidget ("root"); * child.setup (0, 3); * // 2. Set the size: * child.setWidth (child.minWidth, 1); * child.setHeight (child.minHeight, 1); * // 3. Set the position (necessary part of initialization): * child.setPosition (0,0); * --- */ void createRootWidget(); /** Called before saving (usually when the GUI is about to be destroyed, although not * necessarily). */ void preSave (); public: //BEGIN makeWidget metacode private static { /// Widget types. Items match widget names without the "Widget" suffix. enum WIDGET_TYPE : int { FUNCTION = 0x2000, // Function called instead of widget created (no "Widget" appended to fct name) TAKES_CONTENT = 0x4000, // Flag indicates widget's this should be passed an IContent reference. SAFE_RECURSION = 0x8000, // Safe to instantiate recursively without infinite looping. // Use widget names rather than usual capitals convention Unnamed = 0x0, // Only for use by widgets not created with createWidget // blank: 0x1 FixedBlank = 0x1, SizableBlank = 0x2, Debug = 0xF, // popup widgets: 0x10 PopupMenu = TAKES_CONTENT | 0x11, SubMenu = TAKES_CONTENT | 0x12, // labels: 0x20 ContentLabel = TAKES_CONTENT | 0x20, TextLabel = 0x21, // content functions: 0x30 editContent = FUNCTION | TAKES_CONTENT | SAFE_RECURSION | 0x30, addContent = FUNCTION | 0x31, flatMenuContent = FUNCTION | TAKES_CONTENT | SAFE_RECURSION | 0x32, subMenuContent = FUNCTION | TAKES_CONTENT | 0x33, // content widgets: 0x40 DisplayContent = TAKES_CONTENT | 0x40, BoolContent = TAKES_CONTENT | 0x41, AStringContent = TAKES_CONTENT | 0x42, ButtonContent = TAKES_CONTENT | 0x43, MenuButtonContent = TAKES_CONTENT | 0x44, GridLayout = TAKES_CONTENT | 0x100, ContentList = TAKES_CONTENT | SAFE_RECURSION | 0x110, FloatingArea = TAKES_CONTENT | 0x200, Switch = TAKES_CONTENT | 0x210, } // Only used for binarySearch algorithm generation; must be ordered by numerical values. const char[][] WIDGETS = [ "FixedBlank", "SizableBlank", "Debug", "TextLabel", "addContent", "PopupMenu", "SubMenu", "ContentLabel", "DisplayContent", "BoolContent", "AStringContent", "ButtonContent", "MenuButtonContent", "GridLayout", "FloatingArea", "Switch", "subMenuContent", "ContentList", "editContent", "flatMenuContent"]; /* Generates a binary search algorithm for makeWidget. */ char[] binarySearch (char[] var, char[][] consts) { if (consts.length > 3) { return `if (`~var~` <= WIDGET_TYPE.`~consts[$/2 - 1]~`) {` ~ binarySearch (var, consts[0 .. $/2]) ~ `} else {` ~ binarySearch (var, consts[$/2 .. $]) ~ `}`; } else { char[] ret; foreach (c; consts) { ret ~= `if (` ~ var ~ ` == WIDGET_TYPE.` ~ c ~ `) { debug (mdeWidgets) logger.trace ("Creating new `~c~`."); if (!(WIDGET_TYPE.`~c~` & WIDGET_TYPE.SAFE_RECURSION)) parent.recursionCheck (id); static if (WIDGET_TYPE.`~c~` & WIDGET_TYPE.FUNCTION) return `~c~` (this, parent, id, data, content); else static if (WIDGET_TYPE.`~c~` & WIDGET_TYPE.TAKES_CONTENT) return new `~c~`Widget (this, parent, id, data, content); else return new `~c~`Widget (this, parent, id, data); } else `; } ret = ret[0..$-6]; // remove last else return ret; } } debug { // check items in WIDGETS are listed in order char[] WIDGETS_check () { char[] ret; for (int i = WIDGETS.length-2; i > 0; --i) { ret ~= "WIDGET_TYPE."~WIDGETS[i] ~" >= WIDGET_TYPE."~ WIDGETS[i+1]; if (i>1) ret ~= " || "; } return ret; } mixin ("static if ("~WIDGETS_check~") static assert (false, \"WIDGETS is not in order!\");"); } } //END makeWidget metacode protected: // Dataset/design data: final char[] fileName; char[] defaultDesign; // The design specified in the file header. char[] rendName; // Name of renderer; for saving and creating renderers // Loaded data, indexed by design name. May not be loaded for all gui designs: scope WidgetDataSet[char[]] data; private bool allLoaded = false; // applies to data WidgetDataSet curData; // Current data WidgetDataChanges changes; // Changes for the current design. scope mt.DataSet changesDS; // changes and sections from user file (used for saving) bool loadUserFile = true; // still need to load user file for saving? IRenderer rend; // Widgets: wdim w,h; // current widget size; should be at least (mw,mh) even if not displayable wdim mw,mh; // minimal area required by widgets scope IChildWidget child; // The primary widget. uint setupN; // n to pass to IChildWidget.setup struct ActivePopup { IChildWidget widget; IChildWidget parent; wdabs x,y; wdsize w,h; } ActivePopup[] popups; // Pop-up [menus] to draw. Last element is top popup. ActivePopup[] popupsMem; // allocated memory for popups // callbacks indexed by their frame pointers. Must support removal of elements in foreach: SortedMap!(void*,bool delegate(wdabs cx, wdabs cy, ubyte b, bool state)) clickCallbacks; SortedMap!(void*,void delegate(wdabs cx, wdabs cy)) motionCallbacks; IChildWidget keyFocus; // widget receiving keyboard input when non-null IChildWidget highlighted; // NOTE: in some ways should be same as keyFocus Mutex mutex; // lock on methods for use outside the package. }