Mercurial > projects > mde
view mde/gui/WidgetManager.d @ 131:9cff74f68b84
Major revisions to popup handling. Buttons can close menus now, plus some smaller impovements. Removed Widget module.
Moved Widget.AWidget to AChildWidget.AChildWidget and Widget.AParentWidget to AParentWidget.AParentWidget.
Removed ASingleParentWidget to improve code sharing.
AChildWidget doesn't implement IParentWidget like AWidget did.
New IPopupParentWidget extending IParentWidget for the WM and some widgets to handle popups.
Cut old popup management code.
New underMouse() function replacing highlight(); called on all widgets.
Separate menu-popup and button widgets aren't needed for menus now.
Functions returning content widgets have been moved to their own module.
Cleaned up jobs.txt.
Switched to 80 line length for Ddoc.
author | Diggory Hardy <diggory.hardy@gmail.com> |
---|---|
date | Wed, 21 Jan 2009 13:01:40 +0000 |
parents | ad91de8867a0 |
children | 264028f4115a |
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. * * Public non IWidget* methods should be thread-safe. *****************************************************************************/ 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.contentFunctions; 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; underMouse = child; // must be something } /** 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. */ protected void reloadStrings (Content) { Items.loadTranslation; child.setup (++setupN, 2); child.setWidth (w, -1); child.setHeight (h, -1); child.setPosition (0,0); requestRedraw; } // These methods are only intended for use within the gui package. // They are not necessarily thread-safe: //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 IPopupParentWidget methods override IPopupParentWidget getParentIPPW () { return this; } override void addChildIPPW (IPopupParentWidget ippw) { if (childIPPW) childIPPW.removedIPPW; childIPPW = ippw; requestRedraw; } override bool removeChildIPPW (IPopupParentWidget ippw) { if (childIPPW !is ippw) return false; childIPPW.removedIPPW; childIPPW = null; mAIPPW = false; requestRedraw; return false; } override void menuActive (bool mA) { mAIPPW = mA; if (childIPPW) childIPPW.menuActive = mA; } override bool menuActive () { return mAIPPW; } // Don't do anything. E.g. can get called by non-popup buttons. override void menuDone () {} override IChildWidget getPopupWidget (wdabs cx, wdabs cy, bool closePopup) { IChildWidget ret; if (childIPPW) { ret = childIPPW.getPopupWidget (cx, cy, closePopup); if (closePopup && ret is null) { menuActive = false; removeChildIPPW (childIPPW); } } return ret; } debug protected override bool isChild (IPopupParentWidget ippw) { return ippw is childIPPW; } override void removedIPPW () {} // irrelevant override void drawPopup () {} //END IPopupParentWidget 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. } IRenderer renderer () { assert (rend !is null, "WidgetManager.renderer: rend is null"); return rend; } void positionPopup (IChildWidget parent, IChildWidget popup, int flags = 0) { debug assert (parent && popup, "positionPopup: null widget"); wdim w = popup.width, h = popup.height, x, y; 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 } popup.setPosition (x, y); } 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 ("size: {,4},{,4}; minimal: {,4},{,4} - WidgetManager", w,h, mw,mh); child.logWidgetSize; } protected: void updateUnderMouse (wdabs cx, wdabs cy, bool closePopup) { auto oUM = underMouse; underMouse = getPopupWidget (cx, cy, closePopup); if (underMouse is null) { debug assert (child.onSelf (cx, cy), "WidgetManager: child doesn't cover whole area"); underMouse = child.getWidget (cx, cy); } if (underMouse !is oUM) { debug assert (oUM && underMouse, "no widget under mouse: error"); oUM.underMouse (false); underMouse.underMouse (true); } } /** 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, // 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, 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", "ContentLabel", "DisplayContent", "BoolContent", "AStringContent", "ButtonContent", "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 bool mAIPPW; // IPPW variable IPopupParentWidget childIPPW; // child IPPW, if any active // 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 IChildWidget underMouse; // widget under the mouse pointer Mutex mutex; // lock on methods for use outside the package. }