Mercurial > projects > mde
diff mde/gui/widget/WidgetManager.d @ 175:1cbde9807293
Compile/link-time fixes for ldc & non-debug builds.
Moved WidgetManager to widget/
Reverted IChildWidget to an interface, not an abstract class.
Introduced a work-around for a compiler problem. May not cover all cases.
author | Diggory Hardy <diggory.hardy@gmail.com> |
---|---|
date | Fri, 11 Sep 2009 20:56:53 +0200 |
parents | mde/gui/WidgetManager.d@a1ba9157510e |
children | af40e9679436 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mde/gui/widget/WidgetManager.d Fri Sep 11 20:56:53 2009 +0200 @@ -0,0 +1,658 @@ +/* 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 widget manager; root of the widget tree. + * + * Rendering is handled separately by an IRenderer. + *****************************************************************************/ +module mde.gui.widget.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"); +} + +/****************************************************************************** + * 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. + *****************************************************************************/ +abstract class AWidgetManager : IWidgetManager +{ + //BEGIN Public methods, for use outside the widget package + /** Construct a new widget manager. + * + * Params: + * name = The file name of the config for this GUI (to identify multiple GUIs). */ + 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); + } + } + + /** 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; + } + } + + debug public 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; + } + } + + + //BEGIN Public IWidget methods + override bool saveChanges () { + bool ret = childRoot.saveChanges; + ret |= childContext.saveChanges; + if (childDragged !is null) + ret |= childDragged.saveChanges; + return ret; + } + + /** Draw all widgets */ + override void draw () { + if (childRoot) + childRoot.draw; + drawPopup; + } + //END Public IWidget methods + //END Public methods, for use outside the widget package + + //BEGIN IWidget methods for widgets + public override bool dropContent (IContent content) { + return false; + } + //END IWidget methods for widgets + + //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 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 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 + +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"); + underMouse = childRoot; // don't leave null due to a check + 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 + } + + final void wmSizeEvent (int nw, int nh) { + w = cast(wdim) nw; + h = cast(wdim) nh; + matchMinimalSize; + + if (!childRoot) return; // if not created yet. + childRoot.setWidth (w, -1); + childRoot.setHeight (h, -1); + childRoot.setPosition (0,0); + debug logWidgetSize (null); + } + + /** 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; + } + } + } + + // for internal use + private 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); + } + debug assert (oUM && underMouse, "no widget under mouse: error"); + if (underMouse !is oUM) { + 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 + + //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; should never be null when childRoot is non-null + + + // 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. +}