launcher.c 11.8 KB
Newer Older
1
/*
2
 * Copyright (C) 2013-2014 Collabora Ltd.
3
 *
4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public
 * License along with this program; if not, write to the
 * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
 * Boston, MA  02110-1301  USA
 *
 * Authors: Emilio Pozuelo Monfort <emilio.pozuelo@collabora.co.uk>
 *          Jonny Lamb <jonny.lamb@collabora.co.uk>
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
 */

#include "config.h"

#include "launcher.h"

#include "clock.h"
#include "panel.h"
#include "shell-app-system.h"

enum {
  PROP_0,
  PROP_BACKGROUND,
};

enum {
  APP_LAUNCHED,
  N_SIGNALS
};
static guint signals[N_SIGNALS] = { 0 };

42
struct MaynardLauncherPrivate {
43 44 45 46 47 48 49
  /* background widget so we know the output size */
  GtkWidget *background;
  ShellAppSystem *app_system;
  GtkWidget *scrolled_window;
  GtkWidget *grid;
};

50
G_DEFINE_TYPE(MaynardLauncher, maynard_launcher, GTK_TYPE_WINDOW)
51 52 53

/* each grid item is 114x114 */
#define GRID_ITEM_WIDTH 114
54
#define GRID_ITEM_HEIGHT 114
55 56

static void
57
maynard_launcher_init (MaynardLauncher *self)
58 59
{
  self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
60 61
      MAYNARD_LAUNCHER_TYPE,
      MaynardLauncherPrivate);
62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106
}

static gint
sort_apps (gconstpointer a,
    gconstpointer b)
{
  GAppInfo *info1 = G_APP_INFO (a);
  GAppInfo *info2 = G_APP_INFO (b);
  gchar *s1, *s2;
  gint ret;

  s1 = g_utf8_casefold (g_app_info_get_display_name (info1), -1);
  s2 = g_utf8_casefold (g_app_info_get_display_name (info2), -1);

  ret = g_strcmp0 (s1, s2);

  g_free (s1);
  g_free (s2);

  return ret;
}

static gboolean
get_child_position_cb (GtkOverlay *overlay,
    GtkWidget *widget,
    GdkRectangle *allocation,
    gpointer user_data)
{
  GtkOverlayClass *klass = GTK_OVERLAY_GET_CLASS (overlay);

  klass->get_child_position (overlay, widget, allocation);

  /* use the same valign and halign properties, but given we have a
   * border of 1px, respect it and don't draw the overlay widget over
   * the border. */
  allocation->x += 1;
  allocation->y -= 1;
  allocation->width -= 2;

  return TRUE;
}

static gboolean
app_launched_idle_cb (gpointer data)
{
107
  MaynardLauncher *self = data;
108 109 110 111 112 113 114 115 116
  GtkAdjustment *adjustment;

  /* make the scrolled window go back to the top */

  adjustment = gtk_scrolled_window_get_vadjustment (
      GTK_SCROLLED_WINDOW (self->priv->scrolled_window));

  gtk_adjustment_set_value (adjustment, 0.0);

117
  return G_SOURCE_REMOVE;
118 119 120 121 122 123
}

static void
clicked_cb (GtkWidget *widget,
    GDesktopAppInfo *info)
{
124
  MaynardLauncher *self;
125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154

  g_app_info_launch (G_APP_INFO (info), NULL, NULL, NULL);

  self = g_object_get_data (G_OBJECT (widget), "launcher");
  g_assert (self);
  g_signal_emit (self, signals[APP_LAUNCHED], 0);

  /* do this in an idle so it's not done so obviously onscreen */
  g_idle_add (app_launched_idle_cb, self);
}

static gboolean
app_enter_cb (GtkWidget *widget,
    GdkEvent *event,
    GtkWidget *revealer)
{
  gtk_revealer_set_reveal_child (GTK_REVEALER (revealer), TRUE);
  return FALSE;
}

static gboolean
app_leave_cb (GtkWidget *widget,
    GdkEvent *event,
    GtkWidget *revealer)
{
  gtk_revealer_set_reveal_child (GTK_REVEALER (revealer), FALSE);
  return FALSE;
}

static GtkWidget *
155
app_launcher_new_from_desktop_info (MaynardLauncher *self,
156 157 158 159 160 161 162 163 164 165 166 167 168
    GDesktopAppInfo *info)
{
  GIcon *icon;
  GtkWidget *image;
  GtkWidget *button;
  GtkWidget *overlay;
  GtkWidget *revealer;
  GtkWidget *label;
  GtkWidget *ebox;

  /* we need an ebox to catch enter and leave events */
  ebox = gtk_event_box_new ();
  gtk_style_context_add_class (gtk_widget_get_style_context (ebox),
169
      "maynard-grid-item");
170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194

  /* we use an overlay so we can have the app icon showing but use a
   * GtkRevealer to show a label of the app's name. */
  overlay = gtk_overlay_new ();
  gtk_container_add (GTK_CONTAINER (ebox), overlay);

  /* ...but each item has a border of 1px and we don't want the
   * revealer to paint into the border, so overload this function to
   * know where to put it. */
  g_signal_connect (overlay, "get-child-position",
      G_CALLBACK (get_child_position_cb), NULL);

  revealer = gtk_revealer_new ();
  g_object_set (revealer,
      "halign", GTK_ALIGN_FILL, /* all the width */
      "valign", GTK_ALIGN_END, /* only at the bottom */
      NULL);
  gtk_revealer_set_transition_type (GTK_REVEALER (revealer),
      GTK_REVEALER_TRANSITION_TYPE_SLIDE_UP);
  gtk_revealer_set_reveal_child (GTK_REVEALER (revealer), FALSE);
  gtk_overlay_add_overlay (GTK_OVERLAY (overlay), revealer);

  /* app name */
  label = gtk_label_new (g_app_info_get_display_name (G_APP_INFO (info)));
  gtk_label_set_ellipsize (GTK_LABEL (label), PANGO_ELLIPSIZE_END);
195
  gtk_style_context_add_class (gtk_widget_get_style_context (label), "maynard-grid-label");
196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211
  gtk_container_add (GTK_CONTAINER (revealer), label);

  /* icon button to load the app */
  icon = g_app_info_get_icon (G_APP_INFO (info));
  image = gtk_image_new_from_gicon (icon, GTK_ICON_SIZE_DIALOG);
  button = gtk_button_new ();
  gtk_style_context_remove_class (
      gtk_widget_get_style_context (button),
      "button");
  gtk_style_context_remove_class (
      gtk_widget_get_style_context (button),
      "image-button");
  gtk_button_set_image (GTK_BUTTON (button), image);
  g_object_set (image,
      "margin", 30,
      NULL);
Guido Gunther's avatar
Guido Gunther committed
212
  gtk_container_add (GTK_CONTAINER (overlay), button);
213 214 215 216 217 218 219 220 221 222 223 224 225 226 227

  /* TODO: a bit ugly */
  g_object_set_data (G_OBJECT (button), "launcher", self);
  g_signal_connect (button, "clicked", G_CALLBACK (clicked_cb), info);

  /* now we have set everything up, we can refernce the ebox and the
   * revealer. enter will show the label and leave will hide the label. */
  g_signal_connect (ebox, "enter-notify-event", G_CALLBACK (app_enter_cb), revealer);
  g_signal_connect (ebox, "leave-notify-event", G_CALLBACK (app_leave_cb), revealer);

  return ebox;
}

static void
installed_changed_cb (ShellAppSystem *app_system,
228
    MaynardLauncher *self)
229 230 231 232 233 234 235 236 237 238 239 240 241 242 243
{
  GHashTable *entries = shell_app_system_get_entries (app_system);
  GList *l, *values;

  gint output_width, output_height;
  guint cols;
  guint left, top;

  /* remove all children first */
  gtk_container_foreach (GTK_CONTAINER (self->priv->grid),
      (GtkCallback) gtk_widget_destroy, NULL);

  values = g_hash_table_get_values (entries);
  values = g_list_sort (values, sort_apps);

244
  maynard_launcher_calculate (self, NULL, NULL, &cols);
245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269
  cols--; /* because we start from zero here */

  left = top = 0;
  for (l = values; l; l = l->next)
    {
      GDesktopAppInfo *info = G_DESKTOP_APP_INFO (l->data);
      GtkWidget *app = app_launcher_new_from_desktop_info (self, info);

      gtk_grid_attach (GTK_GRID (self->priv->grid), app, left++, top, 1, 1);

      if (left > cols)
        {
          left = 0;
          top++;
        }
    }

  g_list_free (values);

  gtk_widget_show_all (self->priv->grid);
}

static void
background_size_allocate_cb (GtkWidget *widget,
    GdkRectangle *allocation,
270
    MaynardLauncher *self)
271 272 273 274 275
{
  installed_changed_cb (self->priv->app_system, self);
}

static void
276
maynard_launcher_constructed (GObject *object)
277
{
278
  MaynardLauncher *self = MAYNARD_LAUNCHER (object);
279

280
  G_OBJECT_CLASS (maynard_launcher_parent_class)->constructed (object);
281 282

  /* window properties */
283
  gtk_window_set_title (GTK_WINDOW (self), "maynard");
284 285 286 287 288 289
  gtk_window_set_decorated (GTK_WINDOW (self), FALSE);
  gtk_widget_realize (GTK_WIDGET (self));

  /* make it black and slightly alpha */
  gtk_style_context_add_class (
      gtk_widget_get_style_context (GTK_WIDGET (self)),
290
      "maynard-grid");
291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315

  /* scroll it */
  self->priv->scrolled_window = gtk_scrolled_window_new (NULL, NULL);
  gtk_container_add (GTK_CONTAINER (self), self->priv->scrolled_window);

  /* main grid for apps */
  self->priv->grid = gtk_grid_new ();
  gtk_container_add (GTK_CONTAINER (self->priv->scrolled_window),
      self->priv->grid);

  /* fill the grid with apps */
  self->priv->app_system = shell_app_system_get_default ();
  g_signal_connect (self->priv->app_system, "installed-changed",
      G_CALLBACK (installed_changed_cb), self);

  /* refill the grid if the background is changed */
  g_assert (self->priv->background != NULL);
  g_signal_connect (self->priv->background, "size-allocate",
      G_CALLBACK (background_size_allocate_cb), self);

  /* now actually fill the grid */
  installed_changed_cb (self->priv->app_system, self);
}

static void
316
maynard_launcher_get_property (GObject *object,
317 318 319 320
    guint param_id,
    GValue *value,
    GParamSpec *pspec)
{
321
  MaynardLauncher *self = MAYNARD_LAUNCHER (object);
322 323 324 325 326 327 328 329 330 331 332 333 334

  switch (param_id)
    {
      case PROP_BACKGROUND:
        g_value_set_object (value, self->priv->background);
        break;
      default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
        break;
    }
}

static void
335
maynard_launcher_set_property (GObject *object,
336 337 338 339
    guint param_id,
    const GValue *value,
    GParamSpec *pspec)
{
340
  MaynardLauncher *self = MAYNARD_LAUNCHER (object);
341 342 343 344 345 346 347 348 349 350 351 352 353

  switch (param_id)
    {
      case PROP_BACKGROUND:
        self->priv->background = g_value_get_object (value);
        break;
      default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, param_id, pspec);
        break;
    }
}

static void
354
maynard_launcher_class_init (MaynardLauncherClass *klass)
355 356 357
{
  GObjectClass *object_class = (GObjectClass *)klass;

358 359 360
  object_class->constructed = maynard_launcher_constructed;
  object_class->get_property = maynard_launcher_get_property;
  object_class->set_property = maynard_launcher_set_property;
361 362 363 364 365 366 367 368 369 370 371 372

  g_object_class_install_property (object_class, PROP_BACKGROUND,
      g_param_spec_object ("background",
          "background",
          "The #GtkWidget of the background of the output",
          GTK_TYPE_WIDGET,
          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));

  signals[APP_LAUNCHED] = g_signal_new ("app-launched",
      G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL,
      NULL, G_TYPE_NONE, 0);

373
  g_type_class_add_private (object_class, sizeof (MaynardLauncherPrivate));
374 375 376
}

GtkWidget *
377
maynard_launcher_new (GtkWidget *background_widget)
378
{
379
  return g_object_new (MAYNARD_LAUNCHER_TYPE,
380 381 382 383 384
      "background", background_widget,
      NULL);
}

void
385
maynard_launcher_calculate (MaynardLauncher *self,
386 387 388 389
    gint *grid_window_width,
    gint *grid_window_height,
    gint *grid_cols)
{
390
  gint output_width, output_height, panel_height;
391 392
  gint usable_width, usable_height;
  guint cols, rows;
393 394
  guint num_apps;
  guint scrollbar_width = 13;
395 396 397

  gtk_widget_get_size_request (self->priv->background,
      &output_width, &output_height);
398
  panel_height = output_height * MAYNARD_PANEL_HEIGHT_RATIO;
399

400 401 402
  usable_width = output_width - MAYNARD_PANEL_WIDTH - scrollbar_width;
  /* don't go further down than the panel */
  usable_height = panel_height - MAYNARD_CLOCK_HEIGHT;
403

404 405 406 407
  /* try and fill half the screen, otherwise round down */
  cols = (int) ((usable_width / 2.0) / GRID_ITEM_WIDTH);
  /* try to fit as many rows as possible in the panel height we have */
  rows = (int) (usable_height / GRID_ITEM_HEIGHT);
408

409 410 411 412 413 414
  /* we don't need to include the scrollbar if we already have enough
   * space for all the apps. */
  num_apps = g_hash_table_size (
      shell_app_system_get_entries (self->priv->app_system));
  if ((cols * rows) >= num_apps)
    scrollbar_width = 0;
415

416
  /* done! */
417 418

  if (grid_window_width)
419
    *grid_window_width = (cols * GRID_ITEM_WIDTH) + scrollbar_width;
420 421

  if (grid_window_height)
422
    *grid_window_height = (rows * GRID_ITEM_HEIGHT);
423 424 425 426

  if (grid_cols)
    *grid_cols = cols;
}