Mercurial > projects > mde
view mde/gui/WidgetManager.d @ 173:a1ba9157510e
Enabled ServiceContentList to call its callbacks when its value changes. Tried to fix some other bugs, but this is not a very clean commit, due to wanting to make some big changes to enable better use of invariants next.
author | Diggory Hardy <diggory.hardy@gmail.com> |
---|---|
date | Sat, 08 Aug 2009 15:53:10 +0200 |
parents | 7f7b2011b759 |
children |
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.renderer.createRenderer; import imde = mde.imde; import mde.content.Content; import mde.content.ServiceContent; debug import mde.content.miscContent; // Debug menu debug import mde.content.Debug; // 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.ParentContent; import mde.gui.widget.AParentWidget; public import tango.core.sync.Mutex; import tango.util.log.Log : Log, Logger; import tango.io.Console; // to print exception stack-trace 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. * * Methods in this class are only intended for use within the gui package, * either by widgets (the IXXXWidget methods implementing from an interface in * widgets.Ifaces.d) or by a derived class (back-end methods doing widget * work). None of these methods are intended to be thread-safe when called * concurrently on the same WidgetManager instance, but they should be thread- * safe for calling on separate instances. * * This abstract class exists solely for separating out some of the functionality. *****************************************************************************/ abstract scope class AWidgetManager : IWidgetManager { /** Construct a new widget manager. * * Params: * name = The file name of the config for this GUI (to identify multiple GUIs). */ protected this (char[] name) { auto p = "MiscOptions.l10n" in Content.allContent; assert (p, "MiscOptions.l10n not created!"); p.addCallback (&reloadStrings); serviceContent = ServiceContentList.createItems (name); assert (cast (IServiceContent) Content.get ("menus.services."~name)); debug { // add a debug-mode menu auto lWS = new EventContent ("menus.debug."~name~".logWidgetSize"); lWS.addCallback (&logWidgetSize); } } public: //BEGIN IParentWidget methods // If call reaches the widget manager there isn't any recursion. //NOTE: should be override final void recursionCheck (widgetID, IContent) {} override void minWChange (IChildWidget widget, wdim nmw) { if (widget !is childRoot) { // Probably because widget is a popup widget // This may get called from a CTOR, hence we can't check widget is one of childContext, etc. if (widget.width < nmw) widget.setWidth (nmw, -1); return; } mw = nmw; if (w < nmw) { childRoot.setWidth (nmw, -1); w = nmw; } childRoot.setPosition (0,0); requestRedraw; } override void minHChange (IChildWidget widget, wdim nmh) { if (widget !is childRoot) { if (widget.height < nmh) widget.setHeight (nmh, -1); return; } mh = nmh; if (h < nmh) { childRoot.setHeight (nmh, -1); h = nmh; } childRoot.setPosition (0,0); requestRedraw; } //END IParentWidget methods //BEGIN IWidget methods public override bool saveChanges () { bool ret = childRoot.saveChanges; ret |= childContext.saveChanges; if (childDragged !is null) ret |= childDragged.saveChanges; return ret; } override bool dropContent (IContent content) { return false; } //END IWidget methods //BEGIN IPopupParentWidget methods override IPopupParentWidget getParentIPPW () { return this; } override void addChildIPPW (IPopupParentWidget ippw) { requestRedraw; if (ippw is childContext) { // special handling - a separate IPPW contextActive = true; return; } if (childIPPW) childIPPW.removedIPPW; childIPPW = ippw; } override bool removeChildIPPW (IPopupParentWidget ippw) { if (ippw is childContext && contextActive) { childContext.removedIPPW; contextActive = false; return true; } if (childIPPW !is ippw) return false; childIPPW.removedIPPW; childIPPW = null; mAIPPW = MenuPosition.INACTIVE; requestRedraw; return true; } override void menuActive (MenuPosition mA) { mAIPPW = mA; if (childIPPW) childIPPW.menuActive = mA; if (contextActive) childContext.menuActive = mA; } override MenuPosition menuActive () { return mAIPPW; } override MenuPosition parentMenuActive () { return MenuPosition.INACTIVE; } // Note: also triggered by non-popup widgets override void menuDone () {} override IChildWidget getPopupWidget (wdabs cx, wdabs cy, bool closePopup) { IChildWidget ret; // Don't bother with childDragged; it has no interaction if (contextActive) { ret = childContext.getPopupWidget (cx, cy, closePopup); if (ret) return ret; if (closePopup) { childContext.removedIPPW; contextActive = false; requestRedraw; } } if (childIPPW) { ret = childIPPW.getPopupWidget (cx, cy, closePopup); if (ret) return ret; if (closePopup) { removeChildIPPW (childIPPW); } } return null; } override void drawPopup () { if (childIPPW) childIPPW.drawPopup; if (contextActive) childContext.drawPopup(); if (childDragged) childDragged.draw(); } debug protected override bool isChild (IPopupParentWidget ippw) { if (contextActive && ippw is childContext) return true; return ippw is childIPPW; } override void removedIPPW () {} // irrelevant //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. Exception printed to stderr."); //TODO: find a standard way to output exceptions, and implement everywhere: e.writeOut(delegate void(char[]s){ Cerr(s); }); } return new DebugWidget (this, parent, id, data, content); } 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; } MenuPosition positionPopup (IChildWidget parent, IChildWidget popup, MenuPosition position = MenuPosition.INACTIVE) { debug assert (parent && popup, "positionPopup: null widget"); debug if (Debug.logPopupPositioning()) logger.trace ("Placing popup {} in relation to parent {}; input position: {}", popup, parent, position); wdim w = popup.width, h = popup.height, x, y; if (position & MenuPosition.ACTIVE) { y = parent.yPos; // height flush with top if (y+h > this.h) y += parent.height - h; // or bottom if (position & MenuPosition.LEFT) { // previously left x = parent.xPos - w; // on left if (x < 0) { x = parent.xPos + parent.width; // on right position = MenuPosition.RIGHT; } } else { // previously right or above/below x = parent.xPos + parent.width; // on right position = MenuPosition.RIGHT; if (x+w > this.w) { x = parent.xPos - w; // or left position = MenuPosition.LEFT; } } } else { wdim pw = parent.width; if (popup.minWidth <= pw) popup.setWidth (pw, -1); // neatness x = parent.xPos; // align on left edge if (x+w > this.w) x += pw - w; // align on right edge y = parent.yPos + parent.height; // place below if (y+h > this.h) y = parent.yPos - h; // or above position = MenuPosition.ACTIVE; } if (x < 0) x = 0; // may be placed partially off-screen if (y < 0) y = 0; popup.setPosition (x, y); debug if (Debug.logPopupPositioning()) logger.trace ("Placed popup {} of size ({},{}) at ({},{}); output position: {}", popup, w,h, x,y, position); return position; } void requestRedraw () { imde.mainSchedule.request(imde.SCHEDULE.DRAW); } //END IWidgetManager methods debug void logWidgetSize (IContent) { logger.trace ("size: {,4},{,4}; minimal: {,4},{,4} - WidgetManager", w,h, mw,mh); logger.trace ("childRoot:"); childRoot.logWidgetSize; logger.trace ("childContext:"); childContext.logWidgetSize; if (childDragged !is null) { logger.trace ("childDragged:"); childDragged.logWidgetSize; } } protected: // These methods are called by derived classes to do the widget-management work //BEGIN WidgetManagement methods /** Second stage of widget loading. * * Widget data should be loaded before this is called. */ final void createWidgets () { // The renderer needs to be created on the first load, but not after this. if (rend is null) rend = createRenderer (rendName); debug (mdeWidgets) logger.trace ("Creating root widget..."); childRoot = makeWidget (this, "root"); debug (mdeWidgets) logger.trace ("Setting up root widget..."); childRoot.setup (0, 3); mw = childRoot.minWidth; mh = childRoot.minHeight; matchMinimalSize (); debug (mdeWidgets) logger.trace ("Setting size and position of root widget..."); childRoot.setWidth (w, -1); childRoot.setHeight (h, -1); childRoot.setPosition (0,0); debug (mdeWidgets) logger.trace ("Done creating root widget."); childContext = new PopupHandlerWidget (this, this, "contextHandler", "context", serviceContent); childContext.setup (0,3); debug (mdeWidgets) logger.trace ("Created context handler widget."); underMouse = childRoot; // must be something } /** Draw all widgets */ final void wmDrawWidgets() { if (childRoot) childRoot.draw; drawPopup; } /** For mouse click events. * * Sends the event on to the relevant windows and all click callbacks. */ final void wmMouseClick (wdabs cx, wdabs cy, ubyte b, bool state) { if (childRoot is null) return; // Update underMouse to get the widget clicked on updateUnderMouse (cx, cy, state); // end of a drag? if (dragStart !is null && b == dragButton && state == false) { IChildWidget dS = dragStart; dragStart = null; childDragged = null; requestRedraw; if (dS.dragRelease (cx, cy, underMouse)) return; } // Disable keyboard input if on another widget: if (keyFocus && keyFocus !is underMouse) { keyFocus.keyFocusLost; keyFocus = null; setLetterCallback (null); } // Finally, post the actual event: if (b == 3 && state) { // right click - open context menu Content contextContent = cast(Content)underMouse.content; if (contextContent !is null) { serviceContent.setContent (contextContent); childContext.openMenu (underMouse, contextContent); } } else { // post other button presses to clickEvent int ret = underMouse.clickEvent (cast(wdabs)cx,cast(wdabs)cy,b,state); if (ret & 1) { // keyboard input requested keyFocus = underMouse; setLetterCallback (&underMouse.keyEvent); } if (ret & 2 && dragStart is null) { // drag events requested dragStart = underMouse; dragButton = b; // currently we allow any button to be used for a drag, but.. ? if (ret & 4) { IContent c = underMouse.content(); if (c) { // NOTE: creates a new widget, not optimal childDragged = new DisplayContentWidget (this, this, "dragContentDisplay", WidgetData ([0], []), c); childDragged.setup (0, 3); dragX = underMouse.xPos - cx; dragY = underMouse.yPos - cy; childDragged.setPosition (cx + dragX, cy + dragY); } } } } } /** For mouse motion events. * * Lock on mutex before calling. Pass new mouse coordinates. */ final void wmMouseMotion (wdabs cx, wdabs cy) { updateUnderMouse (cx, cy, false); if (dragStart !is null) { dragStart.dragMotion (cx, cy, underMouse); if (childDragged !is null) { childDragged.setPosition (cx + dragX, cy + dragY); requestRedraw; } } } /** A change callback on MiscOptions.l10n content to update widgets. * * Relies on another callback reloading translations to content first! */ final void reloadStrings (IContent) { synchronized(mutex) { if (childRoot is null) return; childRoot.setup (++setupN, 2); childRoot.setWidth (w, -1); childRoot.setHeight (h, -1); childRoot.setPosition (0,0); childContext.setup (setupN, 2); //TODO: possibly childDragged? requestRedraw; } } // for internal use final void updateUnderMouse (wdabs cx, wdabs cy, bool closePopup) { auto oUM = underMouse; underMouse = getPopupWidget (cx, cy, closePopup); if (underMouse is null) { debug assert (childRoot.onSelf (cx, cy), "WidgetManager: childRoot doesn't cover whole area"); underMouse = childRoot.getWidget (cx, cy); } if (underMouse !is oUM) { debug assert (oUM && underMouse, "no widget under mouse: error"); oUM.underMouse (false); underMouse.underMouse (true); debug if (Debug.logUnderMouse()) logger.trace ("Widget under mouse: {}", underMouse); } } /** If possible, the screen-interaction derived class should override to * make sure the window is at least (mw,mh) in size. In any case, this * method MUST make sure w >= mw and h >= mh even if the window isn't this * big. * * A resize may not be required when this is called, however. */ void matchMinimalSize () { if (w < mw) { logger.warn ("Min width for gui, {}, not met: {}", mw, w); w = mw; } if (h < mh) { logger.warn ("Min height for gui, {}, not met: {}", mh, h); h = mh; } } /// This should be overloaded to set a callback receiving keyboard input. abstract void setLetterCallback(void delegate(ushort, char[])); //END WidgetManagement methods 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. // 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 = TAKES_CONTENT | 0xF, // popup widgets: 0x10 PopupMenu = TAKES_CONTENT | 0x11, // labels: 0x20 TextLabel = 0x21, // content functions: 0x30 editContent = FUNCTION | TAKES_CONTENT | 0x30, addContent = FUNCTION | 0x31, popupListContent = FUNCTION | TAKES_CONTENT | 0x33, // content widgets: 0x40 DisplayContent = TAKES_CONTENT | 0x40, BoolContent = TAKES_CONTENT | 0x41, AStringContent = TAKES_CONTENT | 0x42, ButtonContent = TAKES_CONTENT | 0x43, SliderContent = TAKES_CONTENT | 0x44, GridLayout = TAKES_CONTENT | 0x100, ContentList = TAKES_CONTENT | 0x110, FloatingArea = TAKES_CONTENT | 0x200, Border = TAKES_CONTENT | 0x204, Switch = TAKES_CONTENT | 0x210, Collapsible = TAKES_CONTENT | 0x214, } // Only used for binarySearch algorithm generation; must be ordered by numerical values. const char[][] WIDGETS = [ "FixedBlank", "SizableBlank", "TextLabel", "addContent", "Debug", "PopupMenu", "DisplayContent", "BoolContent", "AStringContent", "ButtonContent", "SliderContent", "GridLayout", "ContentList", "FloatingArea", "Border", "Switch", "Collapsible", "editContent", "popupListContent"]; /* 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~`."); parent.recursionCheck (id, content); 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: // Main child widget: IChildWidget childRoot; // Root of the main GUI widget tree // Dimensions and child set-up data (fit to childRoot): wdim w,h; // current widget size; should be at least (mw,mh) even if not displayable wdim mw,mh; // minimal area required by widgets uint setupN; // n to pass to IChildWidget.setup // IPopupParentWidget stuff for childRoot: MenuPosition mAIPPW; // IPPW variable IPopupParentWidget childIPPW; // child IPPW, if any active IChildWidget keyFocus; // widget receiving keyboard input IChildWidget underMouse; // widget under the mouse pointer // Context menu: // Essentially, we consider childContext a full child IPPW, but handle it separately from // childIPPW. Instead of providing another ref. for this IPPW, shortcut by using this reference // and the boolean contextActive: scope PopupHandlerWidget childContext; // context menu popup (handler) bool contextActive = false; // If true, consider childContext a child IPPW scope IServiceContent serviceContent; // context menu content tree // Drag-and-drop data: //NOTE: could be wrapped with a PopupHandlerWidget, but can't set position then? scope IChildWidget childDragged; // displays dragged content; no interaction IChildWidget dragStart; // if non-null, this widget should receive motion and click-release events int dragButton; // index of button in use for drag wdrel dragX, dragY; // coordinates of dragged content relative to mouse // Renderer: char[] rendName; // Name of renderer; for saving and creating renderers scope IRenderer rend; // Data loaded/to save: WidgetDataSet curData; // Current data WidgetDataChanges changes; // Changes for the current design. Mutex mutex; // lock on methods for use outside the package. }