Translate

Archives

Changing the Activities Button in GNOME Shell 3.2

In the original version of the GNOME Shell (3.0) it was very easy to alter the text of the Activities button or add an icon by means of a simple GNOME Shell extension.

For example, here is the code for a simple GNOME Shell extension that I published shortly after the release of GNOME 3.0 which enables a user to do just that.

const St = imports.gi.St;
const Main = imports.ui.main;
const Panel = imports.ui.panel;

const Gettext = imports.gettext.domain('gnome-shell');
const _ = Gettext.gettext;


function main() {
    
   let hotCornerButton = Main.panel.button;

   let box = new St.BoxLayout({ style_class: 'activities_box'});

   // change the text string if you want to display different text
   // for the activities button
   let label = new St.Label({ text: _("Activities"),
                              style_class: 'activities_text' });

   // change the icon_name if you want to display a different icon
   // the icon must exist in the appropriate directory
   let logo = new St.Icon({ icon_type: St.IconType.FULLCOLOR, 
                            icon_size: hotCornerButton.height, 
                            icon_name: 'fedora-logo-icon' });

   // comment out this line if you do not want an icon displayed
   box.add_actor(logo);

   // comment out this line if you do not want the label displayed
   box.add_actor(label);

   Main.panel.button.set_child(box);
}


This GNOME Shell extension could be easily customized to display either an icon, a text string or both for the Activities button. All you had to do was comment or or uncomment the appropriate lines in the source code.

With the release of GNOME 3.2, which included GNOME Shell 3.2, in September 2011, the above code no longer works. Life became much more complicated for GNOME Shell extension developers because of new requirements to support three functions: init, enable and disable when coding a new GNOME Shell extension or upgrading an existing extension to support GNOME Shell 3.2. See this post, which I wrote a couple of months ago, for a detailed description of the changes to the GNOME Shell extension infrastructure.

In addition, the JavaScript code relating to the Activities button significantly changed between GNOME Shell 3.0 and 3.2. Here is the relevant code from /usr/share/gnome-shell/js/ui/panel.js with comments removed to reduce the number of lines.

function ActivitiesButton() {
    this._init.apply(this, arguments);
}

ActivitiesButton.prototype = {
    __proto__: PanelMenu.Button.prototype,

    _init: function() {
        PanelMenu.Button.prototype._init.call(this, 0.0);

        let container = new Shell.GenericContainer();
        container.connect('get-preferred-width', Lang.bind(this, this._containerGetPreferredWidth));
        container.connect('get-preferred-height', Lang.bind(this, this._containerGetPreferredHeight));
        container.connect('allocate', Lang.bind(this, this._containerAllocate));
        this.actor.add_actor(container);
        this.actor.name = 'panelActivities';

        this._label = new St.Label({ text: _("Activities") });
        container.add_actor(this._label);

        this._hotCorner = new Layout.HotCorner();
        container.add_actor(this._hotCorner.actor);

        this.menu.open = Lang.bind(this, this._onMenuOpenRequest);
        this.menu.close = Lang.bind(this, this._onMenuCloseRequest);
        this.menu.toggle = Lang.bind(this, this._onMenuToggleRequest);

        this.actor.connect('captured-event', Lang.bind(this, this._onCapturedEvent));
        this.actor.connect_after('button-release-event', Lang.bind(this, this._onButtonRelease));
        this.actor.connect_after('key-release-event', Lang.bind(this, this._onKeyRelease));

        Main.overview.connect('showing', Lang.bind(this, function() {
            this.actor.add_style_pseudo_class('overview');
            this._escapeMenuGrab();
        }));
        Main.overview.connect('hiding', Lang.bind(this, function() {
            this.actor.remove_style_pseudo_class('overview');
            this._escapeMenuGrab();
        }));

        this._xdndTimeOut = 0;
    },
    _containerGetPreferredWidth: function(actor, forHeight, alloc) {
        [alloc.min_size, alloc.natural_size] = this._label.get_preferred_width(forHeight);
    },

    _containerGetPreferredHeight: function(actor, forWidth, alloc) {
        [alloc.min_size, alloc.natural_size] = this._label.get_preferred_height(forWidth);
    },

    _containerAllocate: function(actor, box, flags) {
        this._label.allocate(box, flags);

        let primary = Main.layoutManager.primaryMonitor;
        let hotBox = new Clutter.ActorBox();
        let ok, x, y;
        if (actor.get_direction() == St.TextDirection.LTR) {
            [ok, x, y] = actor.transform_stage_point(primary.x, primary.y)
        } else {
            [ok, x, y] = actor.transform_stage_point(primary.x + primary.width, primary.y);
        }

        hotBox.x1 = Math.round(x);
        hotBox.x2 = hotBox.x1 + this._hotCorner.actor.width;
        hotBox.y1 = Math.round(y);
        hotBox.y2 = hotBox.y1 + this._hotCorner.actor.height;
        this._hotCorner.actor.allocate(hotBox, flags);
    },


Note the use of Shell.GenericContainer and its associated signal handlers. I will return to discussing this object and these signal handlers later in the post.

The remainder of this post describes by means of a number of examples how to modify the above code via a GNOME Shell extension in order to change the (text) label displayed, display an icon instead of a label or display both an icon and a label simultaneously. This post assumes that you are familiar with GNOME Shell extension development; it does not attempt to explain the basics of writing an extension.

Example 1:

This GNOME Shell extension simply displays an alternative label for the Activities button when enabled and reverts back to the original label, i.e. Activities, when disabled. Localization support for the alternative label is not included but can easily be added.

const Main = imports.ui.main;

// Replace this string constant with your text
const ACTIVITIES_BUTTON_TEXT = "Change Me";


function ChangeActivitiesButtonText() {
    this._init();
}

ChangeActivitiesButtonText.prototype = {

    _init: function() {
        this._label = Main.panel._activitiesButton._label;
        this._originalText = this._label.get_text();
    },

    enable: function() {
        this._label.set_text(ACTIVITIES_BUTTON_TEXT);
    },

    disable: function() {
        this._label.set_text(this._originalText);
    }
};


function init(extensionMeta) {
    return new ChangeActivitiesButtonText();
}


As you can see, the code for this GNOME Shell extension is still fairly simple. When the extension is installed, _init is invoked. When the extension is enabled, enable is invoked and the label on the Activities button is changed to the contents of the ACTIVITIES_BUTTON_TEXT constant. When the extension is disabled, the original label, i.e. Activities, is displayed.

Some people prefer the non-prototypical way of writing a GNOME Shell extension. If you are one of those people, here is what this extension looks like when written to provide the three mandatory functions:

const Main = imports.ui.main;

// Replace this string constant with your text
const ACTIVITIES_BUTTON_TEXT = "Change Me";

let _label;
let _originalText;

function init() {
    _label = Main.panel._activitiesButton._label;
    _originalText = _label.get_text();
}

function enable() {
    _label.set_text(ACTIVITIES_BUTTON_TEXT);
}

function disable() {
    _label.set_text(this._originalText);
}

Example 2:

Things get more complicated when you want to add an icon to the Activities button or replace the text with an icon. This is due to the fact the the code for implementing the Activities button was changed in GNOME Shell 3.2 to use a Shell GenericContainer object. Here is the C source code for this object.

/**
 * SECTION:shell-generic-container
 * @short_description: A container class with signals for allocation
 *
 * #ShellGenericContainer is mainly a workaround for the current
 * lack of GObject subclassing + vfunc overrides in gjs.  We
 * implement the container interface, but proxy the virtual functions
 * into signals, which gjs can catch.
 *
 * #ShellGenericContainer is an #StWidget, and automatically takes its
 * borders and padding into account during size request and allocation.
 */

#include "config.h"

#include "shell-generic-container.h"

#include <clutter/clutter.h>
#include <gtk/gtk.h>
#include <girepository.h>

static void shell_generic_container_iface_init (ClutterContainerIface *iface);

G_DEFINE_TYPE_WITH_CODE(ShellGenericContainer,
                        shell_generic_container,
                        ST_TYPE_CONTAINER,
                        G_IMPLEMENT_INTERFACE (CLUTTER_TYPE_CONTAINER,
                                               shell_generic_container_iface_init));

struct _ShellGenericContainerPrivate {
  GHashTable *skip_paint;
};

/* Signals */
enum
{
  GET_PREFERRED_WIDTH,
  GET_PREFERRED_HEIGHT,
  ALLOCATE,
  LAST_SIGNAL
};

static guint shell_generic_container_signals [LAST_SIGNAL] = { 0 };

static gpointer
shell_generic_container_allocation_ref (ShellGenericContainerAllocation *alloc)
{
  alloc->_refcount++;
  return alloc;
}

static void
shell_generic_container_allocation_unref (ShellGenericContainerAllocation *alloc)
{
  if (--alloc->_refcount == 0)
    g_slice_free (ShellGenericContainerAllocation, alloc);
}

static void
shell_generic_container_allocate (ClutterActor           *self,
                                  const ClutterActorBox  *box,
                                  ClutterAllocationFlags  flags)
{
  StThemeNode *theme_node;
  ClutterActorBox content_box;

  CLUTTER_ACTOR_CLASS (shell_generic_container_parent_class)->allocate (self, box, flags);

  theme_node = st_widget_get_theme_node (ST_WIDGET (self));
  st_theme_node_get_content_box (theme_node, box, &content_box);

  g_signal_emit (G_OBJECT (self), shell_generic_container_signals[ALLOCATE], 0,
                 &content_box, flags);
}

static void
shell_generic_container_get_preferred_width (ClutterActor *actor,
                                             gfloat        for_height,
                                             gfloat       *min_width_p,
                                             gfloat       *natural_width_p)
{
  ShellGenericContainerAllocation *alloc = g_slice_new0 (ShellGenericContainerAllocation);
  StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (actor));

  st_theme_node_adjust_for_height (theme_node, &for_height);

  alloc->_refcount = 1;
  g_signal_emit (G_OBJECT (actor), shell_generic_container_signals[GET_PREFERRED_WIDTH], 0,
                 for_height, alloc);
  if (min_width_p)
    *min_width_p = alloc->min_size;
  if (natural_width_p)
    *natural_width_p = alloc->natural_size;
  shell_generic_container_allocation_unref (alloc);

  st_theme_node_adjust_preferred_width (theme_node, min_width_p, natural_width_p);
}

static void
shell_generic_container_get_preferred_height (ClutterActor *actor,
                                              gfloat        for_width,
                                              gfloat       *min_height_p,
                                              gfloat       *natural_height_p)
{
  ShellGenericContainerAllocation *alloc = g_slice_new0 (ShellGenericContainerAllocation);
  StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (actor));

  st_theme_node_adjust_for_width (theme_node, &for_width);

  alloc->_refcount = 1;
  g_signal_emit (G_OBJECT (actor), shell_generic_container_signals[GET_PREFERRED_HEIGHT], 0,
                 for_width, alloc);
  if (min_height_p)
    *min_height_p = alloc->min_size;
  if (natural_height_p)
    *natural_height_p = alloc->natural_size;
  shell_generic_container_allocation_unref (alloc);

  st_theme_node_adjust_preferred_height (theme_node, min_height_p, natural_height_p);
}

static void
shell_generic_container_paint (ClutterActor  *actor)
{
  ShellGenericContainer *self = (ShellGenericContainer*) actor;
  GList *iter, *children;

  CLUTTER_ACTOR_CLASS (shell_generic_container_parent_class)->paint (actor);

  children = st_container_get_children_list (ST_CONTAINER (actor));
  for (iter = children; iter; iter = iter->next)
    {
      ClutterActor *child = iter->data;

      if (g_hash_table_lookup (self->priv->skip_paint, child))
        continue;

      clutter_actor_paint (child);
    }
}

static void
shell_generic_container_pick (ClutterActor        *actor,
                              const ClutterColor  *color)
{
  ShellGenericContainer *self = (ShellGenericContainer*) actor;
  GList *iter, *children;

  CLUTTER_ACTOR_CLASS (shell_generic_container_parent_class)->pick (actor, color);

  children = st_container_get_children_list (ST_CONTAINER (actor));
  for (iter = children; iter; iter = iter->next)
    {
      ClutterActor *child = iter->data;

      if (g_hash_table_lookup (self->priv->skip_paint, child))
        continue;

      clutter_actor_paint (child);
    }
}

static GList *
shell_generic_container_get_focus_chain (StContainer *container)
{
  ShellGenericContainer *self = SHELL_GENERIC_CONTAINER (container);
  GList *children, *focus_chain;

  focus_chain = NULL;
  for (children = st_container_get_children_list (container); children; children = children->next)
    {
      ClutterActor *child = children->data;

      if (CLUTTER_ACTOR_IS_VISIBLE (child) &&
          !shell_generic_container_get_skip_paint (self, child))
        focus_chain = g_list_prepend (focus_chain, child);
    }

  return g_list_reverse (focus_chain);
}

/**
 * shell_generic_container_get_n_skip_paint:
 * @self:  A #ShellGenericContainer
 *
 * Returns: Number of children which will not be painted.
 */
guint
shell_generic_container_get_n_skip_paint (ShellGenericContainer  *self)
{
  return g_hash_table_size (self->priv->skip_paint);
}

/**
 * shell_generic_container_get_skip_paint:
 * @self: A #ShellGenericContainer
 * @child: Child #ClutterActor
 *
 * Gets whether or not @actor is skipped when painting.
 *
 * Return value: %TRUE or %FALSE
 */
gboolean
shell_generic_container_get_skip_paint (ShellGenericContainer  *self,
                                        ClutterActor           *child)
{
  return g_hash_table_lookup (self->priv->skip_paint, child) != NULL;
}

/**
 * shell_generic_container_set_skip_paint:
 * @self: A #ShellGenericContainer
 * @child: Child #ClutterActor
 * @skip: %TRUE if we should skip painting
 *
 * Set whether or not we should skip painting @actor.  Workaround for
 * lack of gjs ability to override _paint vfunc.
 */
void
shell_generic_container_set_skip_paint (ShellGenericContainer  *self,
                                        ClutterActor           *child,
                                        gboolean                skip)
{
  gboolean currently_skipping;

  currently_skipping = g_hash_table_lookup (self->priv->skip_paint, child) != NULL;
  if (!!skip == currently_skipping)
    return;

  if (!skip)
    g_hash_table_remove (self->priv->skip_paint, child);
  else
    g_hash_table_insert (self->priv->skip_paint, child, child);

  clutter_actor_queue_redraw (CLUTTER_ACTOR (self));
}

static void
shell_generic_container_finalize (GObject *object)
{
  ShellGenericContainer *self = (ShellGenericContainer*) object;

  g_hash_table_destroy (self->priv->skip_paint);

  G_OBJECT_CLASS (shell_generic_container_parent_class)->finalize (object);
}

static void
shell_generic_container_class_init (ShellGenericContainerClass *klass)
{
  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
  ClutterActorClass *actor_class = CLUTTER_ACTOR_CLASS (klass);
  StContainerClass *container_class = ST_CONTAINER_CLASS (klass);

  gobject_class->finalize = shell_generic_container_finalize;

  actor_class->get_preferred_width = shell_generic_container_get_preferred_width;
  actor_class->get_preferred_height = shell_generic_container_get_preferred_height;
  actor_class->allocate = shell_generic_container_allocate;
  actor_class->paint = shell_generic_container_paint;
  actor_class->pick = shell_generic_container_pick;

  container_class->get_focus_chain = shell_generic_container_get_focus_chain;

  /**
   * ShellGenericContainer::get-preferred-width:
   * @self: the #ShellGenericContainer
   * @for_height: as in clutter_actor_get_preferred_width()
   * @alloc: a #ShellGenericContainerAllocation to be filled in
   *
   * Emitted when clutter_actor_get_preferred_width() is called
   * on @self. You should fill in the fields of @alloc with the
   * your minimum and natural widths. #ShellGenericContainer
   * will deal with taking its borders and padding into account
   * for you.
   *
   * @alloc's fields are initialized to 0, so unless you have a fixed
   * width specified (via #ClutterActor:width or CSS), you must
   * connect to this signal and fill in the values.
   */
  shell_generic_container_signals[GET_PREFERRED_WIDTH] =
    g_signal_new ("get-preferred-width",
                  G_TYPE_FROM_CLASS (klass),
                  G_SIGNAL_RUN_LAST,
                  0,
                  NULL, NULL,
                  gi_cclosure_marshal_generic,
                  G_TYPE_NONE, 2, G_TYPE_FLOAT, SHELL_TYPE_GENERIC_CONTAINER_ALLOCATION);

  /**
   * ShellGenericContainer::get-preferred-height:
   * @self: the #ShellGenericContainer
   * @for_width: as in clutter_actor_get_preferred_height()
   * @alloc: a #ShellGenericContainerAllocation to be filled in
   *
   * Emitted when clutter_actor_get_preferred_height() is called
   * on @self. You should fill in the fields of @alloc with the
   * your minimum and natural heights. #ShellGenericContainer
   * will deal with taking its borders and padding into account
   * for you.
   *
   * @alloc's fields are initialized to 0, so unless you have a fixed
   * height specified (via #ClutterActor:height or CSS), you must
   * connect to this signal and fill in the values.
   */
  shell_generic_container_signals[GET_PREFERRED_HEIGHT] =
    g_signal_new ("get-preferred-height",
                  G_TYPE_FROM_CLASS (klass),
                  G_SIGNAL_RUN_LAST,
                  0,
                  NULL, NULL,
                  gi_cclosure_marshal_generic,
                  G_TYPE_NONE, 2, G_TYPE_FLOAT, SHELL_TYPE_GENERIC_CONTAINER_ALLOCATION);

  /**
   * ShellGenericContainer::allocate:
   * @self: the #ShellGenericContainer
   * @box: @self's content box
   * @flags: the allocation flags.
   *
   * Emitted when @self is allocated, after chaining up to the parent
   * allocate method.
   *
   * Note that @box is @self's content box (qv
   * st_theme_node_get_content_box()), NOT its allocation.
   */
  shell_generic_container_signals[ALLOCATE] =
    g_signal_new ("allocate",
                  G_TYPE_FROM_CLASS (klass),
                  G_SIGNAL_RUN_LAST,
                  0,
                  NULL, NULL,
                  gi_cclosure_marshal_generic,
                  G_TYPE_NONE, 2, CLUTTER_TYPE_ACTOR_BOX, CLUTTER_TYPE_ALLOCATION_FLAGS);

  g_type_class_add_private (gobject_class, sizeof (ShellGenericContainerPrivate));
}

static void
shell_generic_container_actor_removed (ClutterContainer *container,
                                       ClutterActor     *actor)
{
  ShellGenericContainerPrivate *priv = SHELL_GENERIC_CONTAINER (container)->priv;

  g_hash_table_remove (priv->skip_paint, actor);
}

static void
shell_generic_container_iface_init (ClutterContainerIface *iface)
{
  iface->actor_removed = shell_generic_container_actor_removed;
}

static void
shell_generic_container_init (ShellGenericContainer *area)
{
  area->priv = G_TYPE_INSTANCE_GET_PRIVATE (area, SHELL_TYPE_GENERIC_CONTAINER,
                                            ShellGenericContainerPrivate);
  area->priv->skip_paint = g_hash_table_new (NULL, NULL);
}

GType
shell_generic_container_allocation_get_type (void)
{
  static GType gtype = G_TYPE_INVALID;
  if (gtype == G_TYPE_INVALID)
    {
      gtype = g_boxed_type_register_static ("ShellGenericContainerAllocation",
         (GBoxedCopyFunc)shell_generic_container_allocation_ref,
         (GBoxedFreeFunc)shell_generic_container_allocation_unref);
    }
  return gtype;
}

One way to work around the Shell GenericContainer issue is to use CSS to restyle the Activities Button to display an icon instead of the default label as shown below:

const St = imports.gi.St;
const Shell = imports.gi.Shell;
const Main = imports.ui.main;


function ThemeActivitiesButton(meta) {
    this._init(meta)
}


ThemeActivitiesButton.prototype = {
    _init: function(meta) {
        this._defaultStylesheet = Main._defaultCssStylesheet;
        this._patchStylesheet = meta.path + '/activitiesbutton.css';
        this._themeContext = St.ThemeContext.get_for_stage(global.stage);
    },

    enable: function() {
        let theme = new St.Theme ({ application_stylesheet: this._patchStylesheet,
                                    theme_stylesheet: this._defaultStylesheet });
        try {
            this._themeContext.set_theme(theme);
        } catch (e) {
            global.logError('Stylesheet parse error: ' + e);
        }
    },

    disable: function() {
        let theme = new St.Theme ({ theme_stylesheet: this._defaultStylesheet });
        try {
            this._themeContext.set_theme(theme);
        } catch (e) {
            global.logError('Stylesheet parse error: ' + e);
        }
    }

};


function init(meta) {
    return new ThemeActivitiesButton(meta); 
}


Here is the corresponding stylesheet.css

/* activitiesbutton.png should be 24x24 PNG icon */

#panelActivities {
    border: none;
    background-image: url("activitiesbutton.png");
    background-position: 4 0;
    width: 24px;
    height: 24px;
    padding-left: 12px;
    padding-right: 0px;
    color: rgba(0,0,0,0.0);
    transition-duration: 100;
}

#panelActivities:hover {
    border-image: url("activitiesbutton-border.svg") 10 10 0 2;
}


The problems with this approach are (1) you cannot display both text and an icon on the Activities button, and (2) there is potential for the styling of the GNOME Shell to become confused due to multiple stylesheets in multiple GNOME Shell extensions. This is a known problem in the GNOME Shell. It is something that can easily occur with badly written Shell extensions that make unwarranted assumptions about what they can and cannot do.

The better approach is to understand how Shell GenericContainer works. Probably the most important thing to understand about this object is that you cannot allocate a height or width when you are creating an instance of this object. Instead you have to use three separate signals as shown in the following example:

// create the new shell generic container object
let container = new Shell.GenericContainer();

// set up the three signals 
container.connect('get-preferred-width', Lang.bind(this, this._containerGetPreferredWidth));
container.connect('get-preferred-height', Lang.bind(this, this._containerGetPreferredHeight));
container.connect('allocate', Lang.bind(this, this._containerAllocate));

// signal code
_containerGetPreferredWidth: function(actor, forHeight, alloc) {
    [alloc.min_size, alloc.natural_size] = this._label.get_preferred_width(forHeight);
},
 
_containerGetPreferredHeight: function(actor, forWidth, alloc) {
    [alloc.min_size, alloc.natural_size] = this._label.get_preferred_height(forWidth);
},
 
_containerAllocate: function(actor, box, flags) {
    this._label.allocate(box, flags);
    .... 
},


Here is a complete extension.js which displays the Fedora Project logo on the Activities button when enabled. All the remaining examples in this post are based on this code.

const Clutter = imports.gi.Clutter;
const Shell = imports.gi.Shell;
const St = imports.gi.St;
const Layout = imports.ui.layout;
const PanelMenu = imports.ui.panelMenu;
const Main = imports.ui.main;
const Lang = imports.lang;
const Mainloop = imports.mainloop;
const Signals = imports.signals;


// ------------ change to suit -----------
const ACTIVITIES_BUTTON_ICON_SIZE    = 20;
const ACTIVITIES_BUTTON_ICON_NAME    = 'fedora-logo-icon';  


function ActivitiesButtonIcon() {
    this._init.apply(this, arguments);
}

// ----- most of this code came straight from panel.js
ActivitiesButtonIcon.prototype = {
    __proto__: PanelMenu.Button.prototype,

    _init: function() {
        PanelMenu.Button.prototype._init.call(this, 0.0);

        this.actor.name = 'panelActivities';

        let container = new Shell.GenericContainer();
        container.connect('get-preferred-width', Lang.bind(this, this._containerGetPreferredWidth));
        container.connect('get-preferred-height', Lang.bind(this, this._containerGetPreferredHeight));
        container.connect('allocate', Lang.bind(this, this._containerAllocate));
        this.actor.add_actor(container);

        // ---------------- icon code -----------------
        this._iconBox = new St.Bin({ width: ACTIVITIES_BUTTON_ICON_SIZE,
                                     height: ACTIVITIES_BUTTON_ICON_SIZE,
                                     x_fill: true,
                                     y_fill: true });
        this._logo = new St.Icon({ icon_type: St.IconType.FULLCOLOR, 
                                   icon_size: ACTIVITIES_BUTTON_ICON_SIZE, 
                                   icon_name: ACTIVITIES_BUTTON_ICON_NAME });
        this._iconBox.child = this._logo;
        container.add_actor(this._iconBox);
        
        this._hotCorner = new Layout.HotCorner();
        container.add_actor(this._hotCorner.actor);

        // Hack up our menu...
        this.menu.open = Lang.bind(this, this._onMenuOpenRequest);
        this.menu.close = Lang.bind(this, this._onMenuCloseRequest);
        this.menu.toggle = Lang.bind(this, this._onMenuToggleRequest);

        this.actor.connect('captured-event', Lang.bind(this, this._onCapturedEvent));
        this.actor.connect_after('button-release-event', Lang.bind(this, this._onButtonRelease));
        this.actor.connect_after('key-release-event', Lang.bind(this, this._onKeyRelease));

        Main.overview.connect('showing', Lang.bind(this, function() {
            this.actor.add_style_pseudo_class('overview');
            this._escapeMenuGrab();
        }));
        Main.overview.connect('hiding', Lang.bind(this, function() {
            this.actor.remove_style_pseudo_class('overview');
            this._escapeMenuGrab();
        }));

        this._xdndTimeOut = 0;
    },

    _containerGetPreferredWidth: function(actor, forHeight, alloc) {
        [alloc.min_size, alloc.natural_size] = this._iconBox.get_preferred_width(forHeight);
    },

    _containerGetPreferredHeight: function(actor, forWidth, alloc) {
        [alloc.min_size, alloc.natural_size] = this._iconBox.get_preferred_height(forWidth);
    },

    _containerAllocate: function(actor, box, flags) {
        this._iconBox.allocate(box, flags);

        let primary = Main.layoutManager.primaryMonitor;
        let hotBox = new Clutter.ActorBox();
        let ok, x, y;
        if (actor.get_direction() == St.TextDirection.LTR) {
            [ok, x, y] = actor.transform_stage_point(primary.x, primary.y)
        } else {
            [ok, x, y] = actor.transform_stage_point(primary.x + primary.width, primary.y);
        }

        hotBox.x1 = Math.round(x);
        hotBox.x2 = hotBox.x1 + this._hotCorner.actor.width;
        hotBox.y1 = Math.round(y);
        hotBox.y2 = hotBox.y1 + this._hotCorner.actor.height;
        this._hotCorner.actor.allocate(hotBox, flags);
    },

    handleDragOver: function(source, actor, x, y, time) {
        if (source != Main.xdndHandler)
            return;

        if (this._xdndTimeOut != 0)
            Mainloop.source_remove(this._xdndTimeOut);
        this._xdndTimeOut = Mainloop.timeout_add(BUTTON_DND_ACTIVATION_TIMEOUT,
                                                 Lang.bind(this, this._xdndShowOverview, actor));
    },

    _escapeMenuGrab: function() {
        if (this.menu.isOpen)
            this.menu.close();
    },

    _onCapturedEvent: function(actor, event) {
        if (event.type() == Clutter.EventType.BUTTON_PRESS) {
            if (!this._hotCorner.shouldToggleOverviewOnClick())
                return true;
        }
        return false;
    },

    _onMenuOpenRequest: function() {
        this.menu.isOpen = true;
        this.menu.emit('open-state-changed', true);
    },

    _onMenuCloseRequest: function() {
        this.menu.isOpen = false;
        this.menu.emit('open-state-changed', false);
    },

    _onMenuToggleRequest: function() {
        this.menu.isOpen = !this.menu.isOpen;
        this.menu.emit('open-state-changed', this.menu.isOpen);
    },

    _onButtonRelease: function() {
        if (this.menu.isOpen) {
            this.menu.close();
            Main.overview.toggle();
        }
    },

    _onKeyRelease: function(actor, event) {
        let symbol = event.get_key_symbol();
        if (symbol == Clutter.KEY_Return || symbol == Clutter.KEY_space) {
            if (this.menu.isOpen)
                this.menu.close();
            Main.overview.toggle();
        }
    },

    _xdndShowOverview: function(actor) {
        let [x, y, mask] = global.get_pointer();
        let pickedActor = global.stage.get_actor_at_pos(Clutter.PickMode.REACTIVE, x, y);

        if (pickedActor == this.actor) {
            if (!Main.overview.visible && !Main.overview.animationInProgress) {
                Main.overview.showTemporarily();
                Main.overview.beginItemDrag(actor);
            }
        }

        Mainloop.source_remove(this._xdndTimeOut);
        this._xdndTimeOut = 0;
    }

};


function ChangeActivitiesButton() {
    this._init();
}

ChangeActivitiesButton.prototype = {

    _init: function() {
        this._myActivitiesButton = new ActivitiesButtonIcon();
        this._orgActivitiesButton = Main.panel._activitiesButton;
    },

    enable: function() {
        Main.panel._leftBox.remove_actor(this._orgActivitiesButton.actor);
        Main.panel._leftBox.insert_actor(this._myActivitiesButton.actor, 0);
    },

    disable: function() {
        Main.panel._leftBox.remove_actor(this._myActivitiesButton.actor);
        Main.panel._leftBox.insert_actor(this._orgActivitiesButton.actor, 0);
    }
};


function init(extensionMeta) {
    return new ChangeActivitiesButton();
}


In GNOME Shell 3.0, we could have used monkey patching to patch the ActivitiesButton prototype code in /usr/share/gnome-shell/js/ui/panel.js. This is no longer possible in GNOME Shell 3.2 due to a change in when GNOME Shell extensions are loaded and enabled. Hence the need to provide a complete ActivitiesButton prototype in the extension.

This code can be used to display any icon that is available in the usual places on a Linux platform as specified by the XDG Base Directory Specification. You can also place a custom icon in $HOME/.icons and it will displayed by the GNOME Shell extension. The above example uses a SVG for the icon but other filetypes are accepted.

Example 3:

In this example, our new GNOME Shell extension from Example 2 is modified so that a file containing an icon is read and icon contained therein is displayed on the Activities button.

const ACTIVITIES_BUTTON_ICON_FILENAME = 'activities_button_icon.svg';  

function ActivitiesButtonIcon() {
    this._init.apply(this, arguments);
}

// ----- most of this code came straight from panel.js
ActivitiesButtonIcon.prototype = {
    __proto__: PanelMenu.Button.prototype,

    _init: function(iconpath) {
        PanelMenu.Button.prototype._init.call(this, 0.0);

        this.actor.name = 'panelActivities';

        // ---------------- icon code -----------------
        let textureCache = St.TextureCache.get_default();
        this._iconActivitiesButton = textureCache.load_uri_async("file://" + iconpath + "/" + ACTIVITIES_BUTTON_ICON_FILENAME, -1, -1);

        this._iconBox = new St.Bin({ style_class: 'activities-button-icon',
                                     child: this._iconActivitiesButton });
        container.add_actor(this._iconBox);
        ....
    },

    _containerGetPreferredWidth: function(actor, forHeight, alloc) {
        [alloc.min_size, alloc.natural_size] = this._iconBox.get_preferred_width(forHeight);
    },

    _containerGetPreferredHeight: function(actor, forWidth, alloc) {
        [alloc.min_size, alloc.natural_size] = this._iconBox.get_preferred_height(forWidth);
    },

    _containerAllocate: function(actor, box, flags) {
        this._iconBox.allocate(box, flags);
        ...
    },
     
    ...
};

function init(extensionMeta) {
    return new ChangeActivitiesButton(extensionMeta.path);
}


In the above example, the icon file must be located in the same directory as the extension code. Certain metadata is passed to a GNOME Shell extension when it is loaded. The full path to where the extension code is located is one component of the available metadata. In the above example, this path is retrieved using extensionMeta.path.

Example 4:

In this final example, our new GNOME Shell extension from Example 2 is modified so that both an icon and text are displayed.

Here are the relevant code changes:

        // ---------------- icon and text code -----------------
        let textureCache = St.TextureCache.get_default();
        this._iconActivitiesButton = textureCache.load_uri_async("file://" + iconpath + "/" + ACTIVITIES_BUTTON_ICON_FILENAME, -1, -1);

        this._boxIconText = new St.BoxLayout({ vertical: false });
        this._boxIconText.add_actor( new St.Bin({ style_class: 'activities-button-icon',
                                                  child: this._iconActivitiesButton }));
        this._boxIconText.add_actor(new St.Label({ text: _("Activities"),
                                                   style_class: 'activities-button-text' }));
        container.add_actor(this._boxIconText);

    ....

    _containerGetPreferredWidth: function(actor, forHeight, alloc) {
        [alloc.min_size, alloc.natural_size] = this._boxIconText.get_preferred_width(forHeight);
    },

    _containerGetPreferredHeight: function(actor, forWidth, alloc) {
        [alloc.min_size, alloc.natural_size] = this._boxIconText.get_preferred_height(forWidth);
    },

    _containerAllocate: function(actor, box, flags) {
        this._boxIconText.allocate(box, flags);
    ...


Support for an extra CSS ruleset, activities-button-text, was added to allow the Activities button label to be styled.

.activities-button-text {
    font-style: italic;
    font-size: 75%;
}


If you examine the above screenshot, you will see that the Activities label is displayed in a smaller italicized font. There is no reason that such support could not be added to the Example 1 GNOME Shell extension. I will leave that as an exercise for you to do.

Well, the above examples should have provided you with all the necessary knowledge and skills to enable you to develop your own GNOME 3.2 Shell extension to customize your Activities button in whatever manner that you like. All of the above examples can be downloaded from my GNOME Shell Extensions webpage as working extensions.

P.S. Please feel free to email me with your suggestions of additional topics relating to extending the GNOME Shell that you would like me to write about.

3 comments to Changing the Activities Button in GNOME Shell 3.2

  • Awolf

    well I did install this the way it is recommended im using ubuntu 11.10 where am I supposed to put the icon I want to set as the activities button ? I know im supposed to change the icons name in the extension code but what about where should the icon be? can i set the icon location at
    const ACTIVITIES_BUTTON_ICON_NAME = ‘fedora-logo-icon’

    PS: I’m no programmer and i dont know coding im working on it actually self-studying Thnx for any help

  • Woqer

    Hi, thanks for that amount of information and for the code of the extensions :) not sure if this is the place (gseATfpmurphy.com is down) but your “activitiesbuttonlocalicon-1.0” extension has a little bug: after restarting the shell the hot corner doesn’t work. Btw the activitiesbuttonicon-2.0 works perfectly on my Arch x86_64

    • fpmurphy

      Thanks for the kind words and the bug report. I will check it out. It certainly was working when I first wrote the extension but the underlying GNOME Shell code may have changed.