Mercurial > projects > mde
view mde/gui/WidgetManager.d @ 113:9824bee909fd
Popup menu; works for simple menus except that clicking an item doesn't close it.
Revised popup support a bit; EnumContentWidget is broken and due to be replaced.
author | Diggory Hardy <diggory.hardy@gmail.com> |
---|---|
date | Fri, 19 Dec 2008 10:32:28 +0000 |
parents | fe061009029d |
children | b16a534f5302 |
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. * * This is the module to use externally to create a graphical user interface (likely also with * content modules). *************************************************************************************************/ module mde.gui.WidgetManager; import mde.gui.WidgetDataSet; import mde.gui.widget.Ifaces; import mde.gui.renderer.createRenderer; import imde = mde.imde; import mde.input.Input; import mde.scheduler.Scheduler; import mde.setup.Screen; import Items = mde.content.Items; // loadTranslation import mde.lookup.Options; // miscOpts.L10n callback import tango.core.sync.Mutex; import tango.util.log.Log : Log, Logger; import tango.util.container.CircularList; // pop-up draw callbacks import tango.util.container.SortedMap; private Logger logger; static this () { logger = Log.getLogger ("mde.gui.WidgetManager"); } /************************************************************************************************* * The widget manager. * * This provides a layer on top of WidgetLoader, handling input and rendering. Other functionality * is contained in the super class, to simplify supporting new input/graphics libraries. * * Currently mouse coordinates are passed to widgets untranslated. It may make sense to translate * them and possibly drop events for some uses, such as if the gui is drawn to a texture. * * Aside from the IWidgetManager methods, this class should be thread-safe. *************************************************************************************************/ class WidgetManager : WidgetLoader, Screen.IDrawable { /** Construct a new widget manager. * * params: * fileName = Name of file specifying the gui, excluding path and extension. */ this (char[] file) { super(file); Screen.addDrawable (this); clickCallbacks = new typeof(clickCallbacks); motionCallbacks = new typeof(motionCallbacks); } // this() runs during static this(), when imde.input doesn't exist. init() runs later. void init () { // Doesn't need a lock - cannot conflict with other class functions. // Events we want to know about: imde.input.addMouseClickCallback(&clickEvent); imde.input.addMouseMotionCallback(&motionEvent); Items.loadTranslation (); miscOpts.L10n.addCallback (&reloadStrings); } /** Draw the gui. */ void draw() { synchronized(mutex) { if (child) child.draw; foreach (popup; popups.iterator.reverse) popup.widget.draw(); } } /** For mouse click events. * * Sends the event on to the relevant windows and all click callbacks. */ void clickEvent (ushort cx, ushort cy, ubyte b, bool state) { debug scope (failure) logger.warn ("clickEvent: failed!"); mutex.lock; scope(exit) mutex.unlock; if (child is null) return; // 1. Callbacks have the highest priority recieving events (e.g. a button release) foreach (dg; clickCallbacks) if (dg (cast(wdabs)cx, cast(wdabs)cy, b, state)) return; // 2. Then pop-ups static IChildWidget[] removedPopupParents; uint removedPopups = 0; IChildWidget widg; // widget clicked on { auto i = popups.iterator; foreach (popup; i) with (popup) { if (cx < x || cx >= x + w || cy < y || cy >= y + h) { i.remove; requestRedraw; if (removedPopupParents.length <= removedPopups) removedPopupParents.length = removedPopupParents.length * 2 + 4; removedPopupParents[removedPopups++] = parent; } else { widg = widget.getWidget (cast(wdabs)cx,cast(wdabs)cy); break; } } } // 3. Then the main widget tree debug assert (cx < child.width && cy < child.height, "WidgetManager: child doesn't cover whole area (code error)"); if (widg is null) widg = child.getWidget (cast(wdabs)cx,cast(wdabs)cy); if (keyFocus && keyFocus !is widg) { keyFocus.keyFocusLost; keyFocus = null; imde.input.setLetterCallback (null); } if (widg !is null) { if (widg.clickEvent (cast(wdabs)cx,cast(wdabs)cy,b,state) & 1) { keyFocus = widg; imde.input.setLetterCallback (&widg.keyEvent); } } // Tell parents their popups closed (needs to be after clickEvent for PopupMenuWidget) while (removedPopups) removedPopupParents[--removedPopups].popupRemoved; } /** For mouse motion events. * * Sends the event on to all motion callbacks. */ void motionEvent (ushort scx, ushort scy) { debug scope (failure) logger.warn ("motionEvent: failed!"); mutex.lock; scope(exit) mutex.unlock; wdabs cx = cast(wdabs) scx, cy = cast(wdabs) scy; foreach (dg; motionCallbacks) dg (cx, cy); IChildWidget ohighlighted = highlighted; foreach (popup; popups) with (popup) { if (cx >= x && cx < x+w && cy >= y && cy < y+h) { highlighted = widget.getWidget (cx,cy); goto foundPopup; } } highlighted = null; // not over a popup foundPopup: if (ohighlighted != highlighted) { if (ohighlighted) ohighlighted.highlight (false); if (highlighted) highlighted.highlight (true); requestRedraw; } } void sizeEvent (int nw, int nh) { // IDrawable function mutex.lock; scope(exit) mutex.unlock; w = cast(wdim) nw; h = cast(wdim) nh; if (w < mw || h < mh) logger.warn ("Minimal dimensions ({},{}) not met: ({},{}), but I cannot resize myself!",mw,mh,w,h); if (!child) return; // if not created yet. child.setWidth (w, -1); child.setHeight (h, -1); child.setPosition (0,0); } //BEGIN IWidgetManager methods // 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; } /** Place a pop-up widget (widg) above or below parent. * * WidgetManager sets its position, draws it, passes it click events and removes it; other * functionality should be handled by the widget's parent. * * Popups currently should not change their size while active. */ void addPopup (IChildWidget parent, IChildWidget widg) { debug assert (parent && widg, "addPopup: null widget"); ActivePopup popup; popup.parent = parent; with (popup) { widget = widg; w = widg.width; h = widg.height; 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.prepend (popup); requestRedraw; } /+ Not required but possibly useful later. Not optimal. void removePopup (IChildWidget widg) { auto i = popups.iterator; foreach (popup; i) { if (popup.widget is widg) i.remove; } 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 protected: /* Second stage of widget loading. * Note: sizeEvent should be called with window size before this. */ void createRootWidget () { // The renderer needs to be created on the first load, but not after this. if (rend is null) rend = createRenderer (rendName); popups = new CircularList!(ActivePopup); child = makeWidget ("root"); child.setup (0, 3); mw = child.minWidth; mh = child.minHeight; if (w < mw || h < mh) logger.warn ("Minimal dimensions ({},{}) not met: ({},{}), but I cannot resize myself!",mw,mh,w,h); child.setWidth (w, -1); child.setHeight (h, -1); child.setPosition (0,0); } void preSave () { if (keyFocus) { keyFocus.keyFocusLost; keyFocus = null; imde.input.setLetterCallback (null); } } private: struct ActivePopup { IChildWidget widget; IChildWidget parent; wdabs x,y; wdsize w,h; } IRenderer rend; CircularList!(ActivePopup) popups;// Pop-up [menus] to draw. First element is top popup. // 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 } import mde.gui.exception; import mde.content.Content; // AContent passed to a callback import mde.gui.widget.createWidget; import mde.file.mergetag.Reader; import mde.file.mergetag.Writer; import mde.setup.paths; /************************************************************************************************* * 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 WidgetLoader : 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; } /* 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!"); // 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"); } } /** 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; return if no changes: if (!child.saveChanges) 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 (AContent) { Items.loadTranslation; child.setup (++setupN, 2); child.setWidth (w, -1); child.setHeight (h, -1); child.setPosition (0,0); requestRedraw; } 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 IWidgetManager methods IChildWidget makeWidget (widgetID id, IContent content = null) { debug (mdeWidgets) logger.trace ("Creating widget \""~id~'"'); return createWidget (this, id, curData[id], content); } IChildWidget makeWidget (widgetID id, WidgetData data, IContent content = null) { debug (mdeWidgets) logger.trace ("Creating widget \""~id~'"'); return createWidget (this, id, data, content); } wdims dimData (widgetID id) { return curData.dims (id); } void setData (widgetID id, WidgetData d) { changes[id] = d; // also updates WidgetDataSet in data. } void setDimData (widgetID id, wdims d) { changes.setDims(id, d); // also updates WidgetDataSet in data. } //END IWidgetManager methods protected: 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? wdim w,h; // area available to the widgets wdim mw,mh; // minimal area required by widgets (ideally for limiting w,h) scope IChildWidget child; // The primary widget. uint setupN; // n to pass to IChildWidget.setup Mutex mutex; // lock on methods for use outside the package. }