/* * UILayout.jsx * Author: Peter Torpey * www.petertorpey.com * Version: 1.3 01-26-05,10-26-04 * Original code Copyright 2004-2005 Peter Torpey. * This code may be used, distributed, and modified for personal use * provided the name and URL of the original author, above, remain * intact. * * If this code is used in the creation of a notable production, * please let the author know. The author's e-mail address may be * found at http://www.petertorpey.com/contact.html . * * Description: * A rudimentary layout manager for Adobe JavaScript UIs. * * Usage: * Include this file in your script. * Using this layout manager, you needn't concern yourself with specifying * meaningful bounds for UI components as they are added. * Prior to calling show() (or center()) on the Window object to layout, call * layoutUI() passing a reference to the Window object. This will lay out * the component and its children. * This script adds properties to several standard UI components that offer * control over the layout. * If you do wish to explicitly specify bounds for a UI component that is in * a container that is otherwise being layed out, set the component * objects "dontLayout" property to "true". * To specify a custom layout for a container and its children, define a * "layoutUI()" member function that can take a UI component object as a * parameter. This function can then layout the component itself and its * children. The function should return a Bounds object representing its * size after layout. If the component should not be layed out with * respect to its container and siblings, return "null". * Window and Panel objects have a new method addUI() which takes the same * parameters as "add()" with the exception of the Bounds object. For * example: .addUI("button", "Button Label", {name:"ok"}) ** As of 1.3: addUI() will accept a type "break" which will break the * current flow. For example: .addUI("break"). ** As of 1.3: addUI() will accept a type "labeledittext" which will add * an empty EditText field with a label of the given text in one step. ** As of 1.3: addUI() can take an optional fourth argument which is the * the preferred width of the component, overriding the default width. * This applies to any UI component other than a container and is subject * to the setting of "layoutFill" for that component. * Window and Panel objects also have "layoutAlign" and "layoutFlow" * properties that may be set to one of the "UILM_ALIGN" or "UILM_FLOW" * constants (see below). * All UI objects have "layoutFill" properties that may be set to one of the * "UILM_FILL" constants (see below). ** As of 1.3: UILM_FILL_EXPAND will fill the remaining width of a * container for most components (excluding cotainers). ** As of 1.3: The StaticText and Button components will respond to * "layoutFill" set to "UILM_FILL_SHRINK" with a crude approximation * of the string's display length using a proportional font. * * Known Issues: * Default sizes have not been tested in other configurations. * Tested on AE 6.5, WinXP, Themes enabled, default sizes, Western. * UILM_ALIGN_CENTER is not implemented. * UILM_FILL_EXPAND does not account for components already in the current * line of the flow. We need to make multiple passes for this to work. ** As of 1.3: UILM_FILL_EXPAND will fill the remaining width of a * container for most components (excluding Panel). * Creation mechanism for UIFlowBreak is not implemented. ** As of 1.3: addUI() will accept a type "break" which will break the * current flow. * Bugs in Adobe's UI implementation require different means for specifying * bounds in different places. It seems as though components' positions * are stored in absolute (Window) coordinates, even though accessed by * relative coordinates. * Sliders and Scrollbars do not have their bounds set... haven't used a * a scrollbar yet to see how they work. * There is no facility to shrink leaf components to fit their text. ** As of 1.3: The StaticText and Button components will respond to * "layoutFill" set to "UILM_FILL_SHRINK" with a crude approximation * of the string's display length using a proportional font. The default * assumes a standard system font. Since there is no way for the script * to determine the font size being used, adjust * UILM_PROPORTIONAL_FONT_AVERAGE to be the average number of pixels in * in width of a glyph for your system, system font, and font size. For * fixed-width system fonts, this value may be the font size in pixels. * There is no facility for vertical alignment. Baseline alignment for * StaticText labels would be nice. ** As of 1.3: StaticText is assumed to have a raised baseline. Since there * is no way for a script to determine the system font metrics, adjust * UILM_D_TEXT_BASELINE to be a pleasing baseline height in pixels for * your system, system font, and font size. * RIGHT aligned flows need to be specified in right-to-left order. * * For the Future: * This may become an object with "static" members or a member of UI * containers. * */ // defaults (may be implemented as properties of each UI object prototype) var UILM_D_PANEL_MARGIN_V = 8; var UILM_D_PANEL_MARGIN_H = 8; var UILM_D_PANEL_PADDING_TOPTEXT = 14; var UILM_D_PANEL_PADDING_V = 5; var UILM_D_PANEL_PADDING_H = 8; var UILM_D_ELEMENT_PADDING = 6; var UILM_D_BUTTON_HEIGHT = 26; var UILM_D_BUTTON_WIDTH; // from Adobe DemoPalette.jsx if (system.osName.indexOf("Windows") != -1) { // Windows system has narrower buttons. UILM_D_BUTTON_WIDTH = 105; } else { // Mac has wider buttons UILM_D_BUTTON_WIDTH = 120; } if (app.language == Language.JAPANESE) { // Add 20 pixels for japanese machines, default font is wider UILM_D_BUTTON_WIDTH += 17; } var UILM_DISABLE_STRING_DISPLAY_WIDTH_GUESS = false; var UILM_PROPORTIONAL_FONT_AVERAGE = 6.5; var UILM_D_TEXT_BASELINE = 3; var UILM_D_CHECKBOX_HEIGHT = 26; var UILM_D_TEXT_HEIGHT = 16; var UILM_D_EDIT_TEXT_HEIGHT = 20; var UILM_D_TEXT_WIDTH = 120; var UILM_D_SLIDER_HEIGHT = 20; var UILM_D_SLIDER_WIDTH = 200; // constants and enums // child alignment in container var UILM_ALIGN_LEFT = 1; var UILM_ALIGN_CENTER = 2; var UILM_ALIGN_RIGHT = 4; // container size var UILM_FILL_IGNORE = 1; // doesn't calculate var UILM_FILL_SHRINK = 2; // container is only big enough to hold children var UILM_FILL_EXPAND = 4; // container grows to fit available space within its parent container // controls flow of a container's children var UILM_FLOW_RIGHT = 1; // elements are laid out left to right, ad infinitum var UILM_FLOW_WRAP = 2; // elements that don't fit in a line wrap to a new line var UILM_FLOW_DOWN = 4; // elements are laid out top to bottom /* // Future constructor of LayoutManager with "static" method layoutUI() // or make layoutUI an overide-able member of every UI class? function LayoutManager() { } // "Static" Objects to expose enumerated types... // Maybe these types should just be members of LayoutManager. // For now, we'll use the global constants. function LayoutManagerAlign() { this.LEFT = UILM_ALIGN_LEFT; this.CENTER = UILM_ALIGN_CENTER; this.RIGHT = UILM_ALIGN_RIGHT; } function LayoutManagerFill() { this.IGNORE = UILM_FILL_IGNORE; this.SHRINK = UILM_FILL_SHRINK; this.EXPAND = UILM_FILL_EXPAND; } function LayoutManagerFlow() { this.RIGHT = UILM_FLOW_RIGHT; this.WRAP = UILM_FLOW_WRAP; this.DOWN = UILM_FLOW_DOWN; } */ // Need to allow for changing the default sizes. Since we're currently using // global constants, these can just be reassigned. function layoutUI(uiobj, offset) { if (uiobj.dontLayout) { return uiobj.bounds; } // Just in case offset is not passed and we're expecting it to be. if (!offset) offset = UILM_D_PANEL_PADDING_H + UILM_D_PANEL_PADDING_H; // buttons if (uiobj.type == "button") { var tw = uiobj.parent.bounds.width - offset; var pw = ((uiobj.layoutPreferredWidth) ? uiobj.layoutPreferredWidth : UILM_D_BUTTON_WIDTH); uiobj.bounds = [0, 0, ((uiobj.layoutFill == UILM_FILL_EXPAND) ? tw : ((uiobj.layoutFill == UILM_FILL_SHRINK) ? Math.min(getStringDisplayWidth(uiobj.text) + UILM_D_ELEMENT_PADDING, tw) : pw)), UILM_D_BUTTON_HEIGHT]; } // checkboxes, and radiobuttons else if (uiobj.type == "checkbox" || uiobj.type == "radiobutton") { var pw = ((uiobj.layoutPreferredWidth) ? uiobj.layoutPreferredWidth : UILM_D_BUTTON_WIDTH); uiobj.bounds = [0, 0, ((uiobj.layoutFill == UILM_FILL_EXPAND) ? uiobj.parent.bounds.width - offset : pw), UILM_D_CHECKBOX_HEIGHT]; } // text elements else if (uiobj.type == "statictext") { var tw = uiobj.parent.bounds.width - offset; var pw = ((uiobj.layoutPreferredWidth) ? uiobj.layoutPreferredWidth : UILM_D_TEXT_WIDTH); var th = UILM_D_TEXT_HEIGHT var lines = uiobj.text.split(/\n|\r|\r\n/).length; if (lines > 1) th *= lines; uiobj.bounds = [0, 0, ((uiobj.layoutFill == UILM_FILL_EXPAND) ? tw : ((uiobj.layoutFill == UILM_FILL_SHRINK) ? Math.min(getStringDisplayWidth(uiobj.text), tw) : pw)), th]; } else if (uiobj.type == "edittext") { var pw = ((uiobj.layoutPreferredWidth) ? uiobj.layoutPreferredWidth : UILM_D_TEXT_WIDTH); uiobj.bounds = [0, 0, ((uiobj.layoutFill == UILM_FILL_EXPAND) ? uiobj.parent.bounds.width - offset : pw), UILM_D_EDIT_TEXT_HEIGHT]; } // scrollbar??? else if (uiobj.type == "scrollbar") { } // slider else if (uiobj.type == "slider") { var pw = ((uiobj.layoutPreferredWidth) ? uiobj.layoutPreferredWidth : UILM_D_SLIDER_WIDTH); uiobj.bounds = [0, 0, ((uiobj.layoutFill == UILM_FILL_EXPAND) ? uiobj.parent.bounds.width - offset : pw), UILM_D_SLIDER_HEIGHT]; } // panel (and window, i.e. containers) else if (uiobj.type == "panel" || uiobj.type == "dialog" || uiobj.type == "palette") { // if EXPAND, set width if (uiobj.layoutFill == UILM_FILL_EXPAND && uiobj.parent) { // uiobj.bounds.width = uiobj.parent.width - UILM_D_PANEL_MARGIN_H - UILM_D_PANEL_MARGIN_H; uiobj.bounds = [uiobj.bounds.left, uiobj.bounds.top, uiobj.parent.bounds.width - UILM_D_PANEL_MARGIN_H - UILM_D_PANEL_MARGIN_H, uiobj.bounds.height]; } // recursively layout children var uichildren = uiobj.children, child; var l = UILM_D_PANEL_PADDING_H, t = ((uiobj.type == "panel" && uiobj.text) ? UILM_D_PANEL_PADDING_TOPTEXT : UILM_D_PANEL_PADDING_V); var maxh = 0, maxw = 0; var cb, cl; // CENTER alignment not implemented for (var i = 0; i < uichildren.length; i++) { child = uichildren[i]; // if (child.type == "uiflowbreak") { // Kludge to implement breaking. if (child.type == "statictext" && child._layoutBreak) { t += Math.max(maxh, child.height) + UILM_D_ELEMENT_PADDING; l = UILM_D_PANEL_PADDING_H; maxh = 0; } else { if (child.dontlayout) { cb = child.bounds; } else if (typeof child.layoutUI == "function") { cb = child.layoutUI(child); // element has a custom layout function to size and position if (cb == null || !(cb instanceof Bounds)) { // If an element has it's own layout function and returns null, // we don't reposition it or include it in the flow. // If it does return (a Bounds object), then we'll position it and include it in the flow. continue; // move on to next child } } else { cb = layoutUI(child, l + UILM_D_ELEMENT_PADDING + UILM_D_PANEL_PADDING_H); } if (uiobj.layoutFlow == UILM_FLOW_DOWN) { cl = ((uiobj.layoutAlign == UILM_ALIGN_RIGHT) ? uiobj.bounds.width - l - cb.width : l); // child.bounds = [cl, t, cl + cb.width, t + cb.height]; // bug: setting bounds(x,y) offsets children child.bounds.right = cl + cb.width; child.bounds.bottom = t + cb.height; child.bounds.left = cl; child.bounds.top = t; maxh = cb.height; t += maxh + UILM_D_ELEMENT_PADDING; } else if (uiobj.layoutFlow == UILM_FLOW_WRAP) { var lineLen = cb.width + UILM_D_ELEMENT_PADDING + l + UILM_D_PANEL_PADDING_H; if (lineLen > uiobj.bounds.width) { t += maxh + UILM_D_ELEMENT_PADDING; l = UILM_D_PANEL_PADDING_H; maxh = cb.height; } else { maxh = Math.max(maxh, cb.height); } cl = ((uiobj.layoutAlign == UILM_ALIGN_RIGHT) ? uiobj.bounds.width - l - cb.width : l); // child.bounds = [cl, t, cl + cb.width, t + cb.height]; // bug: setting bounds(x,y) offsets children child.bounds.right = cl + cb.width; child.bounds.bottom = t + cb.height; child.bounds.left = cl; child.bounds.top = t; if (child.type == "statictext") child.bounds.top += UILM_D_TEXT_BASELINE; if (lineLen == uiobj.bounds.width) { t += maxh + UILM_D_ELEMENT_PADDING; l = UILM_D_PANEL_PADDING_H; maxh = 0; } else { l += cb.width + UILM_D_ELEMENT_PADDING; } } else { // UILM_FLOW_RIGHT cl = ((uiobj.layoutAlign == UILM_ALIGN_RIGHT) ? uiobj.bounds.width - l - cb.width : l); // child.bounds = [cl, t, cl + cb.width, t + cb.height]; // bug: setting bounds(x,y) offsets children child.bounds.right = cl + cb.width; child.bounds.bottom = t + cb.height; child.bounds.left = cl; child.bounds.top = t; if (child.type == "statictext") child.bounds.top += UILM_D_TEXT_BASELINE; l += cb.width + UILM_D_ELEMENT_PADDING; maxh = Math.max(maxh, cb.height); } maxw = Math.max(maxw, l); } } // if SHRINK, set width if (uiobj.layoutFill == UILM_FILL_SHRINK) { uiobj.bounds.width = maxw; // uiobj.bounds = [uiobj.bounds.left, uiobj.bounds.top, maxw, uiobj.bounds.height]; } // if not ignore, set height if (uiobj.layoutFill != UILM_FILL_IGNORE) { uiobj.bounds.height = t + ((uiobj.layoutFlow == UILM_FLOW_DOWN) ? 0 : maxh) + UILM_D_PANEL_PADDING_V; // uiobj.bounds = [uiobj.bounds.left, uiobj.bounds.top, uiobj.bounds.width, t + maxh + UILM_D_PANEL_PADDING_V + UILM_D_PANEL_PADDING_V]; // maxh = Math.max(maxh, uiobj.bounds.height); } } /* dialog window, for now same implementation as panel else if (uiobj.type == "dialog" || uiobj.type == "palette") { } */ return uiobj.bounds; } // if Western, guess at proportional string length in px; function getStringDisplayWidth(str) { if (UILM_DISABLE_STRING_DISPLAY_WIDTH_GUESS) return UILM_D_TEXT_WIDTH; return Math.round(str.length * UILM_PROPORTIONAL_FONT_AVERAGE); } // define a UI element UIFlowBreak // add to a container to deliberately break the flow /* function UIFlowBreak () { this.type = "uiflowbreak"; this.visible = false; this.bounds = [0, 0, 0, UILM_D_BUTTON_HEIGHT]; } */ // add the UIFlowBreak type to Window.add() // Will this actually work? Can we call the super-prototypes add() version? // Will the existing implementation complain about the unknown element? // If so, can implement as a Panel. /* function UILayoutWindowAdd() { if (this[0] == "uiflowbreak") { this.children[this.children.length] = new UIFlowBreak(); } else { this.prototype.add(); } } */ function UILayoutWindowAdd2(sType, sName, oProps, iWidth) { var rv; if (sType == "labeledittext") { rv = this.add("statictext", [0, 0, 10, 10], sName); if (rv) rv.justify = "right"; rv = this.add("edittext", [0, 0, 10, 10], "", oProps); if (rv && iWidth) rv.layoutPreferredWidth = iWidth; return rv; } if (sType == "break") { // Kludge to implement breaking. rv = this.add("statictext", [0, 0, 0, UILM_D_BUTTON_HEIGHT]); if (rv) { rv.visible = false; rv._layoutBreak = true; rv.height = UILM_D_BUTTON_HEIGHT; // fix for undefined height after creation... why? } return rv; } rv = this.add(sType, [0, 0, 10, 10], sName, oProps); if (rv && iWidth) rv.layoutPreferredWidth = iWidth; return rv; } /* Fix to force UI classes to exist before used. */ if (typeof Panel == "undefined") { var o_Win = new Window("dialog", "", [0, 0, 0, 0]); var o_Pnl = o_Win.add("panel"); o_Pnl.add("checkbox"); o_Pnl.add("statictext"); } Window.prototype.addUI = UILayoutWindowAdd2; Panel.prototype.addUI = UILayoutWindowAdd2; // add default layout values to UI elements Window.prototype.layoutFill = UILM_FILL_EXPAND; Window.prototype.layoutAlign = UILM_ALIGN_RIGHT; Window.prototype.layoutFlow = UILM_FLOW_WRAP; // This creates loops during layout and doesn't properly layout. // The moral of this story is that the Windows aren't user-resizable for a reason. // Window.prototype.onResize = function () {layoutUI(this);}; Panel.prototype.layoutFill = UILM_FILL_EXPAND; Panel.prototype.layoutAlign = UILM_ALIGN_LEFT; Panel.prototype.layoutFlow = UILM_FLOW_WRAP; Checkbox.prototype.layoutFill = UILM_FILL_EXPAND; StaticText.prototype.layoutFill = UILM_FILL_SHRINK; // Kludge to implement breaking. StaticText.prototype._layoutBreak = false;