Mercurial > projects > mde
view mde/gui/widget/layout.d @ 90:b525ff28774b
Widgets generated dynamically from a list can now be standard widgets selected from data files.
Started on allowing alignment to be shared between instances of a layout widget in a dynamic list (to allow column alignment of list's rows).
author | Diggory Hardy <diggory.hardy@gmail.com> |
---|---|
date | Wed, 01 Oct 2008 23:37:51 +0100 |
parents | 56c0ddd90193 |
children | 4d5d53e4f881 |
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; 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: * [widgetID, r, c, 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, WidgetData data, IContent content) { // Get grid size and check data // Check sufficient data for rows, cols, and possibly row/col widths. if (data.ints.length < 3) throw new WidgetDataException; rows = data.ints[1]; cols = data.ints[2]; // Check: at least one sub-widget, ints length == 3 or also contains row & col widths, // strings' length is correct: if (rows < 1 || cols < 1 || (data.ints.length != 3 && data.ints.length != 3 + rows + cols) || data.strings.length != rows * cols) throw new WidgetDataException; this.data = data; // Get all sub-widgets subWidgets.length = rows*cols; foreach (i, ref subWidget; subWidgets) { subWidget = mgr.makeWidget (data.strings[i], content); } super (mgr, data); if (data.ints.length == 3 + rows + cols) { col.setWidths (cast(wdim[]) data.ints[3..cols+3]); row.setWidths (cast(wdim[]) data.ints[cols+3..$]); } else { col.setWidths; row.setWidths; } adjustCache; } // Save column/row sizes. Currently always do so. bool saveChanges (widgetID id) { with (data) { foreach (i, widget; subWidgets) // recurse on subwidgets widget.saveChanges (strings[i]); ints = ints[0..3] ~ cast(int[])col.width ~ cast(int[])row.width; } mgr.setData (id, data); return true; } protected: WidgetData data; } /************************************************************************************************* * Trial layout of sub-widgets of one type only. *************************************************************************************************/ class TrialContentLayoutWidget : GridWidget { this (IWidgetManager mgr, WidgetData data) { debug scope (failure) logger.warn ("TrialContentLayoutWidget: failure"); WDCheck (data, 1, 1); OptionList optsList = OptionList.trial(); rows = optsList.list.length; cols = 1; // Get all sub-widgets subWidgets.length = rows*cols; foreach (i, c; optsList.list) { subWidgets[i] = mgr.makeWidget (data.strings[0], c); } super (mgr, data); // Set col/row widths to minimals. col.setWidths; row.setWidths; adjustCache; } 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 : Widget { //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 should call setWidths on col and row, and then call * adjustCache, after calling this. */ protected this (IWidgetManager mgr, WidgetData data) { super (mgr, data); // Create cell aligners with appropriate col/row adjustment function col = (new AlignColumns (cols)).addSetCallback (&setColWidth); row = (new AlignColumns (rows)).addSetCallback (&setRowHeight); // Calculate cached construction data genCachedConstructionData; } /** Generates cached mutable data. * * Should be called by adjust() after calling setWidths. */ void adjustCache () { // Generate cached mutable data // Calculate column and row locations: w = col.genPositions; h = row.genPositions; // 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) { if (nw == w) return; w += col.adjustCellSizes (nw - w, (dir == -1 ? col.lastSizable : col.firstSizable), dir); // Note: setPosition must be called after! } void setHeight (wdim nh, int dir) { if (nh == h) return; h += row.adjustCellSizes (nh - h, (dir == -1 ? row.lastSizable : row.firstSizable), dir); // Note: setPosition must be called after! } void setPosition (wdim x, wdim y) { this.x = x; this.y = y; 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"); // 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.findResize (cx - x) && row.findResize (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. */ void genCachedConstructionData () { // Will only change if renderer changes: col.spacing = row.spacing = mgr.renderer.layoutSpacing; // Calculate the minimal column and row sizes: // set length, making sure the arrays are initialised to zero: col.minWidth = new wdim[cols]; row.minWidth = new wdim[rows]; 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; } // Calculate the overall minimal size, starting with the spacing: mh = mgr.renderer.layoutSpacing; // use mh temporarily mw = mh * cast(wdim)(cols - 1); mh *= cast(wdim)(rows - 1); foreach (x; col.minWidth) // add the column/row's dimensions mw += x; foreach (x; row.minWidth) mh += x; // Find which cols/rows are resizable: // reset: col.sizable = new bool[cols]; row.sizable = new bool[rows]; col.firstSizable = row.firstSizable = -1; 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; if (col.firstSizable < 0) col.firstSizable = i; col.lastSizable = i; } 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.lastSizable = i / cols; row.sizable[row.lastSizable] = true; if (row.firstSizable < 0) row.firstSizable = row.lastSizable; } } //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.resize (cx - dragX); row.resize (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 /* All widgets in the grid, by row. Order: [ 0 1 ] * [ 2 3 ] */ IChildWidget[] subWidgets; AlignColumns col, row; } /// Position information on top of widths. //FIXME - merge classes back together? class AlignColumns : AlignWidths { /// See AlignWidths.this this (myIt columns) { super (columns); } /** 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; } /** Generate position infomation and return total width of columns. */ wdim genPositions () { pos.length = minWidth.length; wdim x = 0; foreach (i, w; width) { pos[i] = x; x += w + spacing; } return x - spacing; } // Get the row/column a click occured in // Returns -i if in space to left of col i, or i if on col i 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 debug assert (i+1 < minWidth.length, "getCell: l >= pos[$-1] + width[$-1] (code error)"); return -i - 1; // note: i might be 0 so cannot just return -i } return i; } // Calculate resizeU/resizeD, and return true if unable to resize. bool findResize (wdim l) { resizeU = -getCell (l); // potential start for upward-resizes if (resizeU <= 0) return true; // not on a space between cells 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 } /* 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. */ wdim adjustCellSizes (wdim diff, myDiff start, int incr) in { assert (width, "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; } void resize (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); } } private: wdim[] pos; // relative position (cumulative width[i-1] plus spacing) wdim spacing; // used by genPositions (which cannot access the layout class's data) // Callbacks used to actually adjust a column's width: void delegate (myIt,wdim,int) setWidthCb[]; // set width of a column, with resize direction } /************************************************************************************************** * 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. * * FIXME: * 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. *************************************************************************************************/ //FIXME: how to set cell positions after resizes? /+ FIXME - remove position information from here, removing need of horizontal alignment of cells into columns. On resizing, don't permanently adjust sizes until callback ends (mouse button released). Until then, always readjust from current "permanent" size. +/ class AlignWidths { /** 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("AlignWidths: created with <1 column"); minWidth = new wdim[columns]; width = null; // enforce calling setWidths after this } /** Initialize widths as minimal widths. */ void setWidths () { width = minWidth.dup; } /** Initialize widths from supplied list, checking validity. */ void setWidths (wdim[] data) { // 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; } /** Minimal width for each column. * * Initialized to zero. Each class using this AlignWidths 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. * firstSizable and lastSizable are the indicies of the first/last resizable column (negative * if none are resizable). * * Set along with minWidth before calling setWidths. */ bool[] sizable; // set by genCachedConstructionData /// ditto myDiff firstSizable, lastSizable; // set by genCachedConstructionData /** Current width for each column. * * Treat as READ ONLY! */ wdim[] width; // only adjusted within the class protected: myDiff resizeD, // resize down from this index (<0 if not resizing) resizeU; // and up from this index } // 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;