From 0d00b758975eb54f57460eb9d3b60740deefac58 Mon Sep 17 00:00:00 2001 From: Michael M Slusarz Date: Tue, 1 Dec 2009 18:36:22 -0700 Subject: [PATCH] Import Horde_Block from CVS HEAD --- framework/Block/lib/Horde/Block.php | 235 ++++ framework/Block/lib/Horde/Block/Collection.php | 472 +++++++ framework/Block/lib/Horde/Block/Layout.php | 126 ++ framework/Block/lib/Horde/Block/Layout/Manager.php | 1384 ++++++++++++++++++++ framework/Block/lib/Horde/Block/Layout/View.php | 142 ++ framework/Block/lib/Horde/Block/Ui.php | 114 ++ framework/Block/package.xml | 119 ++ 7 files changed, 2592 insertions(+) create mode 100644 framework/Block/lib/Horde/Block.php create mode 100644 framework/Block/lib/Horde/Block/Collection.php create mode 100644 framework/Block/lib/Horde/Block/Layout.php create mode 100644 framework/Block/lib/Horde/Block/Layout/Manager.php create mode 100644 framework/Block/lib/Horde/Block/Layout/View.php create mode 100644 framework/Block/lib/Horde/Block/Ui.php create mode 100644 framework/Block/package.xml diff --git a/framework/Block/lib/Horde/Block.php b/framework/Block/lib/Horde/Block.php new file mode 100644 index 000000000..9124415b9 --- /dev/null +++ b/framework/Block/lib/Horde/Block.php @@ -0,0 +1,235 @@ + + * @author Jan Schneider + * @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 index 000000000..6878e49ba --- /dev/null +++ b/framework/Block/lib/Horde/Block/Collection.php @@ -0,0 +1,472 @@ + + * @author Jan Schneider + * @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 = '\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('', $param_id, $checked); + break; + + case 'enum': + $widget = sprintf(''; + break; + + case 'multienum': + $widget = sprintf(''; + 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('
\n"; + + $widget .= sprintf("
\n"; + break; + + case 'int': + case 'text': + $widget = sprintf('', $param_id, !isset($val[$param_id]) ? $param['default'] : $val[$param_id]); + break; + + case 'password': + $widget = sprintf('', $param_id, !isset($val[$param_id]) ? $param['default'] : $val[$param_id]); + break; + + case 'error': + $widget = '' . $param['default'] . ''; + 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 index 000000000..27ac4f26f --- /dev/null +++ b/framework/Block/lib/Horde/Block/Layout.php @@ -0,0 +1,126 @@ + + * @author Jan Schneider + * @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')) + . ''; + } + 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')) + . ''; + } + 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 index 000000000..d99fcca22 --- /dev/null +++ b/framework/Block/lib/Horde/Block/Layout/Manager.php @@ -0,0 +1,1384 @@ + + * @author Jan Schneider + * @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')) . ''; + } + + /** + * 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 index 000000000..45a027219 --- /dev/null +++ b/framework/Block/lib/Horde/Block/Layout/View.php @@ -0,0 +1,142 @@ + + * @author Jan Schneider + * @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 = ''; + + $covered = array(); + foreach ($this->_layout as $row_num => $row) { + $width = floor(100 / count($row)); + $html .= ''; + 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 .= ''; + } + } else { + $html .= ''; + } + } + $html .= ''; + } + $html .= '
  
'; + + // Strip any CSS tags out of the returned content so + // they can be handled seperately. + if (preg_match_all('//', $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 index 000000000..76ae029da --- /dev/null +++ b/framework/Block/lib/Horde/Block/Ui.php @@ -0,0 +1,114 @@ + + * @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 index 000000000..f3963b958 --- /dev/null +++ b/framework/Block/package.xml @@ -0,0 +1,119 @@ + + + Block + pear.horde.org + Horde Block API + The Horde_Block API provides a mechanism for displaying content + blocks from numerous Horde applications or other sources, manipulating those + blocks, configuring them, etc. + + + Chuck Hagenbuch + chuck + chuck@horde.org + yes + + + Jan Schneider + jan + jan@horde.org + yes + + 2009-12-01 + + 0.1.0 + 0.1.0 + + + beta + beta + + LGPL + * Initial Horde 4 package. + + + + + + + + + + + + + + + + + + + + + + + 5.2.0 + + + 1.7.0 + + + Core + pear.horde.org + + + Util + pear.horde.org + + + + + gettext + + + + + + + + + + + + + + + + + 0.0.2 + 0.0.2 + + 2006-05-08 + + + alpha + alpha + + LGPL + Converted to package.xml 2.0 for pear.horde.org + + + + + 0.0.1 + 0.0.1 + + + alpha + alpha + + 2004-02-13 + LGPL + Initial packaging + + + + -- 2.11.0