summaryrefslogtreecommitdiff
path: root/src/contacts-avatar.vala
blob: 873d638683d9edee2664937de2e854637fe2ec1b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
/*
 * Copyright (C) 2011 Alexander Larsson <alexl@redhat.com>
 *
 * 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, see <http://www.gnu.org/licenses/>.
 */

using Gtk;
using Folks;
using Gee;

/**
 * The Avatar of a Contact is responsible for showing an {@link Folks.Individual}'s
 * avatar, or a fallback if it's not available.
 */
public class Contacts.Avatar : DrawingArea {
  private int size;
  private Gdk.Pixbuf? pixbuf = null;

  private Contact? contact = null;
  // We want to lazily load the Pixbuf to make sure we don't draw all contact avatars at once.
  // As long as there is no need for it to be drawn, keep this to false.
  private bool avatar_loaded = false;

  // The background color used in case of a fallback avatar
  private Gdk.RGBA? bg_color = null;
  // The color used for an initial or the fallback icon
  private const Gdk.RGBA fg_color = { 0, 0, 0, 0.25 };

  public Avatar (int size, Contact? contact = null) {
    this.contact = contact;
    if (contact != null) {
      contact.individual.notify["avatar"].connect ( (s, p) => {
          load_avatar.begin ();
        });
    }

    this.size = size;
    set_size_request (size, size);

    // If we don't have an avatar, don't try to load it later
    this.avatar_loaded = (contact == null || contact.individual.avatar == null);

    show ();
  }

  /**
   * Manually set the avatar to the given pixbuf, even if the contact has an avatar.
   */
  public void set_pixbuf (Gdk.Pixbuf? a_pixbuf) {
    this.pixbuf = a_pixbuf;
    queue_draw ();
  }

  private async void load_avatar () {
    assert (this.contact != null);

    this.avatar_loaded = true;
    try {
      var stream = yield this.contact.individual.avatar.load_async (this.size);
      this.pixbuf = yield new Gdk.Pixbuf.from_stream_at_scale_async (stream, this.size, this.size, true);
      queue_draw ();
    } catch (Error e) {
      debug ("Couldn't load avatar of contact %s. Reason: %s", this.contact.individual.display_name, e.message);
    }
  }

  public override bool draw (Cairo.Context cr) {
    // This exists to implement lazy loading: i.e. only load the avatar on the first draw()
    if (!this.avatar_loaded)
      load_avatar.begin ();

    if (this.pixbuf != null)
      draw_contact_avatar (cr);
    else // No avatar available, draw a fallback
      draw_fallback (cr);

    return true;
  }

  private void draw_contact_avatar (Cairo.Context cr) {
    Gdk.cairo_set_source_pixbuf (cr, this.pixbuf, 0, 0);
    // Clip with a circle
    create_circle (cr);
    cr.clip_preserve ();
    cr.paint ();
  }

  private void draw_fallback (Cairo.Context cr) {
    // The background color
    if (this.bg_color == null)
      calculate_color ();

    // Fill the background circle
    cr.set_source_rgb (this.bg_color.red, this.bg_color.green, this.bg_color.blue);
    cr.arc (this.size / 2, this.size / 2, this.size / 2, 0, 2*Math.PI);
    create_circle (cr);
    cr.fill_preserve ();

    // Draw the icon
    try {
      // FIXME we can probably cache this
      var theme = IconTheme.get_default ();
      var fallback_avatar = theme.lookup_icon ("avatar-default",
                                               this.size * 4 / 5,
                                               IconLookupFlags.FORCE_SYMBOLIC);
      var icon_pixbuf = fallback_avatar.load_symbolic (fg_color);
      create_circle (cr);
      cr.clip_preserve ();
      Gdk.cairo_set_source_pixbuf (cr, icon_pixbuf, 1 + this.size / 10, 1 + this.size / 5);
      cr.paint ();
    } catch (Error e) {
      warning ("Couldn't get default avatar icon: %s", e.message);
    }
  }

  private void calculate_color () {
    // We use the hash of the id so we get the same color each time for the same contact
    var hash = (this.contact != null)? str_hash (this.contact.individual.id) : Gdk.CURRENT_TIME;

    var r = ((hash & 0xFF0000) >> 16) / 255.0;
    var g = ((hash & 0x00FF00) >> 8) / 255.0;
    var b = (hash & 0x0000FF) / 255.0;

    // Make it a bit lighter by default (and since the foreground will be darker)
    this.bg_color = Gdk.RGBA () {
      red = (r + 2) / 3.0,
      green = (g + 2) / 3.0,
      blue = (b + 2) / 3.0,
      alpha = 0
    };
  }

  private void create_circle (Cairo.Context cr) {
    cr.arc (this.size / 2, this.size / 2, this.size / 2, 0, 2*Math.PI);
  }
}