Import Horde_Block from CVS HEAD
authorMichael M Slusarz <slusarz@curecanti.org>
Wed, 2 Dec 2009 01:36:22 +0000 (18:36 -0700)
committerMichael M Slusarz <slusarz@curecanti.org>
Wed, 2 Dec 2009 01:36:22 +0000 (18:36 -0700)
framework/Block/lib/Horde/Block.php [new file with mode: 0644]
framework/Block/lib/Horde/Block/Collection.php [new file with mode: 0644]
framework/Block/lib/Horde/Block/Layout.php [new file with mode: 0644]
framework/Block/lib/Horde/Block/Layout/Manager.php [new file with mode: 0644]
framework/Block/lib/Horde/Block/Layout/View.php [new file with mode: 0644]
framework/Block/lib/Horde/Block/Ui.php [new file with mode: 0644]
framework/Block/package.xml [new file with mode: 0644]

diff --git a/framework/Block/lib/Horde/Block.php b/framework/Block/lib/Horde/Block.php
new file mode 100644 (file)
index 0000000..9124415
--- /dev/null
@@ -0,0 +1,235 @@
+<?php
+/**
+ * The abstract Horde_Block:: class represents a single block within
+ * the Blocks framework.
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author   Mike Cochrane <mike@graftonhall.co.nz>
+ * @author   Jan Schneider <jan@horde.org>
+ * @category Horde
+ * @package  Horde_Block
+ */
+class Horde_Block
+{
+    /**
+     * Whether this block has changing content.
+     *
+     * @var boolean
+     */
+    public $updateable = false;
+
+    /**
+     * Block specific parameters.
+     *
+     * @var array
+     */
+    protected $_params = array();
+
+    /**
+     * The Block row.
+     *
+     * @var integer
+     */
+    protected $_row;
+
+    /**
+     * The Block column.
+     *
+     * @var integer
+     */
+    protected $_col;
+
+    /**
+     * Application that this block originated from.
+     *
+     * @var string
+     */
+    protected $_app;
+
+    /**
+     * Constructor.
+     *
+     * @param array|boolean $params  Any parameters the block needs. If false,
+     *                               the default parameter will be used.
+     * @param integer $row           The block row.
+     * @param integer $col           The block column.
+     */
+    public function __construct($params = array(), $row = null, $col = null)
+    {
+        // @todo: we can't simply merge the default values and stored values
+        // because empty parameter values are not stored at all, so they would
+        // always be overwritten by the defaults.
+        if ($params === false) {
+            $params = $this->getParams();
+            foreach ($params as $name => $param) {
+                $this->_params[$name] = $param['default'];
+            }
+        } else {
+            $this->_params = $params;
+        }
+        $this->_row = $row;
+        $this->_col = $col;
+    }
+
+    /**
+     * Returns the application that this block belongs to.
+     *
+     * @return string  The application name.
+     */
+    public function getApp()
+    {
+        return $this->_app;
+    }
+
+    /**
+     * Returns any settable parameters for this block.
+     * It does *not* reference $this->_params; that is for runtime
+     * parameters (the choices made from these options).
+     *
+     * @return array  The block's configurable parameters.
+     */
+    static public function getParams()
+    {
+        /* Switch application contexts, if necessary. Return an error
+         * immediately if pushApp() fails. */
+        try {
+            $app_pushed = $GLOBALS['registry']->pushApp($this->_app);
+        } catch (Horde_Exception $e) {
+            return $e->getMessage();
+        }
+
+        $params = $this->_params();
+
+        /* If we changed application context in the course of this
+         * call, undo that change now. */
+        if ($app_pushed === true) {
+            $GLOBALS['registry']->popApp();
+        }
+
+        return $params;
+    }
+
+    /**
+     * Returns the text to go in the title of this block.
+     *
+     * This function handles the changing of current application as
+     * needed so code is executed in the scope of the application the
+     * block originated from.
+     *
+     * @return string  The block's title.
+     */
+    public function getTitle()
+    {
+        /* Switch application contexts, if necessary. Return an error
+         * immediately if pushApp() fails. */
+        try {
+            $app_pushed = $GLOBALS['registry']->pushApp($this->_app);
+        } catch (Horde_Exception $e) {
+            return $e->getMessage();
+        }
+
+        $title = $this->_title();
+
+        /* If we changed application context in the course of this
+         * call, undo that change now. */
+        if ($app_pushed === true) {
+            $GLOBALS['registry']->popApp();
+        }
+
+        return $title;
+    }
+
+    /**
+     * Returns the content for this block.
+     *
+     * This function handles the changing of current application as
+     * needed so code is executed in the scope of the application the
+     * block originated from.
+     *
+     * @return string  The block's content.
+     */
+    public function getContent()
+    {
+        /* Switch application contexts, if necessary. Return an error
+         * immediately if pushApp() fails. */
+        try {
+            $app_pushed = $GLOBALS['registry']->pushApp($this->_app);
+        } catch (Horde_Exception $e) {
+            return $e->getMessage();
+        }
+
+        $content = $this->_content();
+
+        /* If we changed application context in the course of this
+         * call, undo that change now. */
+        if ($app_pushed === true) {
+            $GLOBALS['registry']->popApp();
+        }
+
+        return $content;
+    }
+
+    /**
+     * TODO
+     */
+    public function buildTree($tree, $indent = 0, $parent = null)
+    {
+        /* Switch application contexts, if necessary. Return an error
+         * immediately if pushApp() fails. */
+        try {
+            $app_pushed = $GLOBALS['registry']->pushApp($this->_app, array('logintasks' => false));
+        } catch (Horde_Exception $e) {
+            return $e->getMessage();
+        }
+
+        $this->_buildTree($tree, $indent, $parent);
+
+        /* If we changed application context in the course of this
+         * call, undo that change now. */
+        if ($app_pushed === true) {
+            $GLOBALS['registry']->popApp();
+        }
+    }
+
+    /**
+     * Returns the title to go in this block.
+     *
+     * @return string  The block title.
+     */
+    protected function _title()
+    {
+        return '';
+    }
+
+    /**
+     * Returns the parameters needed by block.
+     *
+     * @return array  The block's parameters.
+     */
+    protected function _params()
+    {
+        return array();
+    }
+
+    /**
+     * Returns this block's content.
+     *
+     * @return string  The block's content.
+     */
+    protected function _content()
+    {
+        return '';
+    }
+
+    /**
+     * Returns this block's content.
+     */
+    protected function _buildTree($tree, $indent = 0, $parent = null)
+    {
+    }
+
+}
diff --git a/framework/Block/lib/Horde/Block/Collection.php b/framework/Block/lib/Horde/Block/Collection.php
new file mode 100644 (file)
index 0000000..6878e49
--- /dev/null
@@ -0,0 +1,472 @@
+<?php
+/**
+ * The Horde_Block_Collection:: class provides an API to the blocks
+ * (applets) framework.
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author   Mike Cochrane <mike@graftonhall.co.nz>
+ * @author   Jan Schneider <jan@horde.org>
+ * @category Horde
+ * @package  Horde_Block
+ */
+class Horde_Block_Collection
+{
+    /**
+     * Singleton instances.
+     *
+     * @var array
+     */
+    static protected $_instances = array();
+
+    /**
+     * Cache for getBlocksList().
+     *
+     * @var array
+     */
+    static protected $_blocksCache = array();
+
+    /**
+     * What kind of blocks are we collecting? Defaults to any.
+     *
+     * @var string
+     */
+    protected $_type = 'portal';
+
+    /**
+     * A hash storing the information about all available blocks from
+     * all applications.
+     *
+     * @var array
+     */
+    protected $_blocks = array();
+
+    /**
+     * Returns a single instance of the Horde_Blocks class.
+     *
+     * @param string $type  The kind of blocks to list.
+     * @param array $apps   The applications whose blocks to list.
+     *
+     * @return Horde_Block_Collection  The Horde_Block_Collection instance.
+     */
+    static public function singleton($type = null, $apps = array())
+    {
+        sort($apps);
+        $signature = serialize(array($type, $apps));
+        if (!isset(self::$_instances[$signature])) {
+            self::$_instances[$signature] = new Horde_Block_Collection($type, $apps);
+        }
+
+        return self::$_instances[$signature];
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param string $type  The kind of blocks to list.
+     * @param array $apps   The applications whose blocks to list.
+     */
+    public function __construct($type = null, $apps = array())
+    {
+        if (!is_null($type)) {
+            $this->_type = $type;
+        }
+
+        $signature = serialize($apps);
+        if (isset($_SESSION['horde']['blocks'][$signature])) {
+            $this->_blocks = &$_SESSION['horde']['blocks'][$signature];
+            return;
+        }
+
+        foreach ($GLOBALS['registry']->listApps() as $app) {
+            if (count($apps) && !in_array($app, $apps)) {
+                continue;
+            }
+
+            try {
+                $pushed = $GLOBALS['registry']->pushApp($app);
+            } catch (Horde_Exception $e) {
+                continue;
+            }
+
+            $blockdir = $GLOBALS['registry']->get('fileroot', $app) . '/lib/Block';
+            $dh = @opendir($blockdir);
+            if (is_resource($dh)) {
+                while (($file = readdir($dh)) !== false) {
+                    if (substr($file, -4) == '.php') {
+                        $block_name = null;
+                        $block_type = null;
+                        if (is_readable($blockdir . '/' . $file)) {
+                            include_once $blockdir . '/' . $file;
+                        }
+                        if (!is_null($block_type) && !is_null($this->_type) &&
+                            $block_type != $this->_type) {
+                            continue;
+                        }
+                        if (!empty($block_name)) {
+                            $this->_blocks[$app][substr($file, 0, -4)]['name'] = $block_name;
+                        }
+                    }
+                }
+                closedir($dh);
+            }
+            // Don't pop an application if we didn't have to push one.
+            if ($pushed) {
+                $GLOBALS['registry']->popApp($app);
+            }
+        }
+
+        uksort($this->_blocks, array($this, 'sortBlockCollection'));
+        $_SESSION['horde']['blocks'][$signature] = &$this->_blocks;
+    }
+
+    /**
+     * Block sorting helper
+     */
+    public function sortBlockCollection($a, $b)
+    {
+        return strcasecmp($GLOBALS['registry']->get('name', $a), $GLOBALS['registry']->get('name', $b));
+    }
+
+    /**
+     * TODO
+     */
+    public function getBlock($app, $name, $params = null, $row = null,
+                             $col = null)
+    {
+        if ($GLOBALS['registry']->get('status', $app) == 'inactive' ||
+            ($GLOBALS['registry']->get('status', $app) == 'admin' &&
+             !Horde_Auth::isAdmin())) {
+            $error = PEAR::raiseError(sprintf(_("%s is not activated."), $GLOBALS['registry']->get('name', $app)));
+            return $error;
+        }
+
+        $path = $GLOBALS['registry']->get('fileroot', $app) . '/lib/Block/' . $name . '.php';
+        if (is_readable($path)) {
+            include_once $path;
+        }
+        $class = 'Horde_Block_' . $app . '_' . $name;
+        if (!class_exists($class)) {
+            $error = PEAR::raiseError(sprintf(_("%s not found."), $class));
+            return $error;
+        }
+
+        return new $class($params, $row, $col);
+    }
+
+    /**
+     * Returns a pretty printed list of all available blocks.
+     *
+     * @return array  A hash with block IDs as keys and application plus block
+     *                block names as values.
+     */
+    public function getBlocksList()
+    {
+        if (empty(self::$_blocksCache)) {
+            /* Get available blocks from all apps. */
+            foreach ($this->_blocks as $app => $app_blocks) {
+                foreach ($app_blocks as $block_id => $block) {
+                    if (isset($block['name'])) {
+                        self::$_blocksCache[$app . ':' . $block_id] = $GLOBALS['registry']->get('name', $app) . ': ' . $block['name'];
+                    }
+                }
+            }
+        }
+
+        return self::$_blocksCache;
+    }
+
+    /**
+     * Returns a layout with all fixed blocks as per configuration.
+     *
+     * @return string  A default serialized block layout.
+     */
+    public function getFixedBlocks()
+    {
+        $layout = array();
+        if (isset($GLOBALS['conf']['portal']['fixed_blocks'])) {
+            foreach ($GLOBALS['conf']['portal']['fixed_blocks'] as $block) {
+                list($app, $type) = explode(':', $block, 2);
+                $layout[] = array(array('app' => $app,
+                                        'params' => array('type' => $type,
+                                                          'params' => false),
+                                        'height' => 1,
+                                        'width' => 1));
+            }
+        }
+
+        return $layout;
+    }
+
+    /**
+     * Returns a select widget with all available blocks.
+     *
+     * @param string $cur_app    The block from this application gets selected.
+     * @param string $cur_block  The block with this name gets selected.
+     *
+     * @return string  The select tag with all available blocks.
+     */
+    public function getBlocksWidget($cur_app = null, $cur_block = null,
+                                    $onchange = false)
+    {
+        $widget = '<select name="app"';
+        if ($onchange) {
+            $widget .= ' onchange="document.blockform.action.value=\'save-resume\';document.blockform.submit()"';
+        }
+        $widget .= ">\n";
+
+        foreach ($this->getBlocksList() as $id => $name) {
+            $widget .= sprintf("<option value=\"%s\"%s>%s</option>\n",
+                                   $id,
+                                   ($id == $cur_app . ':' . $cur_block) ? ' selected="selected"' : '',
+                                   $name);
+        }
+
+        return $widget . "</select>\n";
+    }
+
+    /**
+     * Returns the option type.
+     *
+     * @param $app TODO
+     * @param $block TODO
+     * @param $param_id TODO
+     *
+     * @return TODO
+     */
+    public function getOptionType($app, $block, $param_id)
+    {
+        $this->getParams($app, $block);
+        return $this->_blocks[$app][$block]['params'][$param_id]['type'];
+    }
+
+    /**
+     * Returns whether the option is required or not. Defaults to true.
+     *
+     * @param $app TODO
+     * @param $block TODO
+     * @param $param_id TODO
+     *
+     * @return TODO
+     */
+    public function getOptionRequired($app, $block, $param_id)
+    {
+        $this->getParams($app, $block);
+        return isset($this->_blocks[$app][$block]['params'][$param_id]['required'])
+            ? $this->_blocks[$app][$block]['params'][$param_id]['required']
+            : true;
+    }
+
+    /**
+     * Returns the values for an option.
+     *
+     * @param $app TODO
+     * @param $block TODO
+     * @param $param_id TODO
+     *
+     * @return TODO
+     */
+    public function getOptionValues($app, $block, $param_id)
+    {
+        $this->getParams($app, $block);
+        return $this->_blocks[$app][$block]['params'][$param_id]['values'];
+    }
+
+    /**
+     * Returns the widget necessary to configure this block.
+     *
+     * @param $app TODO
+     * @param $block TODO
+     * @param $param_id TODO
+     * @param $val TODO
+     *
+     * @return TODO
+     */
+    public function getOptionsWidget($app, $block, $param_id, $val = null)
+    {
+        $widget = '';
+
+        $this->getParams($app, $block);
+        $param = $this->_blocks[$app][$block]['params'][$param_id];
+        if (!isset($param['default'])) {
+            $param['default'] = '';
+        }
+
+        switch ($param['type']) {
+        case 'boolean':
+        case 'checkbox':
+            $checked = !empty($val[$param_id]) ? ' checked="checked"' : '';
+            $widget = sprintf('<input type="checkbox" name="params[%s]"%s />', $param_id, $checked);
+            break;
+
+        case 'enum':
+            $widget = sprintf('<select name="params[%s]">', $param_id);
+            foreach ($param['values'] as $key => $name) {
+                if (Horde_String::length($name) > 30) {
+                    $name = substr($name, 0, 27) . '...';
+                }
+                $widget .= sprintf("<option value=\"%s\"%s>%s</option>\n",
+                                   htmlspecialchars($key),
+                                   (isset($val[$param_id]) && $val[$param_id] == $key) ? ' selected="selected"' : '',
+                                   htmlspecialchars($name));
+            }
+
+            $widget .= '</select>';
+            break;
+
+        case 'multienum':
+            $widget = sprintf('<select multiple="multiple" name="params[%s][]">', $param_id);
+            foreach ($param['values'] as $key => $name) {
+                if (Horde_String::length($name) > 30) {
+                    $name = substr($name, 0, 27) . '...';
+                }
+                $widget .= sprintf("<option value=\"%s\"%s>%s</option>\n",
+                                   htmlspecialchars($key),
+                                   (isset($val[$param_id]) && in_array($key, $val[$param_id])) ? ' selected="selected"' : '',
+                                   htmlspecialchars($name));
+            }
+
+            $widget .= '</select>';
+            break;
+
+        case 'mlenum':
+            // Multi-level enum.
+            if (is_array($val) && isset($val['__' . $param_id])) {
+                $firstval = $val['__' . $param_id];
+            } else {
+                $tmp = array_keys($param['values']);
+                $firstval = current($tmp);
+            }
+            $blockvalues = $param['values'][$firstval];
+            asort($blockvalues);
+
+            $widget = sprintf('<select name="params[__%s]" onchange="document.blockform.action.value=\'save-resume\';document.blockform.submit()">', $param_id) . "\n";
+            foreach ($param['values'] as $key => $values) {
+                $name = Horde_String::length($key) > 30 ? Horde_String::substr($key, 0, 27) . '...' : $key;
+                $widget .= sprintf("<option value=\"%s\"%s>%s</option>\n",
+                                   htmlspecialchars($key),
+                                   $key == $firstval ? ' selected="selected"' : '',
+                                   htmlspecialchars($name));
+            }
+            $widget .= "</select><br />\n";
+
+            $widget .= sprintf("<select name=\"params[%s]\">\n", $param_id);
+            foreach ($blockvalues as $key => $name) {
+                $name = (Horde_String::length($name) > 30) ? Horde_String::substr($name, 0, 27) . '...' : $name;
+                $widget .= sprintf("<option value=\"%s\"%s>%s</option>\n",
+                                   htmlspecialchars($key),
+                                   $val[$param_id] == $key ? ' selected="selected"' : '',
+                                   htmlspecialchars($name));
+            }
+            $widget .= "</select><br />\n";
+            break;
+
+        case 'int':
+        case 'text':
+            $widget = sprintf('<input type="text" name="params[%s]" value="%s" />', $param_id, !isset($val[$param_id]) ? $param['default'] : $val[$param_id]);
+            break;
+
+        case 'password':
+            $widget = sprintf('<input type="password" name="params[%s]" value="%s" />', $param_id, !isset($val[$param_id]) ? $param['default'] : $val[$param_id]);
+            break;
+
+        case 'error':
+            $widget = '<span class="form-error">' . $param['default'] . '</span>';
+            break;
+        }
+
+        return $widget;
+    }
+
+    /**
+     * Returns the name of the specified block.
+     *
+     * @param string $app    An application name.
+     * @param string $block  A block name.
+     *
+     * @return string  The name of the specified block.
+     */
+    public function getName($app, $block)
+    {
+        return isset($this->_blocks[$app][$block])
+            ? $this->_blocks[$app][$block]['name']
+            : sprintf(_("Block \"%s\" of application \"%s\" not found."), $block, $app);
+    }
+
+    /**
+     * Returns the parameter list of the specified block.
+     *
+     * @param string $app    An application name.
+     * @param string $block  A block name.
+     *
+     * @return array  An array with all paramter names.
+     */
+    public function getParams($app, $block)
+    {
+        if (!isset($this->_blocks[$app][$block]['params'])) {
+            $blockOb = $this->getBlock($app, $block);
+            if ($blockOb instanceof PEAR_Error) {
+                return $blockOb;
+            }
+            $this->_blocks[$app][$block]['params'] = $blockOb->getParams();
+        }
+
+        if (isset($this->_blocks[$app][$block]['params']) &&
+            is_array($this->_blocks[$app][$block]['params'])) {
+            return array_keys($this->_blocks[$app][$block]['params']);
+        }
+
+        return array();
+    }
+
+    /**
+     * Returns the (clear text) name of the specified parameter.
+     *
+     * @param string $app    An application name.
+     * @param string $block  A block name.
+     * @param string $param  A parameter name.
+     *
+     * @return string  The name of the specified parameter.
+     */
+    public function getParamName($app, $block, $param)
+    {
+        $this->getParams($app, $block);
+        return $this->_blocks[$app][$block]['params'][$param]['name'];
+    }
+
+    /**
+     * Returns the default value of the specified parameter.
+     *
+     * @param string $app    An application name.
+     * @param string $block  A block name.
+     * @param string $param  A parameter name.
+     *
+     * @return string  The default value of the specified parameter or null.
+     */
+    public function getDefaultValue($app, $block, $param)
+    {
+        $this->getParams($app, $block);
+        return isset($this->_blocks[$app][$block]['params'][$param]['default'])
+            ? $this->_blocks[$app][$block]['params'][$param]['default']
+            : null;
+    }
+
+    /**
+     * Returns if the specified block is customizeable by the user.
+     *
+     * @param string $app    An application name.
+     * @param string $block  A block name.
+     *
+     * @return boolean  True is the block is customizeable.
+     */
+    public function isEditable($app, $block)
+    {
+        $this->getParams($app, $block);
+        return (isset($this->_blocks[$app][$block]['params']) &&
+            count($this->_blocks[$app][$block]['params']));
+    }
+
+}
diff --git a/framework/Block/lib/Horde/Block/Layout.php b/framework/Block/lib/Horde/Block/Layout.php
new file mode 100644 (file)
index 0000000..27ac4f2
--- /dev/null
@@ -0,0 +1,126 @@
+<?php
+/**
+ * The Horde_Block_Layout class provides basic functionality for both managing
+ * and displaying blocks through Horde_Block_Layout_Manager and
+ * Horde_Block_Layout_View.
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author   Mike Cochrane <mike@graftonhall.co.nz>
+ * @author   Jan Schneider <jan@horde.org>
+ * @category Horde
+ * @package  Horde_Block
+ */
+class Horde_Block_Layout
+{
+    /**
+     * TODO
+     *
+     * @var string
+     */
+    protected $_editUrl;
+
+    /**
+     * TODO
+     *
+     * @var string
+     */
+    protected $_viewUrl;
+
+    /**
+     * Returns whether the specified block may be removed.
+     *
+     * @param integer $row  A layout row.
+     * @param integer $col  A layout column.
+     *
+     * @return boolean  True if this block may be removed.
+     */
+    public function isRemovable($row, $col)
+    {
+        global $conf;
+
+        $app = $this->_layout[$row][$col]['app'];
+        $type = $this->_layout[$row][$col]['params']['type'];
+        $block = $app . ':' . $type;
+
+        /* Check if the block is a fixed block. */
+        if (!in_array($block, $conf['portal']['fixed_blocks'])) {
+            return true;
+        }
+
+        /* Check if we have still another block of the same type. */
+        $found = false;
+        foreach ($this->_layout as $cur_row) {
+            foreach ($cur_row as $cur_col) {
+                if (isset($cur_col['app']) &&
+                    $cur_col['app'] == $app &&
+                    $cur_col['params']['type'] == $type) {
+                    if ($found) {
+                        return true;
+                    } else {
+                        $found = true;
+                    }
+                }
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Returns an URL triggering an action to a block.
+     *
+     * @param string $action  An action to trigger.
+     * @param integer $row    A layout row.
+     * @param integer $col    A layout column.
+     *
+     * @return string  An URL with all necessary parameters.
+     */
+    public function getActionUrl($action, $row, $col)
+    {
+        return Horde_Util::addParameter(
+            Horde::url($this->_editUrl),
+            array('col' => $col,
+                  'row' => $row,
+                  'action' => $action,
+                  'url' => $this->_viewUrl,
+                  'nocache' => base_convert(microtime(), 10, 36))) . '#block';
+    }
+
+    /**
+     * Returns the actions for the block header.
+     *
+     * @param integer $row   A layout row.
+     * @param integer $col   A layout column.
+     * @param boolean $edit  Whether to include the edit icon.
+     * @param $url TODO
+     *
+     * @return string  HTML code for the block action icons.
+     */
+    public function getHeaderIcons($row, $col, $edit, $url = null)
+    {
+        $icons = '';
+        if ($edit) {
+            $icons .= Horde::link($this->getActionUrl('edit', $row, $col),
+                                  _("Edit"))
+                . Horde::img('edit.png', _("Edit"), '',
+                             $GLOBALS['registry']->getImageDir('horde'))
+                . '</a>';
+        }
+        if ($this->isRemovable($row, $col)) {
+            $icons .= Horde::link(
+                $this->getActionUrl('removeBlock', $row, $col), _("Remove"),
+                '', '',
+                'return window.confirm(\''
+                . addslashes(_("Really delete this block?")) . '\')')
+                . Horde::img('delete.png', _("Remove"), '',
+                             $GLOBALS['registry']->getImageDir('horde'))
+                . '</a>';
+        }
+        return $icons;
+    }
+
+}
diff --git a/framework/Block/lib/Horde/Block/Layout/Manager.php b/framework/Block/lib/Horde/Block/Layout/Manager.php
new file mode 100644 (file)
index 0000000..d99fcca
--- /dev/null
@@ -0,0 +1,1384 @@
+<?php
+/**
+ * The Horde_Block_Layout_Manager class allows manipulation of Horde_Block
+ * layouts.
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author   Mike Cochrane <mike@graftonhall.co.nz>
+ * @author   Jan Schneider <jan@horde.org>
+ * @category Horde
+ * @package  Horde_Block
+ */
+class Horde_Block_Layout_Manager extends Horde_Block_Layout
+{
+    /**
+     * Singleton instances.
+     *
+     * @var array
+     */
+    static protected $_instances = array();
+
+    /**
+     * Our Horde_Block_Collection instance.
+     *
+     * @var Horde_Block_Collection
+     */
+    protected $_collection;
+
+    /**
+     * The current block layout.
+     *
+     * @var array
+     */
+    protected $_layout = array();
+
+    /**
+     * A cache for the block objects.
+     *
+     * @var array
+     */
+    protected $_blocks = array();
+
+    /**
+     * The maximum number of columns.
+     *
+     * @var integer
+     */
+    protected $_columns = 0;
+
+    /**
+     * Has the layout been updated since it was instantiated.
+     *
+     * @var boolean
+     */
+    protected $_updated = false;
+
+    /**
+     * The current block (array: [row, col]).
+     *
+     * @var array
+     */
+    protected $_currentBlock = array(null, null);
+
+    /**
+     * The new row of the last changed block.
+     *
+     * @var integer
+     */
+    protected $_changed_row = null;
+
+    /**
+     * The new column of the last changed block.
+     *
+     * @var integer
+     */
+    protected $_changed_col = null;
+
+    /**
+     * Returns a single instance of the Horde_Block_Layout_Manager class.
+     *
+     * @param string $name                        TODO
+     * @param Horde_Block_Collection $collection  TODO
+     * @param string $data                        TODO
+     *
+     * @return Horde_Block_Layout_Manager  The requested instance.
+     */
+    static public function &singleton($name, $collection, $data = '')
+    {
+        if (!isset(self::$_instances[$name])) {
+            self::$_instances[$name] = new Horde_Block_Layout_Manager($collection, $data);
+        }
+        return self::$_instances[$name];
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param Horde_Block_Collection $collection  TODO
+     * @param array $layout                       TODO
+     */
+    function __construct($collection, $layout = array())
+    {
+        $this->_collection = $collection;
+        $this->_layout = $layout;
+        $this->_editUrl = Horde::selfUrl();
+
+        // Fill the _covered caches and empty rows.
+        $rows = count($this->_layout);
+        $empty = array();
+        for ($row = 0; $row < $rows; $row++) {
+            $cols = count($this->_layout[$row]);
+            if (!isset($empty[$row])) {
+                $empty[$row] = true;
+            }
+            for ($col = 0; $col < $cols; $col++) {
+                if (!isset($this->_layout[$row][$col])) {
+                    $this->_blocks[$row][$col] = PEAR::raiseError(_("No block exists at the requested position"), 'horde.error');
+                } elseif (is_array($this->_layout[$row][$col])) {
+                    $field = $this->_layout[$row][$col];
+
+                    $empty[$row] = false;
+                    if (isset($field['width'])) {
+                        for ($i = 1; $i < $field['width']; $i++) {
+                            $this->_layout[$row][$col + $i] = 'covered';
+                        }
+                    }
+                    if (isset($field['height'])) {
+                        if (!isset($field['width'])) {
+                            $field['width'] = 1;
+                        }
+                        for ($i = 1; $i < $field['height']; $i++) {
+                            $this->_layout[$row + $i][$col] = 'covered';
+                            for ($j = 1; $j < $field['width']; $j++) {
+                                $this->_layout[$row + $i][$col + $j] = 'covered';
+                            }
+                            $empty[$row + $i] = false;
+                        }
+                    }
+                }
+            }
+
+            // Strip empty blocks from the end of the rows.
+            for ($col = $cols - 1; $col >= 0; $col--) {
+                if (!isset($this->_layout[$row][$col]) ||
+                    $this->_layout[$row][$col] == 'empty') {
+                    unset($this->_layout[$row][$col]);
+                } else {
+                    break;
+                }
+            }
+
+            $this->_columns = max($this->_columns, count($this->_layout[$row]));
+        }
+
+        // Fill all rows up to the same length.
+        $layout = array();
+        for ($row = 0; $row < $rows; $row++) {
+            $cols = count($this->_layout[$row]);
+            if ($cols < $this->_columns) {
+                for ($col = $cols; $col < $this->_columns; $col++) {
+                    $this->_layout[$row][$col] = 'empty';
+                }
+            }
+            $layout[] = $this->_layout[$row];
+        }
+
+        $this->_layout = $layout;
+    }
+
+    /**
+     * Serialize and return the current block layout.
+     *
+     * @return TODO
+     */
+    public function serialize()
+    {
+        return serialize($this->_layout);
+    }
+
+    /**
+     * Resets the current layout to the value stored in the preferences.
+     */
+    public function unserialize($data)
+    {
+        $this->_layout = @unserialize($data);
+    }
+
+    /**
+     * Process a modification to the current layout.
+     *
+     * @param string $action  TODO
+     * @param integer $row    TODO
+     * @param integer $col    TODO
+     * @param string $url     TODO
+     *
+     * @throws Horde_Exception
+     */
+    public function handle($action, $row, $col, $url = null)
+    {
+        switch ($action) {
+        case 'moveUp':
+        case 'moveDown':
+        case 'moveLeft':
+        case 'moveRight':
+        case 'expandUp':
+        case 'expandDown':
+        case 'expandLeft':
+        case 'expandRight':
+        case 'shrinkLeft':
+        case 'shrinkRight':
+        case 'shrinkUp':
+        case 'shrinkDown':
+        case 'removeBlock':
+            try {
+                $result = call_user_func(array($this, $action), $row, $col);
+                $this->_updated = true;
+            } catch (Horde_Exception $e) {
+                $GLOBALS['notification']->push($e);
+            }
+            break;
+
+        // Save the changes made to a block.
+        case 'save':
+        // Save the changes made to a block and continue editing.
+        case 'save-resume':
+            // Get requested block type.
+            list($newapp, $newtype) = explode(':', Horde_Util::getFormData('app'));
+
+            // Is this a new block?
+            $new = false;
+            if ($this->isEmpty($row, $col) ||
+                !$this->rowExists($row) ||
+                !$this->colExists($col)) {
+                // Check permissions.
+                if (Horde::hasPermission('max_blocks') !== true &&
+                    Horde::hasPermission('max_blocks') <= $this->count()) {
+                    try {
+                        $message = Horde::callHook('perms_denied', array('horde:max_blocks'));
+                    } catch (Horde_Exception_HookNotSet $e) {
+                        $message = @htmlspecialchars(sprintf(ngettext("You are not allowed to create more than %d block.", "You are not allowed to create more than %d blocks.", Horde::hasPermission('max_blocks')), Horde::hasPermission('max_blocks')), ENT_COMPAT, Horde_Nls::getCharset());
+                    }
+                    $GLOBALS['notification']->push($message, 'horde.error', array('content.raw'));
+                    break;
+                }
+
+                $new = true;
+                // Make sure there is somewhere to put it.
+                $this->addBlock($row, $col);
+            }
+
+            // Or an existing one?
+            $exists = false;
+            $changed = false;
+            if (!$new) {
+                // Get target block info.
+                $info = $this->getBlockInfo($row, $col);
+                $exists = $this->isBlock($row, $col);
+                // Has a different block been selected?
+                if ($exists &&
+                    ($info['app'] != $newapp ||
+                     $info['block'] != $newtype)) {
+                    $changed = true;
+                }
+            }
+
+            if ($new || $changed) {
+                // Change app or type.
+                $info = array();
+                $info['app'] = $newapp;
+                $info['block'] = $newtype;
+                $params = $this->_collection->getParams($newapp, $newtype);
+                foreach ($params as $newparam) {
+                    $info['params'][$newparam] = $this->_collection->getDefaultValue($newapp, $newtype, $newparam);
+                }
+                $this->setBlockInfo($row, $col, $info);
+            } elseif ($exists) {
+                // Change values.
+                $this->setBlockInfo($row, $col, array('params' => Horde_Util::getFormData('params', array())));
+            }
+            $this->_updated = true;
+            if ($action == 'save') {
+                break;
+            }
+
+        // Make a block the current block for editing.
+        case 'edit':
+            $this->_currentBlock = array($row, $col);
+            $url = null;
+            break;
+        }
+
+        if (!empty($url)) {
+            header('Location: ' . Horde_Util::addParameter($url, 'unique',
+                                                     md5(microtime()), false));
+        }
+    }
+
+    /**
+     * Has the layout been changed since it was instantiated?
+     *
+     * @return boolean
+     */
+    public function updated()
+    {
+        return $this->_updated;
+    }
+
+    /**
+     * Get the current block row and column.
+     *
+     * @return array  [row, col]
+     */
+    public function getCurrentBlock()
+    {
+        return $this->_currentBlock;
+    }
+
+    /**
+     * Returns the Horde_Block at the specified position.
+     *
+     * @param integer $row  A layout row.
+     * @param integer $col  A layout column.
+     *
+     * @return Horde_Block  The block from that position.
+     */
+    public function getBlock($row, $col)
+    {
+        if (!isset($this->_blocks[$row][$col])) {
+            $field = $this->_layout[$row][$col];
+            $this->_blocks[$row][$col] = Horde_Block_Collection::getBlock($field['app'], $field['params']['type'], $field['params']['params']);
+        }
+
+        return $this->_blocks[$row][$col];
+    }
+
+    /**
+     * Returns the coordinates of the block covering the specified
+     * field.
+     *
+     * @param integer $row  A layout row.
+     * @param integer $col  A layout column.
+     *
+     * @return array  The top-left row-column-coordinate of the block
+     *                covering the specified field or null if the field
+     *                is empty.
+     */
+    public function getBlockAt($row, $col)
+    {
+        /* Trivial cases first. */
+        if ($this->isEmpty($row, $col)) {
+            return null;
+        } elseif (!$this->isCovered($row, $col)) {
+            return array($row, $col);
+        }
+
+        /* This is a covered field. */
+        for ($test = $row - 1; $test >= 0; $test--) {
+            if (!$this->isCovered($test, $col) &&
+                !$this->isEmpty($test, $col) &&
+                $test + $this->getHeight($test, $col) - 1 == $row) {
+                return array($test, $col);
+            }
+        }
+        for ($test = $col - 1; $test >= 0; $test--) {
+            if (!$this->isCovered($row, $test) &&
+                !$this->isEmpty($test, $col) &&
+                $test + $this->getWidth($row, $test) - 1 == $col) {
+                return array($row, $test);
+            }
+        }
+    }
+
+    /**
+     * Returns a hash with some useful information about the specified
+     * block.
+     *
+     * Returned hash values:
+     * 'app': application name
+     * 'block': block name
+     * 'params': parameter hash
+     *
+     * @param integer $row  A layout row.
+     * @param integer $col  A layout column.
+     *
+     * @return array  The information hash.
+     * @throws Horde_Exception
+     */
+    public function getBlockInfo($row, $col)
+    {
+        if (!isset($this->_layout[$row][$col]) ||
+            $this->isEmpty($row, $col) ||
+            $this->isCovered($row, $col)) {
+            throw new Horde_Exception('No block exists at the requested position', 'horde.error');
+        }
+
+        return array(
+            'app' => $this->_layout[$row][$col]['app'],
+            'block' => $this->_layout[$row][$col]['params']['type'],
+            'params' => $this->_layout[$row][$col]['params']['params']
+        );
+    }
+
+    /**
+     * Sets a batch of information about the specified block.
+     *
+     * @param integer $row  A layout row.
+     * @param integer $col  A layout column.
+     * @param array $info   A hash with information values.
+     *                      Possible elements are:
+     *                      'app': application name
+     *                      'block': block name
+     *                      'params': parameter hash
+     *
+     * @throws Horde_Exception
+     */
+    public function setBlockInfo($row, $col, $info = array())
+    {
+        if (!isset($this->_layout[$row][$col])) {
+            throw new Horde_Exception('No block exists at the requested position', 'horde.error');
+        }
+
+        if (isset($info['app'])) {
+            $this->_layout[$row][$col]['app'] = $info['app'];
+        }
+        if (isset($info['block'])) {
+            $this->_layout[$row][$col]['params']['type'] = $info['block'];
+        }
+        if (isset($info['params'])) {
+            $this->_layout[$row][$col]['params']['params'] = $info['params'];
+        }
+
+        $this->_changed_row = $row;
+        $this->_changed_col = $col;
+    }
+
+    /**
+     * Returns the number of blocks in the current layout.
+     *
+     * @return integer  The number of blocks.
+     */
+    public function count()
+    {
+        $rows = $this->rows();
+        $count = 0;
+
+        for ($row = 0; $row < $rows; $row++) {
+            $cols = $this->columns($row);
+            for ($col = 0; $col < $cols; $col++) {
+                if (!$this->isEmpty($row, $col) &&
+                    !$this->isCovered($row, $col)) {
+                    ++$count;
+                }
+            }
+        }
+
+        return $count;
+    }
+
+    /**
+     * Returns the number of rows in the current layout.
+     *
+     * @return integer  The number of rows.
+     */
+    public function rows()
+    {
+        return count($this->_layout);
+    }
+
+    /**
+     * Returns the number of columns in the specified row of the
+     * current layout.
+     *
+     * @param integer $row  The row to return the number of columns from.
+     *
+     * @return integer  The number of columns.
+     * @throws Horde_Exception
+     */
+    public function columns($row)
+    {
+        if (isset($this->_layout[$row])) {
+            return count($this->_layout[$row]);
+        }
+
+        throw new Horde_Exception(sprintf('The specified row (%d) does not exist.', $row), 'horde.error');
+    }
+
+    /**
+     * Checks to see if a given location if being used by a block.
+     *
+     * @param integer $row  A layout row.
+     * @param integer $col  A layout column.
+     *
+     * @return boolean  True if the location is empty
+     *                  False is the location is being used.
+     */
+    public function isEmpty($row, $col)
+    {
+        return !isset($this->_layout[$row][$col]) || $this->_layout[$row][$col] == 'empty';
+    }
+
+    /**
+     * Returns if the field at the specified position is covered by
+     * another block.
+     *
+     * @param integer $row  A layout row.
+     * @param integer $col  A layout column.
+     *
+     * @return boolean  True if the specified field is covered.
+     */
+    public function isCovered($row, $col)
+    {
+        return isset($this->_layout[$row][$col])
+            ? $this->_layout[$row][$col] == 'covered'
+            : false;
+    }
+
+    /**
+     * Returns if the specified location is the top left field of
+     * a block.
+     *
+     * @param integer $row  A layout row.
+     * @param integer $col  A layout column.
+     *
+     * @return boolean  True if the specified position is a block, false if
+     *                  the field doesn't exist, is empty or covered.
+     */
+    public function isBlock($row, $col)
+    {
+        return ($this->rowExists($row) &&
+                $this->colExists($col) &&
+                !$this->isEmpty($row, $col) &&
+                !$this->isCovered($row, $col));
+    }
+
+    /**
+     * Returns if the specified block has been changed last.
+     *
+     * @param integer $row  A layout row.
+     * @param integer $col  A layout column.
+     *
+     * @return boolean  True if this block is the last one that was changed.
+     */
+    public function isChanged($row, $col)
+    {
+        return (($this->_changed_row === $row) &&
+                ($this->_changed_col === $col));
+    }
+
+    /**
+     * Returns a control (linked arrow) for a certain action on the
+     * specified block.
+     *
+     * @param string $type  A control type in the form
+     *                      "modification/direction". Possible values for
+     *                      modification: expand, shrink, move. Possible values
+     *                      for direction: up, down, left, right.
+     * @param integer $row  A layout row.
+     * @param integer $col  A layout column.
+     *
+     * @return string  A link containing an arrow representing the requested
+     *                 control.
+     */
+    public function getControl($type, $row, $col)
+    {
+        $type = explode('/', $type);
+        $action = $type[0] . ucfirst($type[1]);
+        $url = $this->getActionUrl($action, $row, $col);
+
+        switch ($type[0]) {
+        case 'expand':
+            $title = _("Expand");
+            $img = 'large_' . $type[1];
+            break;
+
+        case 'shrink':
+            $title = _("Shrink");
+            $img = 'large_';
+
+            switch ($type[1]) {
+            case 'up':
+                $img .= 'down';
+                break;
+
+            case 'down':
+                $img .= 'up';
+                break;
+
+            case 'left':
+                $img .= 'right';
+                break;
+
+            case 'right':
+                $img .= 'left';
+                break;
+            }
+            break;
+
+        case 'move':
+            switch ($type[1]) {
+            case 'up':
+                $title = _("Move Up");
+                break;
+
+            case 'down':
+                $title = _("Move Down");
+                break;
+
+            case 'left':
+                $title = _("Move Left");
+                break;
+
+            case 'right':
+                $title = _("Move Right");
+                break;
+            }
+
+            $img = $type[1];
+            break;
+        }
+
+        return Horde::link($url, $title) .
+            Horde::img('block/' . $img . '.png', $title, '', $GLOBALS['registry']->getImageDir('horde')) . '</a>';
+    }
+
+    /**
+     * Does a row exist?
+     *
+     * @param integer $row  The row to look for.
+     *
+     * @return boolean  True if the row exists.
+     */
+    public function rowExists($row)
+    {
+        return $row < count($this->_layout);
+    }
+
+    /**
+     * Does a column exist?
+     *
+     * @param integer $col  The column to look for.
+     *
+     * @return boolean  True if the column exists.
+     */
+    public function colExists($col)
+    {
+        return $col < $this->_columns;
+    }
+
+    /**
+     * Get the width of the block at a given location.
+     * This returns the width if there is a block at this location, otherwise
+     * returns 1.
+     *
+     * @param integer $row  A layout row.
+     * @param integer $col  A layout column.
+     *
+     * @return integer  The number of columns this block spans.
+     */
+    public function getWidth($row, $col)
+    {
+        if (!isset($this->_layout[$row][$col]) ||
+            !is_array($this->_layout[$row][$col])) {
+            return 1;
+        }
+        if (!isset($this->_layout[$row][$col]['width'])) {
+            $this->_layout[$row][$col]['width'] = 1;
+        }
+        return $this->_layout[$row][$col]['width'];
+    }
+
+    /**
+     * Get the height of the block at a given location.
+     * This returns the height if there is a block at this location, otherwise
+     * returns 1.
+     *
+     * @param integer $row  A layout row.
+     * @param integer $col  A layout column.
+     *
+     * @return integer  The number of rows this block spans.
+     */
+    public function getHeight($row, $col)
+    {
+        if (!isset($this->_layout[$row][$col]) ||
+            !is_array($this->_layout[$row][$col])) {
+            return 1;
+        }
+        if (!isset($this->_layout[$row][$col]['height'])) {
+            $this->_layout[$row][$col]['height'] = 1;
+        }
+        return $this->_layout[$row][$col]['height'];
+    }
+
+    /**
+     * Adds an empty block at the specified position.
+     *
+     * @param integer $row  A layout row.
+     * @param integer $col  A layout column.
+     */
+    public function addBlock($row, $col)
+    {
+        if (!$this->rowExists($row)) {
+            $this->addRow($row);
+        }
+        if (!$this->colExists($col)) {
+            $this->addCol($col);
+        }
+
+        $this->_layout[$row][$col] = array('app' => null,
+                                           'height' => 1,
+                                           'width' => 1,
+                                           'params' => array('type' => null,
+                                                             'params' => array()));
+    }
+
+    /**
+     * Adds a new row to the layout.
+     *
+     * @param integer $row  The number of the row to add
+     */
+    public function addRow($row)
+    {
+        if ($this->_columns > 0) {
+            $this->_layout[$row] = array_fill(0, $this->_columns, 'empty');
+        }
+    }
+
+    /**
+     * Adds a new column to the layout.
+     *
+     * @param integer $col  The number of the column to add
+     */
+    public function addCol($col)
+    {
+        foreach ($this->_layout as $id => $val) {
+            $this->_layout[$id][$col] = 'empty';
+        }
+        ++$this->_columns;
+    }
+
+    /**
+     * Removes a block.
+     *
+     * @param integer $row  A layout row.
+     * @param integer $col  A layout column.
+     */
+    public function removeBlock($row, $col)
+    {
+        $width = $this->getWidth($row, $col);
+        $height = $this->getHeight($row, $col);
+        for ($i = $height - 1; $i >= 0; $i--) {
+            for ($j = $width - 1; $j >= 0; $j--) {
+                $this->_layout[$row + $i][$col + $j] = 'empty';
+                if (!$this->colExists($col + $j + 1)) {
+                    $this->removeColIfEmpty($col + $j);
+                }
+            }
+            if (!$this->rowExists($row + $i + 1) && $this->rowExists($row + $i)) {
+                $this->removeRowIfEmpty($row + $i);
+            }
+        }
+
+        $this->_changed_row = $row;
+        $this->_changed_col = $col;
+
+        if (!$this->rowExists($row)) {
+            do {
+                --$row;
+            } while ($row >= 0 && $this->removeRowIfEmpty($row));
+        }
+        if (!$this->colExists($col)) {
+            do {
+                $col--;
+            } while ($col >= 0 && $this->removeColIfEmpty($col));
+        }
+    }
+
+    /**
+     * Removes a row if it's empty.
+     *
+     * @param integer $row  The number of the row to to check
+     *
+     * @return boolean  True if the row is now removed.
+     *                  False if the row still exists.
+     */
+    public function removeRowIfEmpty($row)
+    {
+        if (!$this->rowExists($row)) {
+            return true;
+        }
+
+        $rows = count($this->_layout[$row]);
+        for ($i = 0; $i < $rows; $i++) {
+            if (isset($this->_layout[$row][$i]) && $this->_layout[$row][$i] != 'empty') {
+                return false;
+            }
+        }
+        unset($this->_layout[$row]);
+
+        return true;
+    }
+
+    /**
+     * Removes a column if it's empty.
+     *
+     * @param integer $col  The number of the column to to check
+     *
+     * @return boolean  True if the column is now removed.
+     *                  False if the column still exists.
+     */
+    public function removeColIfEmpty($col)
+    {
+        if (!$this->colExists($col)) {
+            return true;
+        }
+
+        $cols = count($this->_layout);
+        for ($i = 0; $i < $cols; $i++) {
+            if (isset($this->_layout[$i][$col]) && $this->_layout[$i][$col] != 'empty') {
+                return false;
+            }
+        }
+
+        for ($i = 0; $i < $cols; $i++) {
+            unset($this->_layout[$i][$col]);
+        }
+
+        return true;
+    }
+
+    /**
+     * Moves a block one row up.
+     *
+     * @param integer $row  A layout row.
+     * @param integer $col  A layout column.
+     *
+     * @throws Horde_Exception
+     */
+    public function moveUp($row, $col)
+    {
+        if ($this->rowExists($row - 1)) {
+            $width = $this->getWidth($row, $col);
+            // See if there's room to move into
+            for ($i = 0; $i < $width; $i++) {
+                if (!$this->isEmpty($row - 1, $col + $i)) {
+                    $in_way = $this->getBlockAt($row - 1, $col + $i);
+                    if (!is_null($in_way) &&
+                        $in_way[1] == $col &&
+                        $this->getWidth($in_way[0], $in_way[1]) == $width) {
+                        // We need to swap the blocks.
+                        $rec1 = Horde_Array::getRectangle($this->_layout, $row, $col,
+                                                          $this->getHeight($row, $col), $this->getWidth($row, $col));
+                        $rec2 = Horde_Array::getRectangle($this->_layout, $in_way[0], $in_way[1],
+                                                          $this->getHeight($in_way[0], $in_way[1]), $this->getWidth($in_way[0], $in_way[1]));
+                        for ($j = 0; $j < count($rec1); $j++) {
+                            for ($k = 0; $k < count($rec1[$j]); $k++) {
+                                $this->_layout[$in_way[0] + $j][$in_way[1] + $k] = $rec1[$j][$k];
+                            }
+                        }
+                        for ($j = 0; $j < count($rec2); $j++) {
+                            for ($k = 0; $k < count($rec2[$j]); $k++) {
+                                $this->_layout[$in_way[0] + count($rec1) + $j][$in_way[1] + $k] = $rec2[$j][$k];
+                            }
+                        }
+                        $this->_changed_row = $in_way[0];
+                        $this->_changed_col = $in_way[1];
+                        return;
+                    }
+                    // Nowhere to go.
+                    throw new Horde_Exception('Shrink or move neighboring block(s) out of the way first', 'horde.warning');
+                }
+            }
+
+            $lastrow = $row + $this->getHeight($row, $col) - 1;
+            for ($i = 0; $i < $width; $i++) {
+                $prev = $this->_layout[$row][$col + $i];
+                // Move top edge
+                $this->_layout[$row - 1][$col + $i] = $prev;
+                $this->_layout[$row][$col + $i] = 'covered';
+                // Move bottom edge
+                $this->_layout[$lastrow][$col + $i] = 'empty';
+            }
+
+            if (!$this->rowExists($lastrow + 1)) {
+                // Was on the bottom row
+                $this->removeRowIfEmpty($lastrow);
+            }
+        }
+
+        $this->_changed_row = $row - 1;
+        $this->_changed_col = $col;
+    }
+
+    /**
+     * Moves a block one row down.
+     *
+     * @param integer $row  A layout row.
+     * @param integer $col  A layout column.
+     *
+     * @throws Horde_Exception
+     */
+    public function moveDown($row, $col)
+    {
+        $width = $this->getWidth($row, $col);
+        $lastrow = $row + $this->getHeight($row, $col);
+        if ($this->rowExists($lastrow)) {
+            // See if there's room to move into
+            for ($i = 0; $i < $width; $i++) {
+                if (!$this->isEmpty($lastrow, $col + $i)) {
+                    $in_way = $this->getBlockAt($lastrow, $col + $i);
+                    if (!is_null($in_way) &&
+                        $in_way[1] == $col &&
+                        $this->getWidth($in_way[0], $in_way[1]) == $width) {
+                        // We need to swap the blocks.
+                        $rec1 = Horde_Array::getRectangle($this->_layout, $row, $col,
+                                                          $this->getHeight($row, $col), $this->getWidth($row, $col));
+                        $rec2 = Horde_Array::getRectangle($this->_layout, $in_way[0], $in_way[1],
+                                                          $this->getHeight($in_way[0], $in_way[1]), $this->getWidth($in_way[0], $in_way[1]));
+                        for ($j = 0; $j < count($rec2); $j++) {
+                            for ($k = 0; $k < count($rec2[$j]); $k++) {
+                                $this->_layout[$row + $j][$col + $k] = $rec2[$j][$k];
+                            }
+                        }
+                        for ($j = 0; $j < count($rec1); $j++) {
+                            for ($k = 0; $k < count($rec1[$j]); $k++) {
+                                $this->_layout[$row + count($rec2) + $j][$col + $k] = $rec1[$j][$k];
+                            }
+                        }
+                        $this->_changed_row = $in_way[0];
+                        $this->_changed_col = $in_way[1];
+                        return;
+                    }
+                    // No where to go
+                    throw new Horde_Exception('Shrink or move neighbouring block(s) out of the way first', 'horde.warning');
+                }
+            }
+        } else {
+            // Make room to move into
+            $this->addRow($lastrow);
+        }
+
+        for ($i = 0; $i < $width; $i++) {
+            if (!isset($this->_layout[$row][$col + $i])) {
+                continue;
+            }
+            $prev = $this->_layout[$row][$col + $i];
+            // Move bottom edge
+            $this->_layout[$lastrow][$col + $i] = 'covered';
+            // Move top edge
+            $this->_layout[$row + 1][$col + $i] = $prev;
+            $this->_layout[$row][$col + $i] = 'empty';
+        }
+
+        $this->_changed_row = $row + 1;
+        $this->_changed_col = $col;
+    }
+
+    /**
+     * Moves all blocks below a certain row one row down.
+     *
+     * @param integer $row  A layout row.
+     *
+     * @return boolean  True if all rows could be moved down.
+     */
+    function moveDownBelow($row)
+    {
+        $moved = array();
+        for ($y = count($this->_layout) - 1; $y > $row; $y--) {
+            for ($x = 0; $x < $this->_columns; $x++) {
+                $block = $this->getBlockAt($y, $x);
+                if (empty($block)) {
+                    continue;
+                }
+                if (empty($moved[$block[1] . ':' . $block[0]])) {
+                    try {
+                        $result = $this->moveDown($block[0], $block[1]);
+                    } catch (Horde_Exception $e) {
+                        return false;
+                    }
+                    $moved[$block[1] . ':' . ($block[0] + 1)] = true;
+                }
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Moves a block one column left.
+     *
+     * @param integer $row  A layout row.
+     * @param integer $col  A layout column.
+     *
+     * @throws Horde_Exception
+     */
+    public function moveLeft($row, $col)
+    {
+        if ($this->colExists($col - 1)) {
+            $height = $this->getHeight($row, $col);
+            // See if there's room to move into.
+            for ($i = 0; $i < $height; $i++) {
+                if (!$this->isEmpty($row + $i, $col - 1)) {
+                    $in_way = $this->getBlockAt($row + $i, $col - 1);
+                    if (!is_null($in_way) &&
+                        $in_way[0] == $row &&
+                        $this->getHeight($in_way[0], $in_way[1]) == $height) {
+                        // We need to swap the blocks.
+                        $rec1 = Horde_Array::getRectangle($this->_layout, $row, $col,
+                                                          $this->getHeight($row, $col), $this->getWidth($row, $col));
+                        $rec2 = Horde_Array::getRectangle($this->_layout, $in_way[0], $in_way[1],
+                                                          $this->getHeight($in_way[0], $in_way[1]), $this->getWidth($in_way[0], $in_way[1]));
+                        for ($j = 0; $j < count($rec1); $j++) {
+                            for ($k = 0; $k < count($rec1[$j]); $k++) {
+                                $this->_layout[$in_way[0] + $j][$in_way[1] + $k] = $rec1[$j][$k];
+                            }
+                        }
+                        for ($j = 0; $j < count($rec2); $j++) {
+                            for ($k = 0; $k < count($rec2[$j]); $k++) {
+                                $this->_layout[$in_way[0] + $j][$in_way[1] + count($rec1[$j]) + $k] = $rec2[$j][$k];
+                            }
+                        }
+                        $this->_changed_row = $in_way[0];
+                        $this->_changed_col = $in_way[1];
+                        return;
+                    }
+                    // No where to go
+                    throw new Horde_Exception('Shrink or move neighboring block(s) out of the way first', 'horde.warning');
+                }
+            }
+
+            $lastcol = $col + $this->getWidth($row, $col) - 1;
+            for ($i = 0; $i < $height; $i++) {
+                if (!isset($this->_layout[$row + $i][$col])) {
+                    continue;
+                }
+                $prev = $this->_layout[$row + $i][$col];
+                // Move left hand edge
+                $this->_layout[$row + $i][$col - 1] = $prev;
+                $this->_layout[$row + $i][$col] = 'covered';
+                // Move right hand edge
+                $this->_layout[$row + $i][$lastcol] = 'empty';
+            }
+
+            if (!$this->colExists($lastcol + 1)) {
+                // Was on the right-most column
+                $this->removeColIfEmpty($lastcol);
+            }
+
+            $this->_changed_row = $row;
+            $this->_changed_col = $col - 1;
+        }
+    }
+
+    /**
+     * Moves a block one column right.
+     *
+     * @param integer $row  A layout row.
+     * @param integer $col  A layout column.
+     *
+     * @throws Horde_Exception
+     */
+    public function moveRight($row, $col)
+    {
+        $height = $this->getHeight($row, $col);
+        $lastcol = $col + $this->getWidth($row, $col);
+        if ($this->colExists($lastcol)) {
+            // See if there's room to move into.
+            for ($i = 0; $i < $height; $i++) {
+                if (!$this->isEmpty($row + $i, $lastcol)) {
+                    $in_way = $this->getBlockAt($row + $i, $lastcol);
+                    if (!is_null($in_way) &&
+                        $in_way[0] == $row &&
+                        $this->getHeight($in_way[0], $in_way[1]) == $height) {
+                        // We need to swap the blocks.
+                        $rec1 = Horde_Array::getRectangle($this->_layout, $row, $col,
+                                                          $this->getHeight($row, $col), $this->getWidth($row, $col));
+                        $rec2 = Horde_Array::getRectangle($this->_layout, $in_way[0], $in_way[1],
+                                                          $this->getHeight($in_way[0], $in_way[1]), $this->getWidth($in_way[0], $in_way[1]));
+                        for ($j = 0; $j < count($rec2); $j++) {
+                            for ($k = 0; $k < count($rec2[$j]); $k++) {
+                                $this->_layout[$row + $j][$col + $k] = $rec2[$j][$k];
+                            }
+                        }
+                        for ($j = 0; $j < count($rec1); $j++) {
+                            for ($k = 0; $k < count($rec1[$j]); $k++) {
+                                $this->_layout[$row + $j][$col + count($rec2[$j]) + $k] = $rec1[$j][$k];
+                            }
+                        }
+                        $this->_changed_row = $in_way[0];
+                        $this->_changed_col = $in_way[1];
+                        return;
+                    }
+                    // No where to go
+                    throw new Horde_Exception('Shrink or move neighboring block(s) out of the way first', 'horde.warning');
+                }
+            }
+        } else {
+            // Make room to move into.
+            $this->addCol($lastcol);
+        }
+
+        for ($i = 0; $i < $height; $i++) {
+            if (!isset($this->_layout[$row + $i][$col])) {
+                continue;
+            }
+            $prev = $this->_layout[$row + $i][$col];
+            // Move right hand edge
+            $this->_layout[$row + $i][$lastcol] = 'covered';
+            // Move left hand edge
+            $this->_layout[$row + $i][$col + 1] = $prev;
+            $this->_layout[$row + $i][$col] = 'empty';
+        }
+
+        $this->_changed_row = $row;
+        $this->_changed_col = $col + 1;
+    }
+
+    /**
+     * Moves all blocks after a certain column one column right.
+     *
+     * @param integer $col  A layout column.
+     *
+     * @return boolean  True if all columns could be moved right.
+     */
+    public function moveRightAfter($col)
+    {
+        $moved = array();
+        for ($x = $this->_columns - 1; $x > $col; $x--) {
+            for ($y = 0; $y < count($this->_layout); $y++) {
+                $block = $this->getBlockAt($y, $x);
+                if (empty($block)) {
+                    continue;
+                }
+                if (empty($moved[$block[1] . ':' . $block[0]])) {
+                    try {
+                        $result = $this->moveRight($block[0], $block[1]);
+                    } catch (Horde_Exception $e) {
+                        return false;
+                    }
+                    $moved[($block[1] + 1) . ':' . $block[0]] = true;
+                }
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Makes a block one row taller by moving the top up.
+     *
+     * @param integer $row  A layout row.
+     * @param integer $col  A layout column.
+     *
+     * @throws Horde_Exception
+     */
+    public function expandUp($row, $col)
+    {
+        if ($this->rowExists($row - 1)) {
+            $width = $this->getWidth($row, $col);
+            // See if there's room to expand into
+            for ($i = 0; $i < $width; $i++) {
+                if (!$this->isEmpty($row - 1, $col + $i)) {
+                    if (!$this->moveDownBelow($row - 1)) {
+                        throw new Horde_Exception('Shrink or move neighboring block(s) out of the way first', 'horde.warning');
+                    } else {
+                        $row++;
+                    }
+                }
+            }
+
+            for ($i = 0; $i < $width; $i++) {
+                $this->_layout[$row - 1][$col + $i] = $this->_layout[$row][$col + $i];
+                $this->_layout[$row][$col + $i] = 'covered';
+            }
+            $this->_layout[$row - 1][$col]['height'] = $this->getHeight($row - 1, $col) + 1;
+
+            $this->_changed_row = $row - 1;
+            $this->_changed_col = $col;
+        }
+    }
+
+    /**
+     * Makes a block one row taller by moving the bottom down.
+     *
+     * @param integer $row  A layout row.
+     * @param integer $col  A layout column.
+     *
+     * @throws Horde_Exception
+     */
+    public function expandDown($row, $col)
+    {
+        $width = $this->getWidth($row, $col);
+        $lastrow = $row + $this->getHeight($row, $col) - 1;
+        if (!$this->rowExists($lastrow + 1)) {
+            // Add a new row.
+            $this->addRow($lastrow + 1);
+            for ($i = 0; $i < $width; $i++) {
+                $this->_layout[$lastrow + 1][$col + $i] = 'covered';
+            }
+            $this->_layout[$row][$col]['height'] = $this->getHeight($row, $col) + 1;
+        } else {
+            // See if there's room to expand into
+            for ($i = 0; $i < $width; $i++) {
+                if (!$this->isEmpty($lastrow + 1, $col + $i)) {
+                    if (!$this->moveDownBelow($lastrow)) {
+                        throw new Horde_Exception('Shrink or move neighboring block(s) out of the way first', 'horde.warning');
+                    }
+                }
+            }
+
+            for ($i = 0; $i < $width; $i++) {
+                $this->_layout[$lastrow + 1][$col + $i] = 'covered';
+            }
+            $this->_layout[$row][$col]['height'] = $this->getHeight($row, $col) + 1;
+        }
+
+        $this->_changed_row = $row;
+        $this->_changed_col = $col;
+    }
+
+    /**
+     * Makes a block one column wider by moving the left side out.
+     *
+     * @param integer $row  A layout row.
+     * @param integer $col  A layout column.
+     *
+     * @throws Horde_Exception
+     */
+    public function expandLeft($row, $col)
+    {
+        if ($this->colExists($col - 1)) {
+            $height = $this->getHeight($row, $col);
+            // See if there's room to expand into
+            for ($i = 0; $i < $height; $i++) {
+                if (!$this->isEmpty($row + $i, $col - 1)) {
+                    if (!$this->moveRightAfter($col - 1)) {
+                        throw new Horde_Exception('Shrink or move neighboring block(s) out of the way first', 'horde.warning');
+                    } else {
+                        $col++;
+                    }
+                }
+            }
+
+            for ($i = 0; $i < $height; $i++) {
+                $this->_layout[$row + $i][$col - 1] = $this->_layout[$row + $i][$col];
+                $this->_layout[$row + $i][$col] = 'covered';
+            }
+            $this->_layout[$row][$col - 1]['width'] = $this->getWidth($row, $col - 1) + 1;
+
+            $this->_changed_row = $row;
+            $this->_changed_col = $col - 1;
+        }
+    }
+
+    /**
+     * Makes a block one column wider by moving the right side out.
+     *
+     * @param integer $row  A layout row.
+     * @param integer $col  A layout column.
+     *
+     * @throws Horde_Exception
+     */
+    public function expandRight($row, $col)
+    {
+        $height = $this->getHeight($row, $col);
+        $lastcol = $col + $this->getWidth($row, $col) - 1;
+        if ($this->colExists($lastcol + 1)) {
+            // See if there's room to expand into
+            for ($i = 0; $i < $height; $i++) {
+                if (!$this->isEmpty($row + $i, $lastcol + 1)) {
+                    if (!$this->moveRightAfter($lastcol)) {
+                        throw new Horde_Exception('Shrink or move neighbouring block(s) out of the way first', 'horde.warning');
+                    }
+                }
+            }
+
+            for ($i = 0; $i < $height; $i++) {
+                $this->_layout[$row + $i][$lastcol + 1] = 'covered';
+            }
+            $this->_layout[$row][$col]['width'] = $this->getWidth($row, $col) + 1;
+        } else {
+            // Add new column
+            $this->addCol($lastcol + 1);
+            for ($i = 0; $i < $height; $i++) {
+                $this->_layout[$row + $i][$lastcol + 1] = 'covered';
+            }
+            $this->_layout[$row][$col]['width'] = $this->getWidth($row, $col) + 1;
+        }
+
+        $this->_changed_row = $row;
+        $this->_changed_col = $col;
+    }
+
+    /**
+     * Makes a block one row lower by moving the top down.
+     *
+     * @param integer $row  A layout row.
+     * @param integer $col  A layout column.
+     */
+    public function shrinkUp($row, $col)
+    {
+        if ($this->getHeight($row, $col) > 1) {
+            $width = $this->getWidth($row, $col);
+            for ($i = 0; $i < $width; $i++) {
+                $this->_layout[$row + 1][$col + $i] = $this->_layout[$row][$col + $i];
+                $this->_layout[$row][$col + $i] = 'empty';
+            }
+            $this->_layout[$row + 1][$col]['height'] = $this->getHeight($row + 1, $col) - 1;
+
+            $this->_changed_row = $row + 1;
+            $this->_changed_col = $col;
+        }
+    }
+
+    /**
+     * Makes a block one row lower by moving the bottom up.
+     *
+     * @param integer $row  A layout row.
+     * @param integer $col  A layout column.
+     */
+    public function shrinkDown($row, $col)
+    {
+        if ($this->getHeight($row, $col) > 1) {
+            $lastrow = $row + $this->getHeight($row, $col) - 1;
+            $width = $this->getWidth($row, $col);
+            for ($i = 0; $i < $width; $i++) {
+                $this->_layout[$lastrow][$col + $i] = 'empty';
+            }
+            $this->_layout[$row][$col]['height'] = $this->getHeight($row, $col) - 1;
+            if (!$this->rowExists($lastrow + 1)) {
+                // Was on the bottom row
+                $this->removeRowIfEmpty($lastrow);
+            }
+
+            $this->_changed_row = $row;
+            $this->_changed_col = $col;
+        }
+    }
+
+    /**
+     * Makes a block one column narrower by moving the left side in.
+     *
+     * @param integer $row  A layout row.
+     * @param integer $col  A layout column.
+     */
+    public function shrinkLeft($row, $col)
+    {
+        if ($this->getWidth($row, $col) > 1) {
+            $height = $this->getHeight($row, $col);
+            for ($i = 0; $i < $height; $i++) {
+                $this->_layout[$row + $i][$col + 1] = $this->_layout[$row + $i][$col];
+                $this->_layout[$row + $i][$col] = 'empty';
+            }
+            $this->_layout[$row][$col + 1]['width'] = $this->getWidth($row, $col + 1) - 1;
+
+            $this->_changed_row = $row;
+            $this->_changed_col = $col + 1;
+        }
+    }
+
+    /**
+     * Makes a block one column narrower by moving the right side in.
+     *
+     * @param integer $row  A layout row.
+     * @param integer $col  A layout column.
+     */
+    public function shrinkRight($row, $col)
+    {
+        if ($this->getWidth($row, $col) > 1) {
+            $lastcol = $col + $this->getWidth($row, $col) - 1;
+            $height = $this->getHeight($row, $col);
+            for ($i = 0; $i < $height; $i++) {
+                $this->_layout[$row + $i][$lastcol] = 'empty';
+            }
+            $this->_layout[$row][$col]['width'] = $this->getWidth($row, $col) - 1;
+            $this->removeColIfEmpty($lastcol);
+
+            $this->_changed_row = $row;
+            $this->_changed_col = $col;
+        }
+    }
+
+}
diff --git a/framework/Block/lib/Horde/Block/Layout/View.php b/framework/Block/lib/Horde/Block/Layout/View.php
new file mode 100644 (file)
index 0000000..45a0272
--- /dev/null
@@ -0,0 +1,142 @@
+<?php
+/**
+ * The Horde_Block_Layout_View class represents the user defined portal layout.
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author   Mike Cochrane <mike@graftonhall.co.nz>
+ * @author   Jan Schneider <jan@horde.org>
+ * @category Horde
+ * @package  Horde_Block
+ */
+class Horde_Block_Layout_View extends Horde_Block_Layout
+{
+    /**
+     * The current block layout.
+     *
+     * @var array
+     */
+    protected $_layout = array();
+
+    /**
+     * All applications used in this layout.
+     *
+     * @var array
+     */
+    protected $_applications = array();
+
+    /**
+     * CSS link tags pulled out of block content.
+     *
+     * @var array
+     */
+    protected $_linkTags = array();
+
+    /**
+     * Constructor.
+     */
+    public function __construct($layout = array(), $editUrl = '',
+                                $viewUrl = '')
+    {
+        $this->_layout = $layout;
+        $this->_editUrl = $editUrl;
+        $this->_viewUrl = $viewUrl;
+    }
+
+    /**
+     * Render the current layout as HTML.
+     *
+     * @return string  HTML layout.
+     */
+    public function toHtml()
+    {
+        $browser = Horde_Browser::singleton();
+        $tplDir = $GLOBALS['registry']->get('templates', 'horde');
+        $interval = $GLOBALS['prefs']->getValue('summary_refresh_time');
+
+        $html = '<table class="nopadding" cellspacing="8" width="100%">';
+
+        $covered = array();
+        foreach ($this->_layout as $row_num => $row) {
+            $width = floor(100 / count($row));
+            $html .= '<tr>';
+            foreach ($row as $col_num => $item) {
+                if (isset($covered[$row_num]) && isset($covered[$row_num][$col_num])) {
+                    continue;
+                }
+                if (is_array($item)) {
+                    $this->_applications[$item['app']] = $item['app'];
+                    $block = Horde_Block_Collection::getBlock($item['app'], $item['params']['type'], $item['params']['params'], $row_num, $col_num);
+                    $rowspan = $item['height'];
+                    $colspan = $item['width'];
+                    for ($i = 0; $i < $item['height']; $i++) {
+                        if (!isset($covered[$row_num + $i])) {
+                            $covered[$row_num + $i] = array();
+                        }
+                        for ($j = 0; $j < $item['width']; $j++) {
+                            $covered[$row_num + $i][$col_num + $j] = true;
+                        }
+                    }
+                    if ($block instanceof PEAR_Error) {
+                        $header = _("Error");
+                        $content = $block->getMessage();
+                        ob_start();
+                        include $tplDir . '/portal/block.inc';
+                        $html .= ob_get_clean();
+                    } elseif ($block instanceof Horde_Block) {
+                        $header = $block->getTitle();
+                        $content = $block->getContent();
+                        if ($content instanceof PEAR_Error) {
+                            $content = $content->getMessage();
+                        }
+                        if ($browser->hasFeature('xmlhttpreq')) {
+                            $refresh_time = isset($item['params']['params']['_refresh_time']) ? $item['params']['params']['_refresh_time'] : $interval;
+                        }
+                        ob_start();
+                        include $tplDir . '/portal/block.inc';
+                        $html .= ob_get_clean();
+                    } else {
+                        $html .= '<td width="' . ($width * $colspan) . '%">&nbsp;</td>';
+                    }
+                } else {
+                    $html .= '<td width="' . ($width) . '%">&nbsp;</td>';
+                }
+            }
+            $html .= '</tr>';
+        }
+        $html .= '</table>';
+
+        // Strip any CSS <link> tags out of the returned content so
+        // they can be handled seperately.
+        if (preg_match_all('/<link .*?rel="stylesheet".*?\/>/', $html, $links)) {
+            $html = str_replace($links[0], '', $html);
+            $this->_linkTags = $links[0];
+        }
+
+        return $html;
+    }
+
+    /**
+     * Get any link tags found in the view.
+     *
+     * @return TODO
+     */
+    public function getLinkTags()
+    {
+        return $this->_linkTags;
+    }
+
+    /**
+     * Return a list of all the applications used by blocks in this layout.
+     *
+     * @return array  List of applications.
+     */
+    public function getApplications()
+    {
+        return array_keys($this->_applications);
+    }
+
+}
diff --git a/framework/Block/lib/Horde/Block/Ui.php b/framework/Block/lib/Horde/Block/Ui.php
new file mode 100644 (file)
index 0000000..76ae029
--- /dev/null
@@ -0,0 +1,114 @@
+<?php
+/**
+ * Class for setting up Horde Blocks using the Horde_Form:: classes.
+ *
+ * Copyright 2004-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author   Marko Djukic <marko@oblo.com>
+ * @category Horde
+ * @package  Horde_Block
+ */
+class Horde_Block_UI
+{
+    /**
+     * TODO
+     *
+     * @var array
+     */
+    protected $_blocks = array();
+
+    /**
+     * TODO
+     *
+     * @var Horde_Form
+     */
+    protected $_form = null;
+
+    /**
+     * TODO
+     *
+     * @var Horde_Variables
+     */
+    protected $_vars = null;
+
+    /**
+     * Constructor.
+     */
+    public function __construct()
+    {
+        $this->_blocks = Horde_Block_Collection::singleton();
+    }
+
+    /**
+     * TODO
+     */
+    public function setForm($form)
+    {
+        $this->_form = $form;
+    }
+
+    /**
+     * TODO
+     */
+    public function setVars($vars)
+    {
+        $this->_vars = $vars;
+    }
+
+    /**
+     * TODO
+     */
+    public function setupEditForm($field = 'block')
+    {
+        if (is_null($this->_vars)) {
+            /* No existing vars set, get them now. */
+            $this->setVars(Horde_Variables::getDefaultVariables());
+        }
+
+        if (!($this->_form instanceof Horde_Form)) {
+            /* No existing valid form object set so set up a new one. */
+            $this->setForm(new Horde_Form($this->_vars, _("Edit Block")));
+        }
+
+        /* Get the current value of the block selection. */
+        $value = $this->_vars->get($field);
+
+        /* Field to select apps. */
+        $apps = $this->_blocks->getBlocksList();
+        $v = $this->_form->addVariable(_("Application"), $field . '[app]', 'enum', true, false, null, array($apps));
+        $v->setOption('trackchange', true);
+
+        if (empty($value['app'])) {
+            return;
+        }
+
+        /* If a block has been selected, output any options input. */
+        list($app, $block) = explode(':', $value['app']);
+
+        /* Get the options for the requested block. */
+        $options = $this->_blocks->getParams($app, $block);
+
+        /* Go through the options for this block and set up any required
+         * extra input. */
+        foreach ($options as $option) {
+            $name = $this->_blocks->getParamName($app, $block, $option);
+            $type = $this->_blocks->getOptionType($app, $block, $option);
+            $required = $this->_blocks->getOptionRequired($app, $block, $option);
+            $values = $this->_blocks->getOptionValues($app, $block, $option);
+            /* TODO: the setting 'string' should be changed in all blocks
+             * to 'text' so that it conforms with Horde_Form syntax. */
+            if ($type == 'string') {
+                $type = 'text';
+            }
+            $params = array();
+            if ($type == 'enum' || $type == 'mlenum') {
+                $params = array($values, true);
+            }
+            $this->_form->addVariable($name, $field . '[options][' . $option . ']', $type, $required, false, null, $params);
+        }
+    }
+
+}
diff --git a/framework/Block/package.xml b/framework/Block/package.xml
new file mode 100644 (file)
index 0000000..f3963b9
--- /dev/null
@@ -0,0 +1,119 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package packagerversion="1.4.9" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
+http://pear.php.net/dtd/tasks-1.0.xsd
+http://pear.php.net/dtd/package-2.0
+http://pear.php.net/dtd/package-2.0.xsd">
+ <name>Block</name>
+ <channel>pear.horde.org</channel>
+ <summary>Horde Block API</summary>
+ <description>The Horde_Block API provides a mechanism for displaying content
+ blocks from numerous Horde applications or other sources, manipulating those
+ blocks, configuring them, etc.
+ </description>
+ <lead>
+  <name>Chuck Hagenbuch</name>
+  <user>chuck</user>
+  <email>chuck@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <lead>
+  <name>Jan Schneider</name>
+  <user>jan</user>
+  <email>jan@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <date>2009-12-01</date>
+ <version>
+  <release>0.1.0</release>
+  <api>0.1.0</api>
+ </version>
+ <stability>
+  <release>beta</release>
+  <api>beta</api>
+ </stability>
+ <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+ <notes>* Initial Horde 4 package.
+ </notes>
+ <contents>
+  <dir name="/">
+   <dir name="lib">
+    <dir name="Horde">
+     <dir name="Block">
+      <dir name="Layout">
+       <file name="Manager.php" role="php" />
+       <file name="View.php" role="php" />
+      </dir> <!-- /lib/Horde/Block/Layout -->
+      <file name="Collection.php" role="php" />
+      <file name="Layout.php" role="php" />
+      <file name="Ui.php" role="php" />
+     </dir> <!-- /lib/Horde/Block -->
+     <file name="Block.php" role="php" />
+    </dir> <!-- /lib/Horde -->
+   </dir> <!-- /lib -->
+  </dir> <!-- / -->
+ </contents>
+ <dependencies>
+  <required>
+   <php>
+    <min>5.2.0</min>
+   </php>
+   <pearinstaller>
+    <min>1.7.0</min>
+   </pearinstaller>
+   <package>
+    <name>Core</name>
+    <channel>pear.horde.org</channel>
+   </package>
+   <package>
+    <name>Util</name>
+    <channel>pear.horde.org</channel>
+   </package>
+  </required>
+  <optional>
+   <extension>
+    <name>gettext</name>
+   </extension>
+  </optional>
+ </dependencies>
+ <phprelease>
+  <filelist>
+   <install name="lib/Horde/Block/Layout/Manager.php" as="Horde/Block/Layout/Manager.php" />
+   <install name="lib/Horde/Block/Layout/View.php" as="Horde/Block/Layout/View.php" />
+   <install name="lib/Horde/Block/Collection.php" as="Horde/Block/Collection.php" />
+   <install name="lib/Horde/Block/Layout.php" as="Horde/Block/Layout.php" />
+   <install name="lib/Horde/Block/Ui.php" as="Horde/Block/Ui.php" />
+   <install name="lib/Horde/Block.php" as="Horde/Block.php" />
+  </filelist>
+ </phprelease>
+ <changelog>
+  <release>
+   <version>
+    <release>0.0.2</release>
+    <api>0.0.2</api>
+   </version>
+   <date>2006-05-08</date>
+   <time>17:49:36</time>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>Converted to package.xml 2.0 for pear.horde.org
+   </notes>
+  </release>
+  <release>
+   <version>
+    <release>0.0.1</release>
+    <api>0.0.1</api>
+   </version>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <date>2004-02-13</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>Initial packaging
+   </notes>
+  </release>
+ </changelog>
+</package>