Mercurial > projects > mde
diff mde/gui/WidgetManager.d @ 121:5b37d0400732
Widgets now receive and store their parent (IParentWidget). Infinite widget recursion checks. WidgetManager code redistributed.
WidgetManager code redistributed between classes; WMScreen class moved to WMScreen.d.
addContent function now calls makeWidget with another id.
author | Diggory Hardy <diggory.hardy@gmail.com> |
---|---|
date | Fri, 02 Jan 2009 18:07:10 +0000 |
parents | aba2dd815a1f |
children | d3b2cefd46c9 |
line wrap: on
line diff
--- a/mde/gui/WidgetManager.d Thu Jan 01 15:16:00 2009 +0000 +++ b/mde/gui/WidgetManager.d Fri Jan 02 18:07:10 2009 +0000 @@ -14,27 +14,33 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. */ /************************************************************************************************* - * The gui manager class. + * The gui manager class base. * - * This is the module to use externally to create a graphical user interface (likely also with - * content modules). + * 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. *************************************************************************************************/ module mde.gui.WidgetManager; import mde.gui.WidgetDataSet; import mde.gui.widget.Ifaces; -import mde.gui.renderer.createRenderer; +import mde.gui.exception; +import mde.content.Content; 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 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.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.CircularList; // pop-up draw callbacks import tango.util.container.SortedMap; private Logger logger; @@ -43,283 +49,12 @@ } /************************************************************************************************* - * 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) - popup.widget.draw(); - } - } - - - /** For mouse click events. - * - * Sends the event on to the relevant windows and all click callbacks. */ - void clickEvent (ushort usx, ushort usy, ubyte b, bool state) { - debug scope (failure) - logger.warn ("clickEvent: failed!"); - mutex.lock; - scope(exit) mutex.unlock; - if (child is null) return; - - wdabs cx = cast(wdabs) usx, cy = cast(wdabs) usy; - - // 1. Callbacks have the highest priority recieving events (e.g. a button release) - foreach (dg; clickCallbacks) - if (dg (cx, cy, b, state)) return; - - // 2. Then pop-ups: close from top, depending on click pos - // Note: assumes each evaluated popup's parent is not under another still open popup. - // Also assumes popup's parent doesn't have other children in its box. - size_t removeTo = popups.length; - bool eventDone; // don't pass clickEvent - IChildWidget widg; // widget clicked on - foreach_reverse (i,popup; popups) with (popup) { - if (cx < x || cx >= x + w || - cy < y || cy >= y + h) { // on popup - if (parent.onSelf (cx, cy)) { - if (parent.popupParentClick()) removeTo = i; - eventDone = true; - break; - } else { - removeTo = i; - parent.popupClose; - } - } else { - widg = widget.getWidget (cast(wdabs)cx,cast(wdabs)cy); - break; - } - } - if (removeTo < popups.length) { - requestRedraw; - popups = popups[0..removeTo]; - } - if (eventDone) - return; - - // 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); - } - } - } - - /** 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_reverse (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; - } - - void addPopup (IChildWidget parnt, IChildWidget widg, int flags = 0) { - debug assert (parnt && widg, "addPopup: null widget"); - if (popups.length >= popupsMem.length) - popupsMem.length = popupsMem.length * 2 + 2; - with (popupsMem[popups.length]) { - parent = parnt; - widget = widg; - w = widg.width; - h = widg.height; - 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 - } - widget.setPosition (x, y); - } - popups = popupsMem[0..popups.length+1]; - requestRedraw; - } - void removePopup (IChildWidget parnt) { - foreach_reverse (i,popup; popups) { - if (popup.parent is parnt) - popups = popups[0..i]; - } - 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. */ - final override void createRootWidget () { - // The renderer needs to be created on the first load, but not after this. - if (rend is null) - rend = createRenderer (rendName); - popups = null; - - 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); - } - - final override 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; - ActivePopup[] popups; // Pop-up [menus] to draw. Last element is top popup. - ActivePopup[] popupsMem; // allocated memory for popups - // 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; // Content 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 +abstract scope class AWidgetManager : IWidgetManager { /** Construct a new widget loader. * @@ -503,6 +238,110 @@ requestRedraw; } + //BEGIN IParentWidget methods + // If call reaches the widget manager there isn't any recursion. + //NOTE: should be override + final void recursionCheck (widgetID) {} + //END IParentWidget 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. + } + + // 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; + } + + void addPopup (IChildWidget parnt, IChildWidget widg, int flags = 0) { + debug assert (parnt && widg, "addPopup: null widget"); + if (popups.length >= popupsMem.length) + popupsMem.length = popupsMem.length * 2 + 2; + with (popupsMem[popups.length]) { + parent = parnt; + widget = widg; + w = widg.width; + h = widg.height; + 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 + } + widget.setPosition (x, y); + } + popups = popupsMem[0..popups.length+1]; + requestRedraw; + } + void removePopup (IChildWidget parnt) { + foreach_reverse (i,popup; popups) { + if (popup.parent is parnt) + popups = popups[0..i]; + } + 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 loading the widgets. * @@ -525,24 +364,116 @@ void preSave (); public: - //BEGIN IWidgetManager methods - override IChildWidget makeWidget (widgetID id, IContent content = null) { - debug (mdeWidgets) logger.trace ("Creating widget \""~id~'"'); - return createWidget (this, id, curData[id], content); - } + //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, + SubMenu = TAKES_CONTENT | 0x12, + + // 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, + MenuButtonContent = TAKES_CONTENT | 0x44, + + GridLayout = TAKES_CONTENT | 0x100, + ContentList = TAKES_CONTENT | SAFE_RECURSION | 0x110, - override wdims dimData (widgetID id) { - return curData.dims (id); + FloatingArea = 0x200, +} + +// Only used for binarySearch algorithm generation; must be ordered by numerical values. +const char[][] WIDGETS = [ + "FixedBlank", + "SizableBlank", + "Debug", + "TextLabel", + "FloatingArea", + "addContent", + "PopupMenu", + "SubMenu", + "ContentLabel", + "DisplayContent", + "BoolContent", + "AStringContent", + "ButtonContent", + "MenuButtonContent", + "GridLayout", + "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; } - override void setData (widgetID id, WidgetData d) { - changes[id] = d; // also updates WidgetDataSet in data. +} + +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; } - override void setDimData (widgetID id, wdims d) { - changes.setDims(id, d); // also updates WidgetDataSet in data. - } - //END IWidgetManager methods + 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 @@ -555,10 +486,27 @@ 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; // 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 + struct ActivePopup { + IChildWidget widget; + IChildWidget parent; + wdabs x,y; + wdsize w,h; + } + ActivePopup[] popups; // Pop-up [menus] to draw. Last element is top popup. + ActivePopup[] popupsMem; // allocated memory for popups + // 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 + Mutex mutex; // lock on methods for use outside the package. }