Building Custom Panel Panes (CTools Content Types) in Drupal 7


Content types (a.k.a. panel panes) are one of CTools' most visible, versatile, and easy to learn APIs. In this tutorial, we're going to build a custom content type (or panel pane depending on what you call it) that display's a twitter user's latest feeds. As with any new CTools content type, you need to have at minimum three files in following structure:

  1. gtwitpane/gtwitpane.module
  2. gtwitpane/
  3. gtwitpane/plugins/content_types/

Our .info file is pretty standard. Note we don't need to define in the files array.

name = Ghetto Twitter Pane
description = Provides a ghetto twitter pane to panels
core = 7.x
dependencies[] = ctools
files[] = gtwitpane.module

This hook alerts CTools to load our content types (panel panes) from the module's plugins directory. You may have noticed your module doesn't need to explicitly declare individual content types. CTools is dumb (in a good way) and assumes any .inc file in /plugins/content_types needs to be loaded as a plugin. So unlike normal, you don't even have to clear your cache for a new plugin to get discovered. However, if you are editing a panel, and add a pane, you'll want to update and save that pane before you will see your content type.

* Implements hook_ctools_plugin_directory -
* This lets ctools know to scan my module for a content_type plugin file
* Detailed docks in ctools/ctools.api.php
function gtwitpane_ctools_plugin_directory($owner, $plugin_type) {
// we'll be nice and limit scandir() calls
if ($owner == 'ctools' && $plugin_type == 'content_types') {

As you can see, CTools content types are much cleaner than hook block. A config form is not required, but included in this example.

* This plugin array is more or less self documenting
$plugin = array(
// the title in the admin
'title' => t('Ghetto Twitter pane'),
// no one knows if "single" defaults to FALSE...
'single' => TRUE,
// oh joy, I get my own section of panel panes
'category' => array(t('Ghetto Twitter'), -9),
'edit form' => 'gtwitpane_pane_content_type_edit_form',
'render callback' => 'gtwitpane_pane_content_type_render'

* Run-time rendering of the body of the block (content type)
* See ctools_plugin_examples for more advanced info
function gtwitpane_pane_content_type_render($subtype, $conf, $context = NULL) {
// our output is generate by js. Any markup or theme functions
  // could go here.
  // that private js function is so bad that fixing it will be the
  // basis of the next tutorial
$block->content = _gtwit_ghetto_js_that_is_bad($conf['twitter_username']);

* 'Edit form' callback for the content type.
function gtwitpane_pane_content_type_edit_form(&$form, &$form_state) {
$conf = $form_state['conf'];
$form['twitter_username'] = array(
'#type' => 'textfield',
'#title' => t('twitter username'),
'#size' => 50,
'#description' => t('A valid twitter username.'),
'#default_value' => !empty($conf['twitter_username']) ? $conf['twitter_username'] : 'nicklewisatx',
// no submit
return $form;

* Submit function, note anything in the formstate[conf] automatically gets saved
function gtwitpane_pane_content_type_edit_form_submit(&$form, &$form_state) {
$form_state['conf']['twitter_username'] = $form_state['values']['twitter_username'];

* This js handling kills kittens.
function _gtwit_ghetto_js_that_is_bad($twitter_username) {
$output = '<script src="<a href=""></a>"></script>';
$output .= "<script>new TWTR.Widget({

That's all there is to it. Stay tuned, next we're going to cover the CTools object cache, as well as javascript technique in Drupal 7.

Futher Information and Notes:
More advanced examples of ctools content types take a look at ctools/custom_plugin_example/plugins/content_types. I assume ctools_plugin_example works in 7 :-D.
2. The correct term for "panel pane" in CTools world is  "content type" - and "ctools content types" have nothing to do with "node content types". Confused yet?
3.  Also check out advanced_help for ctools. Believe it or not, there is *tons* of ctools documentation "hidden" there.

Package icon gtwitpane.zip2.33 KB