Mercurial > projects > mde
view mde/gui/widget/layout.d @ 94:9520cc0448e5
Boolean options are now encapsulated within a Content class (currently an experiment).
This should facilitate generic option editing widgets.
author | Diggory Hardy <diggory.hardy@gmail.com> |
---|---|
date | Thu, 23 Oct 2008 17:45:49 +0100 |
parents | 08a4ae11454b |
children | 2a364c7d82c9 |
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/>. */ /// Gui layout widgets. module mde.gui.widget.layout; import mde.gui.widget.Widget; import mde.gui.exception; import mde.gui.widget.TextWidget; import mde.gui.content.options; import mde.gui.content.Content; import tango.util.container.HashMap; debug { import tango.util.log.Log : Log, Logger; private Logger logger; static this () { logger = Log.getLogger ("mde.gui.widget.layout"); } } /************************************************************************************************* * Encapsulates a grid of Widgets. * * Currently there is no support for changing number of cells, sub-widgets or sub-widget properties * (namely isW/HSizable and minimal size) after this() has run. * * Since a grid with either dimension zero is not useful, there must be at least one sub-widget. * * The grid has no border but has spacing between widgets. *************************************************************************************************/ class GridLayoutWidget : GridWidget { /** Constructor for a grid layout widget. * * Widget uses the initialisation data: * --- * ints = [widget_type, align_flags, rows, cols] * // or with column widths and row heights: * ints = [widget_type, align_flags, rows, cols, col1width, ..., colCwidth, row1height, ..., rowRheight] * strings = [w11, w12, ..., w1C, ..., wR1, ..., wRC] * --- * where R and C are the number of rows and columns, and wij is the ID (from parent Window's * list) for the widget in row i and column j. The number of parameters must be r*c + 3. * * The content parameter is passed on to all children accepting an IContent. */ this (IWidgetManager mgr, widgetID id, WidgetData data, IContent content) { // Get grid size and check data // Check sufficient data for type, align-flags, rows, cols, and possibly row/col widths. if (data.ints.length != 4) throw new WidgetDataException (this); rows = data.ints[2]; cols = data.ints[3]; // Check: at least one sub-widget, ints length == 3, strings' length is correct: if (rows < 1 || cols < 1 || data.ints.length != 4 || data.strings.length != rows * cols) throw new WidgetDataException (this); // Get all sub-widgets subWidgets.length = rows*cols; foreach (i, ref subWidget; subWidgets) { subWidget = mgr.makeWidget (data.strings[i], content); } initWidths = mgr.dimData (id); // may be null, tested later super (mgr, id, data); } // Save column/row sizes. Currently always do so. bool saveChanges () { foreach (widget; subWidgets) // recurse on subwidgets widget.saveChanges (); mgr.setDimData (id, col.width ~ row.width); return true; } protected: } /************************************************************************************************* * Trial layout of sub-widgets of one type only. *************************************************************************************************/ class TrialContentLayoutWidget : GridWidget { this (IWidgetManager mgr, widgetID id, WidgetData data) { debug scope (failure) logger.warn ("TrialContentLayoutWidget: failure"); WDCheck (data, 2, 2); OptionList optsList = new OptionList(data.strings[1]); cols = 1; if ((rows = optsList.list.length) > 0) { // Get all sub-widgets subWidgets.length = rows*cols; foreach (i, c; optsList.list) { subWidgets[i] = mgr.makeWidget (data.strings[0], c); } } else { rows = 1; subWidgets = [mgr.makeWidget (data.strings[0], new TextContent (data.strings[1], "Invalid Options section"))]; } super (mgr, id, data); } bool saveChanges () { // Since all sub-widgets have the same id, it only makes sense to call on one if (subWidgets is null) return false; return subWidgets[0].saveChanges; } private: OptionList optsList; } /************************************************************************************************* * Backend for grid-based (includes column/row) layout widgets. * * A deriving class must at least do some work in it's constructor (see Ddoc for this() below) * and provide an implementation of getCreationData() (unless Widget's version is sufficient). * * Since a grid with either dimension zero is not useful, there must be at least one sub-widget. * * The grid has no border but has spacing between widgets. *************************************************************************************************/ abstract class GridWidget : ParentWidget { //BEGIN Creation & saving /** Partial constructor for a grid layout widget. * * Deriving classes should check data lengths, and set rows, cols, and the subWidgets array, * before calling this super constructor. (If it's necessary to call super(...) first, * the call to genCachedConstructionData can be moved to the derived this() methods.) * * Derived constructors may also set initWidths to the array of column widths followed by * row heights used to initially set the row/column dimensions. * * Sub-widgets are finalized here, so no methods should be called on sub-widgets before calling * this super. */ protected this (IWidgetManager mgr, widgetID id, WidgetData data) { super (mgr, id, data); // Create cell aligners with appropriate col/row adjustment function if (data.ints[1] & 1) col = AlignColumns.getInstance (id, cols); else col = (new AlignColumns (cols)); col.addSetCallback (&setColWidth); if (data.ints[1] & 2) row = AlignColumns.getInstance (id~"R", rows); // id must be unique to that for cols! else row = (new AlignColumns (rows)); row.addSetCallback (&setRowHeight); } /** Prior to finalizing but after sub-widgets are finalized, some information needs to be * passed to the AlignColumns. */ void prefinalize () { genCachedConstructionData; // min widths, sizableness } /** Responsible for calculating the minimal size and initializing some stuff. * * As such, this must be the first function called after this(). */ void finalize () { if (initWidths.length == cols + rows) { col.setWidths (initWidths[0..cols]); row.setWidths (initWidths[cols..$]); } else { col.setWidths; row.setWidths; } initWidths = null; // free mw = col.mw; mh = row.mw; w = col.w; h = row.w; // Tell subwidgets their new sizes. Positions are given by a later call to setPosition. foreach (i,widget; subWidgets) { // Resizing direction is arbitrarily set to negative: widget.setWidth (col.width[i % cols], -1); widget.setHeight (row.width[i / cols], -1); } } //END Creation & saving //BEGIN Size & position bool isWSizable () { return col.firstSizable >= 0; } bool isHSizable () { return row.firstSizable >= 0; } void setWidth (wdim nw, int dir) { w = col.resizeWidth (nw, dir); // Note: setPosition must be called after! } void setHeight (wdim nh, int dir) { h = row.resizeWidth (nh, dir); // Note: setPosition must be called after! } void setPosition (wdim x, wdim y) { this.x = x; this.y = y; debug assert (col.pos && row.pos, "setPosition: col/row.pos not set (code error)"); foreach (i,widget; subWidgets) widget.setPosition (x + col.pos[i % cols], y + row.pos[i / cols]); } //END Size & position // Find the relevant widget. IChildWidget getWidget (wdim cx, wdim cy) { debug scope (failure) logger.warn ("getWidget: failure; values: click, pos, width - {}, {}, {} - {}, {}, {}", cx, x, w, cy, y, h); debug assert (cx >= x && cx < x + w && cy >= y && cy < y + h, "getWidget: not on widget (code error)"); // Find row/column: myDiff i = col.getCell (cx - x); myDiff j = row.getCell (cy - y); if (i < 0 || j < 0) // on a space between widgets return this; // On a subwidget; recurse call: return subWidgets[i + j*cols].getWidget (cx, cy); } // Resizing columns & rows void clickEvent (wdabs cx, wdabs cy, ubyte b, bool state) { debug scope (failure) logger.warn ("clickEvent: failure"); if (b == 1 && state == true) { /* Note: Because of getWidget, this function is only called if the click is not on a * sub-widget, so we know it's on some divisor (so at least one of resizeCol and * resizeRow is non-negative). */ // find col/row's resizeD & resizeU if (col.findResizeCols (cx - x) && row.findResizeCols (cy - y)) return; // unable to resize dragX = cx; dragY = cy; mgr.addClickCallback (&endCallback); mgr.addMotionCallback (&resizeCallback); } } void draw () { super.draw (); foreach (widget; subWidgets) widget.draw (); } private: //BEGIN Cache calculation functions /* Calculations which need to be run whenever a new sub-widget structure is set * (i.e. to produce cached data calculated from construction data). * Also need to be re-run if the renderer changes. * * rows, cols and subWidgets must be set before calling. Part of the set-up for AlignColumns * (col and row). subWidgets need to know their minimal size and resizability. */ void genCachedConstructionData () { // Will only change if renderer changes: // NOTE shared AlignColumns get this set by all sharing GridWidgets col.spacing = row.spacing = mgr.renderer.layoutSpacing; // Calculate the minimal column and row sizes: // AlignColumns (row, col) takes care of initializing minWidth. foreach (i,widget; subWidgets) { // Increase dimensions if current minimal size is larger: myIt n = i % cols; // column wdim md = widget.minWidth; if (col.minWidth[n] < md) col.minWidth[n] = md; n = i / cols; // row md = widget.minHeight; if (row.minWidth[n] < md) row.minWidth[n] = md; } // Find which cols/rows are resizable: // AlignColumns initializes sizable, and sets first and last sizables. forCols: for (myIt i = 0; i < cols; ++i) { // for each column for (myIt j = 0; j < subWidgets.length; j += cols) // for each row if (!subWidgets[i+j].isWSizable) // column not resizable continue forCols; // continue the outer for loop // column is resizable if we get to here col.sizable[i] = true; } forRows: for (myIt i = 0; i < subWidgets.length; i += cols) { // for each row for (myIt j = 0; j < cols; ++j) // for each column if (!subWidgets[i+j].isHSizable) continue forRows; row.sizable[i / cols] = true; } } //END Cache calculation functions void setColWidth (myIt i, wdim w, int dir) { for (myIt j = 0; j < rows; ++j) { subWidgets[i + cols*j].setWidth (w, dir); } } void setRowHeight (myIt j, wdim h, int dir) { for (myIt i = 0; i < cols; ++i) { subWidgets[i + cols*j].setHeight (h, dir); } } //BEGIN Col/row resizing callback void resizeCallback (wdim cx, wdim cy) { col.resizeCols (cx - dragX); row.resizeCols (cy - dragY); // NOTE: all adjustments are relative; might be better if they were absolute? dragX = cx; dragY = cy; foreach (i,widget; subWidgets) widget.setPosition (x + col.pos[i % cols], y + row.pos[i / cols]); mgr.requestRedraw; } bool endCallback (wdabs cx, wdabs cy, ubyte b, bool state) { if (b == 1 && state == false) { mgr.removeCallbacks (cast(void*) this); return true; // we've handled the up-click } return false; // we haven't handled it } protected: // Data for resizing cols/rows: wdim dragX, dragY; // coords where drag starts //END Col/row resizing callback myIt cols, rows; // number of cells in grid wdim[] initWidths; // see this / setInitialSize /* All widgets in the grid, by row. Order: [ 0 1 ] * [ 2 3 ] */ //IChildWidget[] subWidgets; AlignColumns col, row; } /************************************************************************************************** * Alignment device * * E.g. can control widths of columns within a grid, and provide sensible resizing, respecting the * minimal width required by each cell in a column. Is not restricted to horizontal widths, but to * ease descriptions, a horizontal context (column widths) is assumed. * * Cells should be of type IChildWidget. * * Cells are not directly interacted with, but minimal widths for each column are passed, and * callback functions are used to adjust the width of any column. *************************************************************************************************/ class AlignColumns { /** Instance returned will be shared with any other widgets of same widgetID. * * Also ensures each widget sharing an instance expects the same number of columns. */ static AlignColumns getInstance (widgetID id, myIt columns) { AlignColumns* p = id in instances; if (p) { if (p.minWidth.length != columns) throw new GuiException ("AlignColumns: no. of columns varies between sharing widgets (code error)"); return *p; } else { auto a = new AlignColumns (columns); instances[id] = a; return a; } } /** Create an instance. After creation, the number of columns can only be changed by calling * reset. * * After creation, minimal widths should be set for all columns (minWidth) and * setWidths must be called before other functions are used. */ this (myIt columns) { reset (columns); } /** Reset all column information (only keep set callbacks). * * Widths should be set after calling, as on creation. */ void reset (myIt columns) { if (columns < 1) throw new GuiException("AlignColumns: created with <1 column"); minWidth = new wdim[columns]; sizable = new bool[columns]; width = null; // enforce calling setWidths after this firstSizable = -1; lastSizable = -1; spare = 0; } /** Initialize widths as minimal widths. */ void setWidths () { if (!width) { width = minWidth.dup; initCalc; } } /** Initialize widths from supplied list, checking validity. */ void setWidths (wdim[] data) { if (!width) { // Set to provided data: width = data; // And check sizes are valid: foreach (i, m; minWidth) { // if fixed width or width is less than minimum: if (!sizable[i] || width[i] < m) { width[i] = m; } } initCalc; } } /** Add a callback to be called to notify changes in a column's width. * * All callbacks added are called on a width change so that multiple objects may share a * CellAlign object. */ typeof(this) addSetCallback (void delegate (myIt,wdim,int) setCW) { assert (setCW, "CellAlign.this: setCW is null (code error)"); setWidthCb ~= setCW; return this; } /** Get the row/column of relative position l. * * returns: * -i if in space to left of col i, or i if on col i, or -(num cols + 1) if in $(I spare) * space to right of last column. */ myDiff getCell (wdim l) { myDiff i = minWidth.length - 1; // starting from right... while (l < pos[i]) { // decrement while left of this column debug assert (i > 0, "getCell: l < pos[0] (code error)"); --i; } // now (l >= pos[i]) if (l >= pos[i] + width[i]) { // between columns or in spare space after last column debug assert (i+1 < minWidth.length || l < pos[i] + width[i] + spare, "getCell: l >= total width (code error)"); return -i - 1; // note: i might be 0 so cannot just return -i } return i; } /** Adjust total size with direction dir. * * nw should be at least the minimal width. */ wdim resizeWidth (wdim nw, int dir) { if (nw < mw) { debug logger.warn ("Widget dimension set below minimal"); nw = mw; } if (nw == w) return w; wdim diff = nw - w; if (firstSizable == -1) { spare += diff; w = nw; } else adjustCellSizes (diff, (dir == -1 ? lastSizable : firstSizable), dir); debug if (nw != w) { logger.trace ("resizeWidth to {} failed, new width: {}",nw,w); /+ Also print column widths & positions: logger.trace ("resizeWidth to {} failed! Column dimensions and positions:",nw); foreach (i,w; width) logger.trace ("\t{}\t{}", w,pos[i]);+/ } return w; } /** Calculate resizeU/resizeD, and return true if unable to resize. * * This and resizeCols are for moving dividers between cells. */ bool findResizeCols (wdim l) { resizeU = -getCell (l); // potential start for upward-resizes if (resizeU <= 0 || resizeU > minWidth.length) return true; // not on a space between cells or in spare space after last cell resizeD = resizeU - 1; // potential start for downward-resizes while (!sizable[resizeU]) { // find first actually resizable column (upwards) ++resizeU; if (resizeU >= minWidth.length) { // cannot resize resizeU = -1; return true; } } while (!sizable[resizeD]) { // find first actually resizable column (downwards) --resizeD; if (resizeD < 0) { // cannot resize resizeU = -1; // resizeU is tested to check whether resizes are possible return true; } } return false; // can resize } /// Resize columns based on findResizeCols void resizeCols (wdim diff) { if (resizeU <= 0) return; // do shrinking first (in case we hit the minimum) if (diff >= 0) { diff = -adjustCellSizes (-diff, resizeU, 1); adjustCellSizes (diff, resizeD, -1); } else { diff = -adjustCellSizes (diff, resizeD, -1); adjustCellSizes (diff, resizeU, 1); } } /** Intitialization triggered by setWidths. * * Calculates first/lastSizable from sizable, minimal width and positions. */ private void initCalc () { /* Calculate the minimal width of all columns plus spacing. */ mw = spacing * cast(wdim)(minWidth.length - 1); foreach (w; minWidth) mw += w; genPositions; foreach (i,s; sizable) { if (s) { firstSizable = i; goto gotFirst; } } return; // none resizable - don't search for lastSizable gotFirst: foreach_reverse (i,s; sizable) { if (s) { lastSizable = i; return; // done } } } /* Generate position infomation for each column and set w. */ private void genPositions () { pos.length = minWidth.length; w = 0; foreach (i, cw; width) { pos[i] = w; w += cw + spacing; } w -= spacing; } /* Adjust the total size of rows/columns (including spacing) by diff. * * Params: * diff = amount to increase/decrease the total size * start= index for col/row to start resizing on * incr = direction to resize in (added to index each step). Must be either -1 or +1. * * Returns: * The amount adjusted. This may be larger than diff, since cellD is clamped by cellDMin. * * Note: Check variable used for start is valid before calling! If a non-sizable column's * index is passed, this should get increased (if diff > 0) but not decreased. */ private wdim adjustCellSizes (wdim diff, myDiff start, int incr) in { assert (width.length == minWidth.length, "CellAlign.adjustCellSizes: width is null (code error)"); // Most likely if passed negative when sizing is disabled: assert (start >= 0 && start < minWidth.length, "adjustCellSizes: invalid start"); debug assert (incr == 1 || incr == -1, "adjustCellSizes: invalid incr"); } body { debug scope(failure) logger.trace ("adjustCellSizes: failure"); myDiff i = start; if (diff > 0) { // increase size of first resizable cell width[i] += diff; foreach (dg; setWidthCb) dg(i, width[i], incr); } else if (diff < 0) { // decrease wdim rd = diff; // running diff aCSwhile: while (true) { width[i] += rd; // decrease this cell's size (but may be too much) rd = width[i] - minWidth[i]; if (rd >= 0) { // OK; we're done foreach (dg; setWidthCb) dg(i, width[i], incr); break; // we hit the mark exactly: diff is correct } // else we decreased it too much! width[i] = minWidth[i]; foreach (dg; setWidthCb) dg(i, width[i], incr); // rd is remainder to decrease by bool it = true; // iterate (force first time) while (it) { i += incr; if (i < 0 || i >= minWidth.length) { // run out of next cells diff -= rd; // still had rd left to decrease break aCSwhile; // exception: Array index out of bounds } it = !sizable[i]; // iterate again if row/col isn't resizable } } } // else no adjustment needed (diff == 0) genPositions; return diff; } /** Minimal width for each column. * * Initialized to zero. Each class using this AlignColumns should, for each column, increase * this value to the maximum of the minimal widths (in other words, set * minWidth[i] = max(minWidth[i], cell.minWidth) for each cell in column i). */ wdim[] minWidth; // minimal widths (set by genCachedConstructionData) /** For each column i, sizable[i] is true if that column is resizable. * * Set along with minWidth before calling setWidths. */ bool[] sizable; // set by genCachedConstructionData /** Current width, relative position (for each column) * * Treat as READ ONLY! */ wdim[] width; // only adjusted within the class wdim[] pos; /// ditto protected: myDiff resizeD, // resizeCols works down from this index (<0 if not resizing) resizeU; // and up from this index wdim spacing; // used by genPositions (which cannot access the layout class's data) wdim spare; // fixed size only: extra blank space filler wdim w,mw; // current & minimal widths /* indicies of the first/last resizable column (negative if none are resizable). */ myDiff firstSizable = -1, lastSizable = -1; // set by calcFLSbl // Callbacks used to actually adjust a column's width: void delegate (myIt,wdim,int) setWidthCb[]; // set width of a column, with resize direction static HashMap!(widgetID,AlignColumns) instances; static this () { instances = new HashMap!(widgetID,AlignColumns); } } // Index types. Note that in some cases they need to hold negative values. // int is used for resizing direction (although ptrdiff_t would be more appropriate), // since the value must always be -1 or +1. alias size_t myIt; alias ptrdiff_t myDiff;