Translate

Archives

Custom Tooltips for GNOME Shell Panel Launchers

Tooltip positioning in the GNOME Shell 3.2 is quite unsophisticated. I first spotted the problem when I was updating my GNOME Shell panellaunchers extension. I also discovered the same problem also existed in Frippery Panel Favourites by Ron Yorston as shown below.

GNOME 3.2 extension image

Just reload GNOME shell to observe the problem.

GNOME 3.2 extension image

My first instinct was to modify the tooltip source code to fix the problem. The relevant file is …/src/st/tooltip.c.

/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
/*
 * st-tooltip.c: Plain tooltip actor
 *
 * Copyright 2008, 2009 Intel Corporation
 * Copyright 2009 Red Hat, Inc.
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms and conditions of the GNU Lesser General Public License,
 * version 2.1, as published by the Free Software Foundation.
 *
 * This program is distributed in the hope it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for
 * more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

/**
 * SECTION:st-tooltip
 * @short_description: A tooltip widget
 *
 * #StTooltip implements a single tooltip. It should not normally be created
 * by the application but by the widget implementing tooltip capabilities, for
 * example, #st_button_set_tooltip().
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include <math.h>
#include <stdlib.h>
#include <string.h>

#include <glib.h>

#include <clutter/clutter.h>

#include "st-tooltip.h"

#include "st-widget.h"
#include "st-label.h"
#include "st-private.h"

enum
{
  PROP_0,

  PROP_LABEL,
  PROP_TIP_AREA
};

#define ST_TOOLTIP_GET_PRIVATE(obj)    \
  (G_TYPE_INSTANCE_GET_PRIVATE ((obj), ST_TYPE_TOOLTIP, StTooltipPrivate))

struct _StTooltipPrivate
{
  StLabel         *label;

  ClutterGeometry *tip_area;
};

extern gfloat st_slow_down_factor;

G_DEFINE_TYPE (StTooltip, st_tooltip, ST_TYPE_WIDGET);

static void st_tooltip_show (ClutterActor *self);
static void st_tooltip_show_all (ClutterActor *self);
static void st_tooltip_hide_all (ClutterActor *self);

static void st_tooltip_constrain (StTooltip             *tooltip,
                                  const ClutterGeometry *geometry,
                                  ClutterGeometry       *adjusted_geometry);

static void
st_tooltip_set_property (GObject      *gobject,
                         guint         prop_id,
                         const GValue *value,
                         GParamSpec   *pspec)
{
  StTooltip *tooltip = ST_TOOLTIP (gobject);

  switch (prop_id)
    {
    case PROP_LABEL:
      st_tooltip_set_label (tooltip, g_value_get_string (value));
      break;

    case PROP_TIP_AREA:
      st_tooltip_set_tip_area (tooltip, g_value_get_boxed (value));
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
      break;
    }
}

static void
st_tooltip_get_property (GObject    *gobject,
                         guint       prop_id,
                         GValue     *value,
                         GParamSpec *pspec)
{
  StTooltipPrivate *priv = ST_TOOLTIP (gobject)->priv;

  switch (prop_id)
    {
    case PROP_LABEL:
      g_value_set_string (value, st_label_get_text (priv->label));
      break;

    case PROP_TIP_AREA:
      g_value_set_boxed (value, priv->tip_area);
      break;

    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
      break;
    }
}

static void
st_tooltip_get_preferred_width (ClutterActor *self,
                                gfloat        for_height,
                                gfloat       *min_width_p,
                                gfloat       *natural_width_p)
{
  StTooltipPrivate *priv = ST_TOOLTIP (self)->priv;
  StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (self));
  gfloat label_height;

  st_theme_node_adjust_for_height (theme_node, &for_height);

  if (for_height > -1)
    {
      label_height = for_height;
    }
  else
    {
      label_height = -1;
    }

  clutter_actor_get_preferred_width (CLUTTER_ACTOR (priv->label),
                                     label_height,
                                     min_width_p,
                                     natural_width_p);

  st_theme_node_adjust_preferred_width (theme_node, min_width_p, natural_width_p);
}

static void
st_tooltip_get_preferred_height (ClutterActor *self,
                                 gfloat        for_width,
                                 gfloat       *min_height_p,
                                 gfloat       *natural_height_p)
{
  StTooltipPrivate *priv = ST_TOOLTIP (self)->priv;
  StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (self));
  gfloat min_label_h, natural_label_h;

  st_theme_node_adjust_for_width (theme_node, &for_width);

  clutter_actor_get_preferred_height (CLUTTER_ACTOR (priv->label),
                                      for_width,
                                      &min_label_h,
                                      &natural_label_h);
  if (min_height_p)
    *min_height_p = min_label_h;

  if (natural_height_p)
    *natural_height_p = natural_label_h;

  st_theme_node_adjust_preferred_height (theme_node, min_height_p, natural_height_p);
}

static void
st_tooltip_allocate (ClutterActor          *self,
                     const ClutterActorBox *box,
                     ClutterAllocationFlags flags)
{
  StTooltipPrivate *priv = ST_TOOLTIP (self)->priv;
  StThemeNode *theme_node = st_widget_get_theme_node (ST_WIDGET (self));
  ClutterActorBox content_box, child_box;

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

  st_theme_node_get_content_box (theme_node, box, &content_box);

  child_box.x1 = child_box.y1 = 0;
  child_box.x2 = (box->x2 - box->x1);
  child_box.y2 = (box->y2 - box->y1);

  child_box = content_box;

  clutter_actor_allocate (CLUTTER_ACTOR (priv->label), &child_box, flags);
}

static void
st_tooltip_paint (ClutterActor *self)
{
  StTooltipPrivate *priv = ST_TOOLTIP (self)->priv;

  CLUTTER_ACTOR_CLASS (st_tooltip_parent_class)->paint (self);

  clutter_actor_paint (CLUTTER_ACTOR (priv->label));
}

static void
st_tooltip_dispose (GObject *self)
{
  StTooltipPrivate *priv = ST_TOOLTIP (self)->priv;

  if (priv->label)
    {
      clutter_actor_destroy (CLUTTER_ACTOR (priv->label));
      priv->label = NULL;
    }

  G_OBJECT_CLASS (st_tooltip_parent_class)->dispose (self);
}

static void
st_tooltip_class_init (StTooltipClass *klass)
{
  GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
  ClutterActorClass *actor_class = CLUTTER_ACTOR_CLASS (klass);
  GParamSpec *pspec;

  g_type_class_add_private (klass, sizeof (StTooltipPrivate));

  gobject_class->set_property = st_tooltip_set_property;
  gobject_class->get_property = st_tooltip_get_property;
  gobject_class->dispose = st_tooltip_dispose;

  actor_class->get_preferred_width = st_tooltip_get_preferred_width;
  actor_class->get_preferred_height = st_tooltip_get_preferred_height;
  actor_class->allocate = st_tooltip_allocate;
  actor_class->paint = st_tooltip_paint;
  actor_class->show = st_tooltip_show;
  actor_class->show_all = st_tooltip_show_all;
  actor_class->hide_all = st_tooltip_hide_all;

  pspec = g_param_spec_string ("label",
                               "Label",
                               "Label of the tooltip",
                               NULL, G_PARAM_READWRITE);
  g_object_class_install_property (gobject_class, PROP_LABEL, pspec);

  pspec = g_param_spec_boxed ("tip-area",
                              "Tip Area",
                              "Area on the stage the tooltip applies to",
                              CLUTTER_TYPE_GEOMETRY,
                              ST_PARAM_READWRITE);
  g_object_class_install_property (gobject_class, PROP_TIP_AREA, pspec);
}

static void
st_tooltip_init (StTooltip *tooltip)
{
  tooltip->priv = ST_TOOLTIP_GET_PRIVATE (tooltip);

  tooltip->priv->label = ST_LABEL (st_label_new (NULL));

  tooltip->priv->tip_area = NULL;

  clutter_actor_set_parent (CLUTTER_ACTOR (tooltip->priv->label),
                            CLUTTER_ACTOR (tooltip));

  g_object_set (tooltip, "show-on-set-parent", FALSE, NULL);

  clutter_actor_set_reactive (CLUTTER_ACTOR (tooltip), FALSE);
}

static void
st_tooltip_update_position (StTooltip *tooltip)
{
  StTooltipPrivate *priv = tooltip->priv;
  ClutterGeometry *tip_area = tooltip->priv->tip_area;
  ClutterGeometry geometry;
  ClutterGeometry adjusted_geometry;
  gfloat tooltip_w, tooltip_h, tooltip_x, tooltip_y;

  /* if no area set, just position ourselves top left */
  if (!priv->tip_area)
    {
      clutter_actor_set_anchor_point ((ClutterActor*) tooltip, 0, 0);
      return;
    }

  /* we need to have a style in case there are padding/border values to take into
   * account when calculating width/height */
  st_widget_ensure_style ((StWidget *) tooltip);

  /* find out the tooltip's size */
  clutter_actor_get_size ((ClutterActor*) tooltip, &tooltip_w, &tooltip_h);

  /* attempt to place the tooltip */
  tooltip_x = (int)(tip_area->x + (tip_area->width / 2) - (tooltip_w / 2));
  tooltip_y = (int)(tip_area->y + tip_area->height);

  geometry.x = tooltip_x;
  geometry.y = tooltip_y;
  geometry.width = ceil (tooltip_w);
  geometry.height = ceil (tooltip_h);

  st_tooltip_constrain (tooltip, &geometry, &adjusted_geometry);

  tooltip_x = adjusted_geometry.x;
  tooltip_y = adjusted_geometry.y;

  /* Since we are updating the position out of st_widget_allocate(), we can't
   * call clutter_actor_set_position(), since that would trigger another
   * allocation cycle. Instead, we adjust the anchor position which moves
   * the tooltip actor on the screen without changing its allocation
   */
  clutter_actor_set_anchor_point ((ClutterActor*) tooltip, -tooltip_x, -tooltip_y);
}

/**
 * st_tooltip_get_label:
 * @tooltip: a #StTooltip
 *
 * Get the text displayed on the tooltip
 *
 * Returns: the text for the tooltip. This must not be freed by the application
 */
const gchar *
st_tooltip_get_label (StTooltip *tooltip)
{
  g_return_val_if_fail (ST_IS_TOOLTIP (tooltip), NULL);

  return st_label_get_text (tooltip->priv->label);
}

/**
 * st_tooltip_set_label:
 * @tooltip: a #StTooltip
 * @text: text to set the label to
 *
 * Sets the text displayed on the tooltip
 */
void
st_tooltip_set_label (StTooltip   *tooltip,
                      const gchar *text)
{
  StTooltipPrivate *priv;

  g_return_if_fail (ST_IS_TOOLTIP (tooltip));

  priv = tooltip->priv;

  st_label_set_text (priv->label, text);

  g_object_notify (G_OBJECT (tooltip), "label");
}

static void
st_tooltip_show (ClutterActor *self)
{
  StTooltip *tooltip = ST_TOOLTIP (self);

  st_tooltip_update_position (tooltip);

  /* finally show the tooltip... */
  CLUTTER_ACTOR_CLASS (st_tooltip_parent_class)->show (self);
}

static void
st_tooltip_show_all (ClutterActor *self)
{
  CLUTTER_ACTOR_CLASS (st_tooltip_parent_class)->show_all (self);

  clutter_actor_show_all (CLUTTER_ACTOR (ST_TOOLTIP (self)->priv->label));
}

static void
st_tooltip_hide_all (ClutterActor *self)
{
  CLUTTER_ACTOR_CLASS (st_tooltip_parent_class)->hide_all (self);

  clutter_actor_hide_all (CLUTTER_ACTOR (ST_TOOLTIP (self)->priv->label));
}

/**
 * st_tooltip_set_tip_area:
 * @tooltip: A #StTooltip
 * @area: A #ClutterGeometry
 *
 * Set the area on the stage that the tooltip applies to.
 */
void
st_tooltip_set_tip_area (StTooltip             *tooltip,
                         const ClutterGeometry *area)
{
  g_return_if_fail (ST_IS_TOOLTIP (tooltip));

  if (tooltip->priv->tip_area)
    g_boxed_free (CLUTTER_TYPE_GEOMETRY, tooltip->priv->tip_area);
  tooltip->priv->tip_area = g_boxed_copy (CLUTTER_TYPE_GEOMETRY, area);

  if (clutter_actor_get_stage (CLUTTER_ACTOR (tooltip)))
    st_tooltip_update_position (tooltip);
}

/**
 * st_tooltip_get_tip_area:
 * @tooltip: A #StTooltip
 *
 * Retrieve the area on the stage that the tooltip currently applies to
 *
 * Returns: the #ClutterGeometry, owned by the tooltip which must not be freed
 * by the application.
 */
const ClutterGeometry*
st_tooltip_get_tip_area (StTooltip *tooltip)
{
  g_return_val_if_fail (ST_IS_TOOLTIP (tooltip), NULL);

  return tooltip->priv->tip_area;
}

typedef struct {
  StTooltipConstrainFunc func;
  gpointer data;
  GDestroyNotify notify;
} ConstrainFuncClosure;

static void
constrain_func_closure_free (gpointer data)
{
  ConstrainFuncClosure *closure = data;
  if (closure->notify)
    closure->notify (closure->data);
  g_slice_free (ConstrainFuncClosure, data);
}

static GQuark
st_tooltip_constrain_func_quark (void)
{
  static GQuark value = 0;
  if (G_UNLIKELY (value == 0))
    value = g_quark_from_static_string ("st-tooltip-constrain-func");
  return value;
}

/**
 * st_tooltip_set_constrain_func:
 * @stage: a #ClutterStage
 * @func: (allow-none): function to be called to constrain tooltip position
 * @data: (allow-none): user data to pass to @func
 * @notify: (allow-none): function to be called when @data is no longer needed
 *
 * Sets a callback function that will be used to constrain the position
 * of tooltips within @stage. This can be used, for example, if the stage
 * spans multiple monitors and tooltips should be positioned not to cross
 * monitors.
 */
void
st_tooltip_set_constrain_func (ClutterStage           *stage,
                               StTooltipConstrainFunc  func,
                               gpointer                data,
                               GDestroyNotify          notify)
{
  ConstrainFuncClosure *closure;

  g_return_if_fail (CLUTTER_IS_STAGE (stage));

  if (func)
    {
      closure = g_slice_new (ConstrainFuncClosure);
      closure->func = func;
      closure->data = data;
      closure->notify = notify;
    }
  else
    closure = NULL;

  g_object_set_qdata_full (G_OBJECT (stage), st_tooltip_constrain_func_quark (),
                           closure, constrain_func_closure_free);
}

static void
st_tooltip_constrain (StTooltip             *tooltip,
                      const ClutterGeometry *geometry,
                      ClutterGeometry       *adjusted_geometry)
{
  ConstrainFuncClosure *closure;

  ClutterActor *stage = clutter_actor_get_stage (CLUTTER_ACTOR (tooltip));

  *adjusted_geometry = *geometry;

  if (stage == NULL)
    return;

  closure = g_object_get_qdata (G_OBJECT (stage), st_tooltip_constrain_func_quark ());
  if (closure)
    {
      closure->func (tooltip, geometry, adjusted_geometry, closure->data);
    }
  else
    {
      ClutterActor *parent;
      gfloat parent_w, parent_h;

      parent = clutter_actor_get_parent ((ClutterActor *) tooltip);
      clutter_actor_get_size (parent, &parent_w, &parent_h);

      /* make sure the tooltip is not off parent horizontally */
      if (adjusted_geometry->x < 0)
        adjusted_geometry->x = 0;
      else if (adjusted_geometry->x + adjusted_geometry->width > parent_w)
        adjusted_geometry->x = (int)(parent_w) - adjusted_geometry->width;

      /* make sure the tooltip is not off parent vertically */
      if (adjusted_geometry->y + adjusted_geometry->height > parent_h)
        adjusted_geometry->y = parent_h - adjusted_geometry->height;
    }
}


As you can see from the above code, there is no easy way to add functionality to directly position the tooltip where you want it to be displayed. You could possibly use st_tooltip_set_tip_area to specify the tooltip area. Alternatively, you could probably write a callback function to constrain a tooltip’s position (See st_tooltip_set_constrain_func) but I suspect that lots of problems would lie in wait if I choose that option. Both of those choices involved Clutter.Geometry.

By the way, a useful way to see what functionality is available to you in libgnome-shell is to uncompile /usr/share/lib[64]/gnome-shell/St-1.0.typelib using the g-ir-generate utility. Here is the relevant output.

    <class name="Tooltip" parent="Widget" glib:type-struct="TooltipClass" glib:type-name="StTooltip" glib:get-type="st_tooltip_get_type">
      <implements name="Atk.ImplementorIface"/>
      <implements name="Clutter.Animatable"/>
      <implements name="Clutter.Scriptable"/>
      <field name="parent_instance">
        <type name="Widget"/>
      </field>
      <field name="priv">
        <type name="TooltipPrivate"/>
      </field>
     <function name="set_constrain_func" c:identifier="st_tooltip_set_constrain_func">
        <return-value transfer-ownership="none">
          <type name="none"/>
        </return-value>
        <parameters>
          <parameter name="stage" transfer-ownership="none">
            <type name="Clutter.Stage"/>
          </parameter>
          <parameter name="func" transfer-ownership="none" allow-none="1" scope="notified" closure="2" destroy="3">
            <type name="TooltipConstrainFunc"/>
          </parameter>
          <parameter name="data" transfer-ownership="none" allow-none="1">
            <type name="any"/>
          </parameter>
          <parameter name="notify" transfer-ownership="none" allow-none="1" scope="async">
            <type name="GLib.DestroyNotify"/>
          </parameter>
        </parameters>
      </function>
      <method name="get_label" c:identifier="st_tooltip_get_label">
        <return-value transfer-ownership="none">
          <type name="utf8"/>
        </return-value>
      </method>
      <method name="get_tip_area" c:identifier="st_tooltip_get_tip_area">
        <return-value transfer-ownership="none">
          <type name="Clutter.Geometry"/>
        </return-value>
      </method>
      <method name="set_label" c:identifier="st_tooltip_set_label">
        <return-value transfer-ownership="none">
          <type name="none"/>
        </return-value>
        <parameters>
          <parameter name="text" transfer-ownership="none">
            <type name="utf8"/>
          </parameter>
        </parameters>
      </method>
      <method name="set_tip_area" c:identifier="st_tooltip_set_tip_area">
        <return-value transfer-ownership="none">
          <type name="none"/>
        </return-value>
        <parameters>
          <parameter name="area" transfer-ownership="none">
            <type name="Clutter.Geometry"/>
          </parameter>
        </parameters>
      </method>
      <property name="label" writable="1" transfer-ownership="none">
        <type name="utf8"/>
      </property>
      <property name="tip-area" writable="1" transfer-ownership="none">
        <type name="Clutter.Geometry"/>
      </property>
    </class>


For some time, I have followed the discussions in GNOME Shell Bug 666166. Now that the resulting code changes developed by Seif Lotfy (lead for the Zeitgeist project) are committed, I decided to implement similar tooltip functionality in my panellauncher extension to ensure that tooltips were displayed where I wanted them to be displayed.

GNOME 3.2 extension image

The following code, which again is simple panel launcher for favorites (i.e. the Dash items) extension shows you how to roll your own tooltips for horizontal icon menus. It enables you to control precisely the position of tooltips, their styling, and their fade in and out properties using a combination of constants and stylesheet properties.

//
//  Copyright (c) 2011.  Finnbarr P. Murphy.  All rights reserved.
//

const Lang = imports.lang;
const Mainloop = imports.mainloop;
const Shell = imports.gi.Shell;
const St = imports.gi.St;
const Main = imports.ui.main;
const PopupMenu = imports.ui.popupMenu;
const PanelMenu = imports.ui.panelMenu;
const AppFavorites = imports.ui.appFavorites;
const Tweener = imports.ui.tweener;

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

const ICON_SIZE = 20;
const TOOLTIP_LABEL_SHOW_TIME = 0.15;
const TOOLTIP_LABEL_HIDE_TIME = 0.1;
const TOOLTIP_HOVER_TIMEOUT = 300;


function PanelLauncher(app) {
    this._init(app);
}

PanelLauncher.prototype = {
    _init: function(app) {

        this._labelTimeoutId = 0;
        this._label = null;

        this.actor = new St.Button({ style_class: 'panel-launcher',
                                     reactive: true });

        this.actor.set_child(app.create_icon_texture(ICON_SIZE));
        this.setLabelText(app.get_name());
        this.actor._delegate = this;
        this._app = app;

        this.actor.connect('clicked', Lang.bind(this, function() {
            this._app.open_new_window(-1);
        }));

        this.actor.connect('notify::hover', Lang.bind(this, function() {
           this._onButtonHover();
        }));
    },

   _onButtonHover: function () {
        if (this.actor.get_hover()) {
            if (this._labelTimeoutId == 0) {
                 this._labelTimeoutId = Mainloop.timeout_add(TOOLTIP_HOVER_TIMEOUT, Lang.bind(this, function() {
                     this.showLabel();
                     return false;
                 }));
            }
        } else {
            if (this._labelTimeoutId > 0)
                Mainloop.source_remove(this._labelTimeoutId);
            this._labelTimeoutId = 0;
            this.hideLabel();
        }
    },

    setLabelText: function(text) {
        if (this._label == null)
            this._label = new St.Label({ style_class: 'panel-launcher-label'});

        this._label.set_text(text);
        Main.layoutManager.addChrome(this._label);
        this._label.hide();
    },

    showLabel: function() {
        if (this._label == null) return;
        this._label.opacity = 0;
        this._label.show();

        let [stageX, stageY] = this.actor.get_transformed_position();

        let node = this._label.get_theme_node();
        let xOffset = node.get_length('-x-offset');
        let yOffset = node.get_length('-y-offset');

        let y = stageY + yOffset;
        let x = stageX - (this._label.get_width())/2 + xOffset;

        this._label.set_position(x, y);
        Tweener.addTween(this._label, { opacity: 255,
                                        time: TOOLTIP_LABEL_SHOW_TIME,
                                        transition: 'easeOutQuad',
        });
    },

    hideLabel: function () {
        this._label.opacity = 255;
        Tweener.addTween(this._label, { opacity: 0,
                                        time: TOOLTIP_LABEL_HIDE_TIME,
                                        transition: 'easeOutQuad',
                                        onComplete: Lang.bind(this, function() { this._label.hide(); })
        });
    }

};

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


PanelPopupMenuItem.prototype = {
    __proto__: PopupMenu.PopupBaseMenuItem.prototype,

    _init: function(icon, text, menu_icon_first, params) {
        PopupMenu.PopupBaseMenuItem.prototype._init.call(this, params);

        this.label = new St.Label({ text: text });

        if (menu_icon_first) {
            this.box = new St.BoxLayout({ style_class: 'panel-launcher-box'});
            this.box.add(icon);
            this.box.add(this.label);
            this.addActor(this.box);
        } else {
            this.addActor(this.label);
            this.addActor(icon);
        }
    }

};


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

Favorites.prototype = {
    FAVORITE_APPS_KEY: 'favorite-apps',

    _init: function() {
        this._buttons = [];
        this._appSystem = Shell.AppSystem.get_default();

        this.actor = new St.BoxLayout({ style_class: 'panel-launcher-box'});

        this._display();

        this._appSystem.connect('installed-changed', Lang.bind(this, this._redisplay));
        AppFavorites.getAppFavorites().connect('changed', Lang.bind(this, this._redisplay));

    },

    _display: function() {
        let launchers = global.settings.get_strv(this.FAVORITE_APPS_KEY);
        let appSys = Shell.AppSystem.get_default();

        let j = 0;
        for ( let i = 0; i < launchers.length; ++i ) {
            if ((app = appSys.lookup_app(launchers[i]))) {
                this._buttons[j] = new PanelLauncher(app);
                this.actor.add(this._buttons[j].actor);
                ++j;
            }
        }
    },

    _redisplay: function() {
        for ( let i = 0; i < this._buttons.length; ++i )
            this._buttons[i].actor.destroy();
        this._display();
    },

    enable: function() {
        Main.panel._leftBox.insert_actor(this.actor, 1);
    },

    disable: function() {
        Main.panel._leftBox.remove_actor(this.actor);
    }

};

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


Here is the stylesheet that I use with this extension:

panel-launcher-box {
    spacing: 7px;
}

.panel-launcher {
}

.panel-launcher-label {
    border: 1px solid rgba(255,255,255,0.6);
    border-radius: 5px;
    padding: 2px 12px;
    background-color: rgba(0,0,0,0.9);
    color: #ffffff;
    font-size: 0.8em;
    font-weight: bold;
    text-align: center;
    -x-offset: 8px;
    -y-offset: 30px;
}


The interesting lines in the above stylesheet are -x-offset and -y-offset. These are not approved CSS properties. However the GNOME Shell does not complain about these non-standard properties.

Here is a simple example of how to retrieve the value of these two non-standard properties:

let label = new St.Label({ style_class: 'panel-launcher-label'});
let node = label.get_theme_node();
let xOffset = node.get_length('-x-offset');
let yOffset = node.get_length('-y-offset');

By the way, you can download this particular extension from here.

Merry Christmas and a Happy New Year to all my readers.

5 comments to Custom Tooltips for GNOME Shell Panel Launchers

  • Charles Bowman

    Tweak for Gnome shell Application display

    Ok, you open up the Overview & select Applications but you don’t want it to default to All since that takes a second to load.

    In the appDisplay.js, lines 167 & 254 (this._selectCategory(0);) can be changed from the default -1 to another value, 0 is Accessories, 1 is Graphics, etc..

    So set it to default to your least populated, or your most used, it’s a matter of personal preference. Make sure you match the values in each location, it won’t crash your system but the results won’t be consistent.

  • Charles Bowman

    I hope you don’t mind me depositing a couple hacks here.

    A tweak for Gnome shell Overview

    Ok, as I’m slowly trying to wean myself off of menu extensions & synapse I mapped my old synapse key , to Activities Overview. (put synapse on ctrl+menu as a security blanket).

    Quickly discovered that in comparison to synapse it’s a little slow.

    Let’s speed it up. Open /usr/share/gnome-shell/js/ui/overview.js line 30

    [ // Time for initial animation going into Overview modeconst ANIMATION_TIME = 0.01;]

    default is 0.25, I cut it drastically as you can see here.

    Now locate easeOutQuad, use search & replace to replace ‘easeOutQuad with ‘default’ or ‘easeNone’

    In the tweener.js don’t forget to change the refresh rate to that of your display (currently line 210)

    [ FRAME_RATE : 170,] or you can do what I do, a habit I picked up from using compiz, put in a value of twice your refresh rate, not positive the difference is noticeable but it doesn’t break anything.

    Let me know what you discover. For me the Activities Overview now pops into action almost immediately.

  • Charles Bowman

    Messing with your capnumlockindicator extension & after recoloring the icons things looked a bit crowded so I took the liberty of tweaking the stylesheet.css, it now gives me some leeway in spacing my icons.

    .capnumlock-status-indicator {
    width: 18px;
    height: 18px;
    padding-left: 0px;
    padding-right: 4px;
    }

    I also positioned it top-right by editing extension.js, line 70, & making the value 8

    enable: function() {
    this._setCapNumLockIcon();
    Main.panel._rightBox.insert_actor(this.actor, 8);

    I know right now most people are focusing on extensions that perform a function & aren’t too worried about where or how it is displayed but I’m certain that will change in time & they will expect the flexibility they had with Gnome 2.

  • Mike

    cool stuff! Hey I really like your wallpaper do you have a link to it?

  • Hi murphy, thanks for the extension’s!
    i have installed panellaunchers-with-tooltips@fpmurphy.com,
    edited line in metadata.json for my version gnome-shell 3.4.1,
    ubuntu precise pangolin 64 bit,
    alt+f2 lg show me one error for this exentsion:
    Main.panel._leftBox.insert_actor is not a function
    thanks in any case