Mercurial > projects > mde
view mde/gui/WidgetManager.d @ 166:55667d048c31
Made content displayable while being dragged.
author | Diggory Hardy <diggory.hardy@gmail.com> |
---|---|
date | Sun, 21 Jun 2009 12:19:18 +0200 |
parents | bb2f1a76346d |
children | 620d4ea30228 |
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 imde = mde.imde; import mde.content.Content; debug import mde.content.miscContent; // Debug menu // 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; public 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. * * 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); 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 child) // Usually because widget is a floating widget // This may get called from a CTOR, hence we can't check widget is one of popupContext, etc. return; mw = nmw; if (w < nmw) { child.setWidth (nmw, -1); w = nmw; } child.setPosition (0,0); requestRedraw; } override void minHChange (IChildWidget widget, wdim nmh) { if (widget !is child) return; mh = nmh; if (h < nmh) { child.setHeight (nmh, -1); h = nmh; } child.setPosition (0,0); requestRedraw; } override bool dropContent (IContent content) { return false; } //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 = MenuPosition.INACTIVE; requestRedraw; return false; } override void menuActive (MenuPosition mA) { mAIPPW = mA; if (childIPPW) childIPPW.menuActive = mA; } override MenuPosition menuActive () { return mAIPPW; } override MenuPosition parentMenuActive () { return MenuPosition.INACTIVE; } // 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) { if (popupContext) { if (popupContext.onSelf (cx, cy)) return popupContext; if (closePopup) { if (childIPPW is null) menuActive = MenuPosition.INACTIVE; popupContext = null; requestRedraw; } } if (childIPPW) { IChildWidget ret = childIPPW.getPopupWidget (cx, cy, closePopup); if (ret) return ret; if (closePopup) { menuActive = MenuPosition.INACTIVE; removeChildIPPW (childIPPW); } } return null; } override void drawPopup () { if (popupContext) popupContext.draw(); if (dragContentDisplay) dragContentDisplay.draw(); } debug protected override bool isChild (IPopupParentWidget ippw) { 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.", e.msg); } 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"); 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 logger.trace ("placed popup at {},{}; size: {},{}", x,y, w,h); return position; } void requestRedraw () { imde.mainSchedule.request(imde.SCHEDULE.DRAW); } //END IWidgetManager methods debug void logWidgetSize (Content) { logger.trace ("size: {,4},{,4}; minimal: {,4},{,4} - WidgetManager", w,h, mw,mh); child.logWidgetSize; } protected: // These methods are called by derived classes to do the widget-management work //BEGIN WidgetManagement methods /** Draw all widgets */ void wmDrawWidgets() { if (child) child.draw; if (childIPPW) childIPPW.drawPopup; drawPopup; } /** For mouse click events. * * Sends the event on to the relevant windows and all click callbacks. */ void wmMouseClick (wdabs cx, wdabs cy, ubyte b, bool state) { if (child 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; dragContentDisplay = 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 IContent contextContent = underMouse.content; if (contextContent is null) return; // NOTE: Creates new widgets every time; not optimal popupContext = makeWidget (this, "context", contextContent); popupContext.setup (0, 3); positionPopup (underMouse, popupContext); requestRedraw; } 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 dragContentDisplay = new DisplayContentWidget (this, this, "dragContentDisplay", WidgetData ([0], []), c); dragContentDisplay.setup (0, 3); dragX = underMouse.xPos - cx; dragY = underMouse.yPos - cy; dragContentDisplay.setPosition (cx + dragX, cy + dragY); } } } } } /** For mouse motion events. * * Lock on mutex before calling. Pass new mouse coordinates. */ void wmMouseMotion (wdabs cx, wdabs cy) { updateUnderMouse (cx, cy, false); if (dragStart !is null) { dragStart.dragMotion (cx, cy, underMouse); if (dragContentDisplay !is null) { dragContentDisplay.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! */ void reloadStrings (Content) { synchronized(mutex) { if (child is null) return; child.setup (++setupN, 2); child.setWidth (w, -1); child.setHeight (h, -1); child.setPosition (0,0); requestRedraw; } } // for internal use 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); } } /// 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: WidgetDataSet curData; // Current data WidgetDataChanges changes; // Changes for the current design. char[] rendName; // Name of renderer; for saving and creating renderers 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 MenuPosition mAIPPW; // IPPW variable IPopupParentWidget childIPPW; // child IPPW, if any active // Popup(s) handled directly by AWidgetManager: IChildWidget popupContext; // context menu (active if not null) IChildWidget dragContentDisplay; // 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 IChildWidget keyFocus; // widget receiving keyboard input IChildWidget underMouse; // widget under the mouse pointer Mutex mutex; // lock on methods for use outside the package. }