egroupware/home/inc/class.home_ui.inc.php
Nathan Gray dd0e95d232 More Home progress (favorites):
- Put show header button into header
 - Fix mixing columns between multiple favorites on home
 - Fix changing column spacing when hiding header
 - Fix add favorite from context menu didn't load properly
2014-11-25 21:50:42 +00:00

456 lines
13 KiB
PHP

<?php
/**
* EGroupware - Home - user interface
*
* @link www.egroupware.org
* @author Nathan Gray
* @copyright (c) 2013 by Nathan Gray
* @package home
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @version $Id$
*/
/**
* General user interface object of the Home app
*
* For the purposes of the Home application, a Portlet is [part of] an application that provides
* a specific piece of content to be included as part of the Home page.
* See /home/js/Portlet.js.
*
* While Home is not WSRP complient, it does use many of the ideas, and may someday be,
* if someone wants to fully implement it.
* @link http://docs.oasis-open.org/wsrp/v2/wsrp-2.0-spec-os-01.html
*/
class home_ui
{
public $public_functions = array(
'index' => true
);
/**
* Main UI - generates the container, and aggregates all
* the portlets from the applications
*/
public function index($content = array())
{
// CSS for Gridster grid layout
egw_framework::includeCSS('/phpgwapi/js/jquery/gridster/jquery.gridster.css');
$template = new etemplate_new('home.index');
// Get a list of portlets
$content = array(
'portlets' => $this->get_user_portlets($template)
);
$template->setElementAttribute('home.index','actions',$this->get_actions());
$GLOBALS['egw_info']['flags']['app_header'] = lang('home');
$GLOBALS['egw_info']['flags']['currentapp'] = 'home';
// Main screen message
translation::add_app('mainscreen');
$greeting = translation::translate('mainscreen_message',false,'');
if($greeting == 'mainscreen_message'|| empty($greeting))
{
translation::add_app('mainscreen','en'); // trying the en one
$greeting = translation::translate('mainscreen_message',false,'');
}
if(!($greeting == 'mainscreen_message'|| empty($greeting)))
{
$content['mainscreen_message'] = $greeting;
}
$template->exec('home.home_ui.index', $content);
// Now run the portlets themselves
foreach($content['portlets'] as $portlet => $p_data)
{
$id = $p_data['id'];
if(!$id) continue;
$portlet = $this->get_portlet($id, $p_data, $content, $attrs, true);
}
}
/**
* Get a list of actions on the whole home page. Each portlet also has
* its own actions
*
* @return array of actions
*/
protected function get_actions()
{
$portlets = $this->get_portlet_list();
$add_portlets = $portlets;
$change_for_add = function(&$add_portlets) use (&$change_for_add)
{
foreach($add_portlets as $id => &$add)
{
if(is_array($add['children']))
{
$change_for_add($add['children']);
}
if($id && !$add['children'])
{
$add['id'] = 'add_' . $id;
$add['class'] = 'add_'.$id;
}
}
};
$change_for_add($add_portlets);
$actions = array(
'add' => array(
'type' => 'popup',
'caption' => 'Add',
'onExecute' => 'javaScript:app.home.add',
'children' => $add_portlets
),
// Favorites are sortable which needs special handling,
// handled directly through jQuery
);
// Add all known portlets as drop actions too. If there are multiple matches, there will be a menu
$drop_execute = 'javaScript:app.home.add_from_drop';
foreach($portlets as $app => &$children)
{
// Home portlets - uses link system, so all apps that support that are accepted
if(!$children['children'])
{
$children['class'] = $app;
$children['onExecute'] = $drop_execute;
$children['acceptedTypes'] = array('file','link');
$children['type'] = 'drop';
$actions["drop_$app"] = $children;
}
else
{
foreach($children['children'] as $portlet => $app_portlet)
{
if(!is_array($app_portlet)) continue;
$app_portlet['class'] = $portlet;
$app_portlet['id'] = 'drop_' . $app_portlet['id'];
$app_portlet['onExecute'] = $drop_execute;
$app_portlet['acceptedTypes'] = $app;
$app_portlet['type'] = 'drop';
$actions["drop_$portlet"] = $app_portlet;
}
}
}
return $actions;
}
/**
* Get a list of the user's portlets, and their associated values & settings, for display
*
* Actual portlet content is provided by each portlet.
* @param template etemplate so attributes can be set
*/
protected function get_user_portlets(etemplate_new &$template)
{
$portlets = array();
foreach((array)$GLOBALS['egw_info']['user']['preferences']['home']['portlets'] as $id => $context)
{
if(!$id || in_array($id, array_keys($GLOBALS['egw_info']['user']['apps']))) continue;
$classname = $context['class'];
$portlet = new $classname($context);
$desc = $portlet->get_description();
$portlet_content = array(
'id' => $id
) + $desc + $context;
// Get settings
// Exclude common attributes changed through UI and settings lacking a type
$settings = $portlet->get_properties();
foreach($settings as $key => $setting)
{
if(is_array($setting) && !array_key_exists('type',$setting)) unset($settings[$key]);
}
$settings += $context;
foreach(home_portlet::$common_attributes as $attr)
{
unset($settings[$attr]);
}
$portlet_content['settings'] = $settings;
// Set actions
// Must be after settings so actions can take settings into account
$template->setElementAttribute("portlets[" . count($portlets) . "[$id]", 'actions', $portlet->get_actions());
$portlets[] = $portlet_content;
}
// Add in legacy HTML home bits
// TODO: DOM IDs still collide
//$this->get_legacy_portlets($portlets, $attributes);
return $portlets;
}
/**
* Load the needed info for one portlet, given the context
*
* @param context Array Settings to customize the portlet instance (size, entry, etc)
* These are specific values for the portlet's properties.
* @param content String HTML fragment to be displayed - will be set by the portlet
* @param attributes Array Settings that can be customized on a per-portlet basis - will be set
* @param full_exec Boolean If set, the portlet etemplates should use mode 2, if not use mode -1
* @return home_portlet The portlet object that created the content
*/
protected function get_portlet($id, &$context, &$content, &$attributes, $full_exec = false)
{
if(!$context['class']) $context['class'] = 'home_link_portlet';
// This should be set already, but just in case the execution path
// is different from normal...
if(egw_json_response::isJSONResponse())
{
$GLOBALS['egw']->framework->response = egw_json_response::get();
}
$classname = $context['class'];
$portlet = new $classname($context, $full_exec);
$desc = $portlet->get_description();
// Pre-set up etemplate so it only needs done once
$dom_id = 'home-index_'.$id.'_content';
$etemplate = new etemplate_new();
// Exclude common attributes changed through UI and settings lacking a type
$settings = $portlet->get_properties();
foreach($settings as $key => $setting)
{
if(is_array($setting) && !array_key_exists('type',$setting)) unset($settings[$key]);
}
$settings += $context;
foreach(home_portlet::$common_attributes as $attr)
{
unset($settings[$attr]);
}
$attributes = array(
'title' => $desc['title'],
'color' => $settings['color'],
'settings' => $settings,
'actions' => $portlet->get_actions(),
);
// Set any provided common attributes (size, etc)
foreach(home_portlet::$common_attributes as $name)
{
if(array_key_exists($name, $context))
{
$attributes[$name] = $context[$name];
}
}
foreach($attributes as $attr => $value)
{
$etemplate->setElementAttribute($id, $attr, $value);
}
if($full_exec)
{
$content = $portlet->exec($id, $etemplate, $full_exec ? 2 : -1);
}
return $portlet;
}
/**
* Get a list of pre-etemplate2 home hook content according to the individual
* application preferences. If we find a preference that indicates the user
* wants some content, we make a portlet for that app using the home_legacy_portlet,
* which fetches content from the home hook.
*/
protected function get_legacy_portlets(&$content, &$attributes)
{
$sorted_apps = array_keys($GLOBALS['egw_info']['user']['apps']);
$portal_oldvarnames = array('mainscreen_showevents', 'homeShowEvents','homeShowLatest','mainscreen_showmail','mainscreen_showbirthdays','mainscreen_show_new_updated', 'homepage_display');
foreach($sorted_apps as $appname)
{
// If there's already [new] settings, or no preference, skip it
if($content[$appname]) continue;
$no_pref = true;
foreach($portal_oldvarnames as $varcheck)
{
$thisd = $GLOBALS['egw_info']['user']['preferences'][$appname][$varcheck];
if(!(int)$thisd && $thisd)
{
$no_pref = false;
break;
}
}
if($no_pref || !$GLOBALS['egw']->hooks->hook_exists('home', $appname))
{
continue;
}
$context = array(
'class' => 'home_legacy_portlet', 'app' => $appname,
'width' => 8, 'height' => 3
);
$_content = '';
$_attributes = array();
$this->get_portlet($appname, $context, $_content, $_attributes);
if(trim($_content))
{
$content[$appname] = $_content;
$attributes[$appname] = $_attributes;
}
}
}
/**
* Get a list of all available portlets for add menu
*/
protected function get_portlet_list()
{
$list = array();
$list = egw_cache::getTree('home', 'portlet_classes', function() {
$list = array();
$classes = array();
// Ignore some problem files and base classes that shouldn't be options
$ignore = array(
'.','..',
'class.home_legacy_portlet.inc.php',
'class.home_favorite_portlet.inc.php'
);
// Look through all known classes for portlets - for now, they need 'portlet' in the file name
foreach($GLOBALS['egw_info']['apps'] as $appname => $app)
{
if(in_array($appname, array('phpgwapi', 'felamimail'))) continue;
$files = (array)@scandir(EGW_SERVER_ROOT . '/'.$appname .'/inc/');
if(!$files) continue;
foreach($files as $entry)
{
if (!in_array($entry, $ignore) && substr($entry,-8) == '.inc.php' && strpos($entry,'portlet'))
{
list(,$classname) = explode('.', $entry);
if(class_exists($classname) &&
in_array('home_portlet', class_parents($classname, false)))
{
$classes[$appname][] = $classname;
}
}
}
if(!$classes[$appname]) continue;
// Build 'Add' actions for each discovered portlet.
// Portlets from other apps go in sub-actions
$add_to =& $list;
if($classes[$appname] && $appname != 'home')
{
$list[$appname] = array(
'caption' => lang($appname),
);
$add_to =& $list[$appname]['children'];
}
foreach($classes[$appname] as $portlet)
{
$instance = new $portlet();
$desc = $instance->get_description();
$add_to[$portlet] = array(
'id' => $portlet,
'caption' => $desc['displayName'],
'hint' => $desc['description'],
'onExecute' => 'javaScript:app.home.add',
'allowOnMultiple' => $instance->accept_multiple()
);
}
}
return $list;
}, array(), 60);
return $list;
}
/**
* Update the settings for a particular portlet, and give updated content
*
* @param portlet_id String Unique ID (for the user) for a portlet
* @param values Array List of property => value pairs
*
*/
public function ajax_set_properties($portlet_id, $attributes, $values)
{
if(!$attributes)
{
$attributes = array();
}
$response = egw_json_response::get();
if ($GLOBALS['egw_info']['user']['apps']['preferences'])
{
$prefs = $GLOBALS['egw']->preferences->read_repository();
$portlets = (array)$prefs['home']['portlets'];
if($values =='~reload~')
{
$full_exec = true;
$values = array();
}
if($values == '~remove~')
{
unset($portlets[$portlet_id]);
// Already removed client side
}
else
{
// Get portlet settings, and merge new with old
$context = $values+(array)$portlets[$portlet_id];
// Handle add IDs
$classname =& $context['class'];
if(strpos($classname,'add_') == 0 && !class_exists($classname))
{
$add = true;
$classname = substr($classname, 4);
}
$portlet = $this->get_portlet($portlet_id, $context, $content, $attributes, $full_exec);
$context['class'] = get_class($portlet);
foreach($portlet->get_properties() as $property)
{
if($values[$property['name']])
{
$context[$property['name']] = $values[$property['name']];
}
elseif($portlets[$portlet_id][$property['name']])
{
$context[$property['name']] = $portlets[$portlet_id][$property['name']];
}
}
// Update client side
$update = array('attributes' => $attributes);
// New portlet? Flag going straight to edit mode
if($add)
{
$update['edit_settings'] = true;
}
// Send this back to the portlet widget
$response->data($update);
// Store for preference update
$portlets[$portlet_id] = $context;
}
// Save updated preferences
$GLOBALS['egw']->preferences->add('home', 'portlets', $portlets);
$GLOBALS['egw']->preferences->save_repository(True);
}
}
}