aboutsummaryrefslogtreecommitdiff
path: root/main/src/ui/avatar_image.vala
blob: c052270a2048c03688df78ce4e04138e4eecba68 (plain) (blame)
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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
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
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
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
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
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
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
using Gtk;
using Dino.Entities;
using Xmpp;

namespace Dino.Ui {

public class AvatarImage : Misc {
    public int height { get; set; default = 32; }
    public int width { get; set; default = 32; }
    public bool allow_gray { get; set; default = true; }
    public Account account { get; private set; }
    public StreamInteractor stream_interactor { get; set; }
    public AvatarManager avatar_manager { owned get { return stream_interactor.get_module(AvatarManager.IDENTITY); } }
    public MucManager muc_manager { owned get { return stream_interactor.get_module(MucManager.IDENTITY); } }
    private Jid jid;
    private string? text_only;
    private bool with_plus;
    private bool gray;
    private Jid[] current_jids;
    private Gdk.Pixbuf[] current_avatars;
    private Cairo.ImageSurface? cached_surface;
    private static int8 use_image_surface = -1;

    public AvatarImage() {
        can_focus = false;
        get_style_context().add_class("avatar");
    }

    public override void get_preferred_width(out int minimum_width, out int natural_width) {
        minimum_width = width;
        natural_width = width;
    }

    public override void get_preferred_height(out int minimum_height, out int natural_height) {
        minimum_height = height;
        natural_height = height;
    }

    private Cairo.Surface sub_surface(Cairo.Context ctx, int idx, int width, int height, int font_factor = 1) {
        Cairo.Surface buffer = new Cairo.Surface.similar(ctx.get_target(), Cairo.Content.COLOR_ALPHA, width, height);
        Cairo.Context bufctx = new Cairo.Context(buffer);
        if (idx == -1 || current_avatars[idx] == null) {
            set_source_hex_color(bufctx, gray || idx == -1 ? "555753" : Util.get_avatar_hex_color(stream_interactor, account, current_jids[idx]));
            bufctx.rectangle(0, 0, width, height);
            bufctx.fill();

            string text = text_only ?? (idx == -1 ? "…" : Util.get_display_name(stream_interactor, current_jids[idx], account).get_char(0).toupper().to_string());
            bufctx.select_font_face(get_pango_context().get_font_description().get_family(), Cairo.FontSlant.NORMAL, Cairo.FontWeight.NORMAL);
            bufctx.set_font_size(width / font_factor < 40 ? font_factor * 17 : font_factor * 25);
            Cairo.TextExtents extents;
            bufctx.text_extents(text, out extents);
            double x_pos = width/2 - (extents.width/2 + extents.x_bearing);
            double y_pos = height/2 - (extents.height/2 + extents.y_bearing);
            bufctx.move_to(x_pos, y_pos);
            bufctx.set_source_rgba(1, 1, 1, 1);
            bufctx.show_text(text);
        } else {
            double w_scale = (double) width / current_avatars[idx].width;
            double h_scale = (double) height / current_avatars[idx].height;
            double scale = double.max(w_scale, h_scale);
            bufctx.scale(scale, scale);

            double x_off = 0, y_off = 0;
            if (scale == h_scale) {
                x_off = (width / scale - current_avatars[idx].width) / 2.0;
            } else {
                y_off = (height / scale - current_avatars[idx].height) / 2.0;
            }
            Gdk.cairo_set_source_pixbuf(bufctx, current_avatars[idx], x_off, y_off);
            bufctx.get_source().set_filter(Cairo.Filter.BEST);
            bufctx.paint();
        }
        return buffer;
    }

    private static void set_source_hex_color(Cairo.Context ctx, string hex_color) {
        ctx.set_source_rgba((double) hex_color.substring(0, 2).to_long(null, 16) / 255,
                            (double) hex_color.substring(2, 2).to_long(null, 16) / 255,
                            (double) hex_color.substring(4, 2).to_long(null, 16) / 255,
                            hex_color.length > 6 ? (double) hex_color.substring(6, 2).to_long(null, 16) / 255 : 1);
    }

    public override bool draw(Cairo.Context ctx_in) {
        if (text_only == null && (current_jids == null || current_avatars == null || current_jids.length == 0)) return false;

        Cairo.Context ctx = ctx_in;
        int width = this.width, height = this.height, base_factor = 1;
        if (use_image_surface == -1) {
            // TODO: detect if we have to buffer in image surface
            use_image_surface = 1;
        }
        if (use_image_surface == 1) {
            ctx_in.scale(1f/scale_factor, 1f/scale_factor);
            if (cached_surface != null) {
                ctx_in.set_source_surface(cached_surface, 0, 0);
                ctx_in.paint();
                return true;
            }
            width *= scale_factor;
            height *= scale_factor;
            base_factor *= scale_factor;
            cached_surface = new Cairo.ImageSurface(Cairo.Format.ARGB32, width, height);
            ctx = new Cairo.Context(cached_surface);
        }

        double radius = 3 * base_factor;
        double degrees = Math.PI / 180.0;
        ctx.new_sub_path();
        ctx.arc(width - radius, radius, radius, -90 * degrees, 0 * degrees);
        ctx.arc(width - radius, height - radius, radius, 0 * degrees, 90 * degrees);
        ctx.arc(radius, height - radius, radius, 90 * degrees, 180 * degrees);
        ctx.arc(radius, radius, radius, 180 * degrees, 270 * degrees);
        ctx.close_path();
        ctx.clip();

        if (text_only != null) {
            ctx.set_source_surface(sub_surface(ctx, -1, width, height, base_factor), 0, 0);
            ctx.paint();
        } else if (current_jids.length == 4 || with_plus) {
            Cairo.Surface buffer = new Cairo.Surface.similar(ctx.get_target(), Cairo.Content.COLOR_ALPHA, width, height);
            Cairo.Context bufctx = new Cairo.Context(buffer);
            bufctx.scale(0.5, 0.5);
            bufctx.set_source_surface(sub_surface(ctx, 0, width - 1, height - 1, 2 * base_factor), 0, 0);
            bufctx.paint();
            bufctx.set_source_surface(sub_surface(ctx, 1, width - 1, height - 1, 2 * base_factor), width + 1, 0);
            bufctx.paint();
            bufctx.set_source_surface(sub_surface(ctx, 2, width - 1, height - 1, 2 * base_factor), 0, height + 1);
            bufctx.paint();
            if (with_plus) {
                bufctx.set_source_surface(sub_surface(ctx, -1, width - 1, height - 1, 2 * base_factor), width + 1, height + 1);
                bufctx.paint();
            } else {
                bufctx.set_source_surface(sub_surface(ctx, 3, width - 1, height - 1, 2 * base_factor), width + 1, height + 1);
                bufctx.paint();
            }

            ctx.set_source_surface(buffer, 0, 0);
            ctx.paint();
        } else if (current_jids.length == 3) {
            Cairo.Surface buffer = new Cairo.Surface.similar(ctx.get_target(), Cairo.Content.COLOR_ALPHA, width, height);
            Cairo.Context bufctx = new Cairo.Context(buffer);
            bufctx.scale(0.5, 0.5);
            bufctx.set_source_surface(sub_surface(ctx, 0, width - 1, height - 1, 2 * base_factor), 0, 0);
            bufctx.paint();
            bufctx.set_source_surface(sub_surface(ctx, 1, width - 1, height * 2, 2 * base_factor), width + 1, 0);
            bufctx.paint();
            bufctx.set_source_surface(sub_surface(ctx, 2, width - 1 , height - 1, 2 * base_factor), 0, height + 1);
            bufctx.paint();

            ctx.set_source_surface(buffer, 0, 0);
            ctx.paint();
        } else if (current_jids.length == 2) {
            Cairo.Surface buffer = new Cairo.Surface.similar(ctx.get_target(), Cairo.Content.COLOR_ALPHA, width, height);
            Cairo.Context bufctx = new Cairo.Context(buffer);
            bufctx.scale(0.5, 0.5);
            bufctx.set_source_surface(sub_surface(ctx, 0, width - 1, height * 2, 2 * base_factor), 0, 0);
            bufctx.paint();
            bufctx.set_source_surface(sub_surface(ctx, 1, width - 1, height * 2, 2 * base_factor), width + 1, 0);
            bufctx.paint();

            ctx.set_source_surface(buffer, 0, 0);
            ctx.paint();
        } else if (current_jids.length == 1) {
            ctx.set_source_surface(sub_surface(ctx, 0, width, height, base_factor), 0, 0);
            ctx.paint();
        } else {
            assert_not_reached();
        }

        if (gray) {
            // convert to greyscale
            ctx.set_operator(Cairo.Operator.HSL_COLOR);
            ctx.set_source_rgb(1, 1, 1);
            ctx.rectangle(0, 0, width, height);
            ctx.fill();
            // make the visible part more light
            ctx.set_operator(Cairo.Operator.ATOP);
            ctx.set_source_rgba(1, 1, 1, 0.7);
            ctx.rectangle(0, 0, width, height);
            ctx.fill();
        }

        if (use_image_surface == 1) {
            ctx_in.set_source_surface(ctx.get_target(), 0, 0);
            ctx_in.paint();
        }

        return true;
    }

    public override void destroy() {
        if (stream_interactor != null) {
            stream_interactor.get_module(PresenceManager.IDENTITY).show_received.disconnect(on_show_received);
            stream_interactor.get_module(AvatarManager.IDENTITY).received_avatar.disconnect(on_received_avatar);
            stream_interactor.connection_manager.connection_state_changed.disconnect(on_connection_changed);
            stream_interactor.get_module(RosterManager.IDENTITY).updated_roster_item.disconnect(on_roster_updated);
            stream_interactor.get_module(MucManager.IDENTITY).private_room_occupant_updated.disconnect(on_occupant_updated);
        }
    }

    public void set_jid(StreamInteractor stream_interactor, Jid jid_, Account account, bool force_update = false) {
        this.account = account;
        if (this.stream_interactor == null) {
            this.stream_interactor = stream_interactor;
            stream_interactor.get_module(PresenceManager.IDENTITY).show_received.connect(on_show_received);
            stream_interactor.get_module(AvatarManager.IDENTITY).received_avatar.connect(on_received_avatar);
            stream_interactor.connection_manager.connection_state_changed.connect(on_connection_changed);
            stream_interactor.get_module(RosterManager.IDENTITY).updated_roster_item.connect(on_roster_updated);
            stream_interactor.get_module(MucManager.IDENTITY).private_room_occupant_updated.connect(on_occupant_updated);
        }
        if (muc_manager.is_groupchat(jid_, account) && !avatar_manager.has_avatar(account, jid_)) {
            // Groupchat without avatar
            Gee.List<Jid>? occupants;
            if (muc_manager.is_private_room(account, jid_)) {
                occupants = muc_manager.get_other_offline_members(jid_, account);
            } else {
                occupants = muc_manager.get_other_occupants(jid_, account);
            }
            jid = jid_;
            if (occupants == null || occupants.size == 0) {
                if (force_update || current_jids.length != 1 || !current_jids[0].equals(jid_) || gray != (allow_gray && (occupants == null || !is_self_online()))) {
                    set_jids_(new Jid[] {jid_}, false, occupants == null || !is_self_online());
                }
            } else if (occupants.size > 4) {
                bool requires_update = force_update;
                if (!with_plus) requires_update = true;
                foreach (Jid jid in current_jids) {
                    if (!occupants.contains(jid)) {
                        requires_update = true;
                    }
                }
                if (requires_update) {
                    set_jids_(occupants.slice(0, 3).to_array(), true);
                }
            } else { // 1 <= occupants.size <= 4
                bool requires_update = force_update;
                if (with_plus) requires_update = true;
                if (current_jids.length != occupants.size) requires_update = true;
                foreach (Jid jid in current_jids) {
                    if (!occupants.contains(jid)) {
                        requires_update = true;
                    }
                }
                if (requires_update) {
                    set_jids_(occupants.to_array(), false);
                }
            }
        } else {
            // Single user or MUC with vcard avatar
            this.jid = jid_;
            if (force_update || current_jids.length != 1 || !current_jids[0].equals(jid) || gray != (allow_gray && (!is_counterpart_online(jid) || !is_self_online()))) {
                set_jids_(new Jid[] { jid }, false, !is_counterpart_online(jid) || !is_self_online());
            }
        }
    }

    public void set_jids(StreamInteractor stream_interactor, Jid[] jids, Account account, bool gray = false) {
        this.stream_interactor = stream_interactor;
        this.account = account;
        set_jids_(jids.length > 3 ? jids[0:3] : jids, jids.length > 3, gray);
    }

    private void on_show_received(Show show, Jid jid, Account account) {
        if (!account.equals(this.account)) return;
        if (jid.equals_bare(this.jid)) {
            set_jid(stream_interactor, this.jid, account, true);
            return;
        }
        foreach (Jid jid_ in current_jids) {
            if (jid.equals_bare(jid_)) {
                set_jid(stream_interactor, this.jid, account, true);
                return;
            }
        }
    }

    private void on_received_avatar(Gdk.Pixbuf avatar, Jid jid, Account account) {
        if (!account.equals(this.account)) return;
        if (jid.equals_bare(this.jid)) {
            set_jid(stream_interactor, this.jid, account, true);
            return;
        }
        foreach (Jid jid_ in current_jids) {
            if (jid.equals_bare(jid_)) {
                set_jid(stream_interactor, this.jid, account, true);
                return;
            }
        }
    }

    private void on_connection_changed(Account account, ConnectionManager.ConnectionState state) {
        if (!account.equals(this.account)) return;
        set_jid(stream_interactor, this.jid, account, true);
    }

    private void on_roster_updated(Account account, Jid jid, Roster.Item roster_item) {
        if (!account.equals(this.account)) return;
        if (!jid.equals_bare(this.jid)) return;
        set_jid(stream_interactor, this.jid, account, true);
    }

    private void on_occupant_updated(Account account, Jid room, Jid occupant) {
        if (!account.equals(this.account)) return;
        if (!room.equals_bare(this.jid)) return;
        set_jid(stream_interactor, this.jid, account, true);
    }

    private bool is_self_online() {
        return stream_interactor.connection_manager.get_state(account) == ConnectionManager.ConnectionState.CONNECTED;
    }

    private bool is_counterpart_online(Jid counterpart) {
        return stream_interactor.get_module(PresenceManager.IDENTITY).get_full_jids(counterpart, account) != null;
    }

    public void set_jids_(Jid[] jids, bool with_plus = false, bool gray = false) {
        assert(jids.length > 0);
        assert(jids.length < 5);
        assert(!with_plus || jids.length == 3);
        this.cached_surface = null;
        this.text_only = null;
        this.gray = gray && allow_gray;
        this.with_plus = with_plus;

        set_jids_async.begin(jids);
    }

    public async void set_jids_async(Jid[] jids) {
        Jid[] jids_ = jids;
        Gdk.Pixbuf[] avatars = new Gdk.Pixbuf[jids.length];
        for (int i = 0; i < jids_.length; ++i) {
            Jid? real_jid = muc_manager.get_real_jid(jids_[i], account);
            if (real_jid != null) {
                avatars[i] = yield avatar_manager.get_avatar(account, real_jid);
                if (avatars[i] != null) {
                    jids_[i] = real_jid;
                    continue;
                }
            }
            avatars[i] = yield avatar_manager.get_avatar(account, jids_[i]);
        }
        this.current_avatars = avatars;
        this.current_jids = jids_;

        queue_draw();
    }

    public void set_text(string text, bool gray = true) {
        this.text_only = text;
        this.gray = gray;
        this.with_plus = false;
        this.current_jids = null;
        this.current_avatars = null;
        queue_draw();
    }
}

}