diff options
Diffstat (limited to 'main/src/ui/widgets')
-rw-r--r-- | main/src/ui/widgets/avatar_picture.vala | 519 |
1 files changed, 519 insertions, 0 deletions
diff --git a/main/src/ui/widgets/avatar_picture.vala b/main/src/ui/widgets/avatar_picture.vala new file mode 100644 index 00000000..e632413c --- /dev/null +++ b/main/src/ui/widgets/avatar_picture.vala @@ -0,0 +1,519 @@ +using Dino.Entities; +using Gtk; +using Xmpp; + +public class Dino.Ui.ViewModel.AvatarPictureTileModel : Object { + public string display_text { get; set; } + public Gdk.RGBA background_color { get; set; } + public File? image_file { get; set; } +} + +public class Dino.Ui.ViewModel.AvatarPictureModel : Object { + public ListModel tiles { get; set; } +} + +public class Dino.Ui.ViewModel.ConversationParticipantAvatarPictureTileModel : AvatarPictureTileModel { + private StreamInteractor stream_interactor; + private AvatarManager? avatar_manager { owned get { return stream_interactor == null ? null : stream_interactor.get_module(AvatarManager.IDENTITY); } } + private MucManager? muc_manager { owned get { return stream_interactor == null ? null : stream_interactor.get_module(MucManager.IDENTITY); } } + private Conversation? conversation; + private Jid? primary_avatar_jid; + private Jid? secondary_avatar_jid; + private Jid? display_name_jid; + + public ConversationParticipantAvatarPictureTileModel(StreamInteractor stream_interactor, Conversation conversation, Jid jid) { + this.stream_interactor = stream_interactor; + this.conversation = conversation; + this.primary_avatar_jid = jid; + this.display_name_jid = jid; + + string color_id = jid.to_string(); + if (conversation.type_ != Conversation.Type.CHAT && primary_avatar_jid.equals_bare(conversation.counterpart)) { + Jid? real_jid = muc_manager.get_real_jid(primary_avatar_jid, conversation.account); + if (real_jid != null && muc_manager.is_private_room(conversation.account, conversation.counterpart.bare_jid)) { + secondary_avatar_jid = primary_avatar_jid; + primary_avatar_jid = real_jid.bare_jid; + color_id = primary_avatar_jid.to_string(); + } else { + color_id = jid.resourcepart.to_string(); + } + } else if (conversation.type_ == Conversation.Type.CHAT) { + primary_avatar_jid = jid.bare_jid; + color_id = primary_avatar_jid.to_string(); + } + string display = Util.get_participant_display_name(stream_interactor, conversation, display_name_jid); + display_text = display.get_char(0).toupper().to_string(); + stream_interactor.get_module(RosterManager.IDENTITY).updated_roster_item.connect(on_roster_updated); + + float[] rgbf = color_id != null ? Xep.ConsistentColor.string_to_rgbf(color_id) : new float[] {0.5f, 0.5f, 0.5f}; + background_color = Gdk.RGBA() { red = rgbf[0], green = rgbf[1], blue = rgbf[2], alpha = 1.0f}; + + update_image_file(); + avatar_manager.received_avatar.connect(on_received_avatar); + avatar_manager.fetched_avatar.connect(on_received_avatar); + } + + private void update_image_file() { + File image_file = avatar_manager.get_avatar_file(conversation.account, primary_avatar_jid); + if (image_file == null && secondary_avatar_jid != null) { + image_file = avatar_manager.get_avatar_file(conversation.account, secondary_avatar_jid); + } + this.image_file = image_file; + } + + private void on_received_avatar(Jid jid, Account account) { + if (account.equals(conversation.account) && (jid.equals(primary_avatar_jid) || jid.equals(secondary_avatar_jid))) { + update_image_file(); + } + } + + private void on_roster_updated(Account account, Jid jid) { + if (account.equals(conversation.account) && jid.equals(display_name_jid)) { + string display = Util.get_participant_display_name(stream_interactor, conversation, display_name_jid); + display_text = display.get_char(0).toupper().to_string(); + } + } +} + +public class Dino.Ui.ViewModel.CompatAvatarPictureModel : AvatarPictureModel { + private StreamInteractor stream_interactor; + private AvatarManager? avatar_manager { owned get { return stream_interactor == null ? null : stream_interactor.get_module(AvatarManager.IDENTITY); } } + private MucManager? muc_manager { owned get { return stream_interactor == null ? null : stream_interactor.get_module(MucManager.IDENTITY); } } + private PresenceManager? presence_manager { owned get { return stream_interactor == null ? null : stream_interactor.get_module(PresenceManager.IDENTITY); } } + private ConnectionManager? connection_manager { owned get { return stream_interactor == null ? null : stream_interactor.connection_manager; } } + private Conversation? conversation; + + construct { + tiles = new GLib.ListStore(typeof(ViewModel.AvatarPictureTileModel)); + } + + public CompatAvatarPictureModel(StreamInteractor? stream_interactor) { + this.stream_interactor = stream_interactor; + if (stream_interactor != null) { + connect_signals_weak(this); + } + } + + private static void connect_signals_weak(CompatAvatarPictureModel model_) { + WeakRef model_weak = WeakRef(model_); + ulong muc_manager_private_room_occupant_updated_handler_id = 0; + ulong muc_manager_proom_info_updated_handler_id = 0; + ulong avatar_manager_received_avatar_handler_id = 0; + ulong avatar_manager_fetched_avatar_handler_id = 0; + muc_manager_private_room_occupant_updated_handler_id = model_.muc_manager.private_room_occupant_updated.connect((muc_manager, account, room, jid) => { + CompatAvatarPictureModel? model = (CompatAvatarPictureModel) model_weak.get(); + if (model != null) { + model.on_room_updated(account, room); + } else if (muc_manager_private_room_occupant_updated_handler_id != 0) { + muc_manager.disconnect(muc_manager_private_room_occupant_updated_handler_id); + muc_manager_private_room_occupant_updated_handler_id = 0; + } + }); + muc_manager_proom_info_updated_handler_id = model_.muc_manager.room_info_updated.connect((muc_manager, account, room) => { + CompatAvatarPictureModel? model = (CompatAvatarPictureModel) model_weak.get(); + if (model != null) { + model.on_room_updated(account, room); + } else if (muc_manager_proom_info_updated_handler_id != 0) { + muc_manager.disconnect(muc_manager_proom_info_updated_handler_id); + muc_manager_proom_info_updated_handler_id = 0; + } + }); + avatar_manager_received_avatar_handler_id = model_.avatar_manager.received_avatar.connect((avatar_manager, jid, account) => { + CompatAvatarPictureModel? model = (CompatAvatarPictureModel) model_weak.get(); + if (model != null) { + model.on_received_avatar(jid, account); + } else if (avatar_manager_received_avatar_handler_id != 0) { + avatar_manager.disconnect(avatar_manager_received_avatar_handler_id); + avatar_manager_received_avatar_handler_id = 0; + } + }); + avatar_manager_fetched_avatar_handler_id = model_.avatar_manager.fetched_avatar.connect((avatar_manager, jid, account) => { + CompatAvatarPictureModel? model = (CompatAvatarPictureModel) model_weak.get(); + if (model != null) { + model.on_received_avatar(jid, account); + } else if (avatar_manager_fetched_avatar_handler_id != 0) { + avatar_manager.disconnect(avatar_manager_fetched_avatar_handler_id); + avatar_manager_fetched_avatar_handler_id = 0; + } + }); + } + + private void on_room_updated(Account account, Jid room) { + if (conversation != null && account.equals(conversation.account) && conversation.counterpart.equals_bare(room)) { + reset(); + set_conversation(conversation); + } + } + + private void on_received_avatar(Jid jid, Account account) { + on_room_updated(account, jid); + } + + public void reset() { + (tiles as GLib.ListStore).remove_all(); + } + + public CompatAvatarPictureModel set_conversation(Conversation conversation) { + if (stream_interactor == null) { + critical("set_conversation() used on CompatAvatarPictureModel without stream_interactor"); + return this; + } + this.conversation = conversation; + if (conversation.type_ == Conversation.Type.GROUPCHAT) { + if (avatar_manager.has_avatar(conversation.account, conversation.counterpart)) { + add_internal("#", conversation.counterpart.to_string(), avatar_manager.get_avatar_file(conversation.account, conversation.counterpart)); + } else { + Gee.List<Jid>? occupants = muc_manager.get_other_offline_members(conversation.counterpart, conversation.account); + if (occupants != null && !occupants.is_empty && muc_manager.is_private_room(conversation.account, conversation.counterpart)) { + int count = occupants.size > 4 ? 3 : occupants.size; + for (int i = 0; i < count; i++) { + add_participant(conversation, occupants[i]); + } + if (occupants.size > 4) { + add_internal("+"); + } + } else { + add_internal("#", conversation.counterpart.to_string()); + } + } + } else { + add_participant(conversation, conversation.counterpart); + } + return this; + } + + public CompatAvatarPictureModel add_participant(Conversation conversation, Jid jid) { + if (stream_interactor == null) { + critical("add_participant() used on CompatAvatarPictureModel without stream_interactor"); + return this; + } + (tiles as GLib.ListStore).append(new ConversationParticipantAvatarPictureTileModel(stream_interactor, conversation, jid)); + return this; + } + + public CompatAvatarPictureModel add(string display, string? color_id = null, File? image_file = null) { + add_internal(display, color_id, image_file); + return this; + } + + private AvatarPictureTileModel add_internal(string display, string? color_id = null, File? image_file = null) { + GLib.ListStore store = tiles as GLib.ListStore; + float[] rgbf = color_id != null ? Xep.ConsistentColor.string_to_rgbf(color_id) : new float[] {0.5f, 0.5f, 0.5f}; + var model = new ViewModel.AvatarPictureTileModel() { + display_text = display.get_char(0).toupper().to_string(), + background_color = Gdk.RGBA() { red = rgbf[0], green = rgbf[1], blue = rgbf[2], alpha = 1.0f}, + image_file = image_file + }; + store.append(model); + return model; + } +} + + +public class Dino.Ui.CompatAvatarDrawer { + public float radius_percent { get; set; default = 0.2f; } + public ViewModel.AvatarPictureModel? model { get; set; } + public int height_request { get; set; default = 35; } + public int width_request { get; set; default = 35; } + public string font_family { get; set; default = "Sans"; } + + public Cairo.ImageSurface draw_image_surface() { + Cairo.ImageSurface surface = new Cairo.ImageSurface(Cairo.Format.ARGB32, width_request, height_request); + draw_on_context(new Cairo.Context(surface)); + return surface; + } + + public void draw_on_context(Cairo.Context ctx) { + double radius = (width_request + height_request) * 0.25f * radius_percent; + double degrees = Math.PI / 180.0; + ctx.new_sub_path(); + ctx.arc(width_request - radius, radius, radius, -90 * degrees, 0 * degrees); + ctx.arc(width_request - radius, height_request - radius, radius, 0 * degrees, 90 * degrees); + ctx.arc(radius, height_request - radius, radius, 90 * degrees, 180 * degrees); + ctx.arc(radius, radius, radius, 180 * degrees, 270 * degrees); + ctx.close_path(); + ctx.clip(); + + if (this.model.tiles.get_n_items() == 4) { + Cairo.Surface buffer = new Cairo.Surface.similar(ctx.get_target(), Cairo.Content.COLOR_ALPHA, width_request, height_request); + Cairo.Context bufctx = new Cairo.Context(buffer); + bufctx.scale(0.5, 0.5); + bufctx.set_source_surface(sub_surface_idx(ctx, 0, width_request - 1, height_request - 1, 2), 0, 0); + bufctx.paint(); + bufctx.set_source_surface(sub_surface_idx(ctx, 1, width_request - 1, height_request - 1, 2), width_request + 1, 0); + bufctx.paint(); + bufctx.set_source_surface(sub_surface_idx(ctx, 2, width_request - 1, height_request - 1, 2), 0, height_request + 1); + bufctx.paint(); + bufctx.set_source_surface(sub_surface_idx(ctx, 3, width_request - 1, height_request - 1, 2), width_request + 1, height_request + 1); + bufctx.paint(); + + ctx.set_source_surface(buffer, 0, 0); + ctx.paint(); + } else if (this.model.tiles.get_n_items() == 3) { + Cairo.Surface buffer = new Cairo.Surface.similar(ctx.get_target(), Cairo.Content.COLOR_ALPHA, width_request, height_request); + Cairo.Context bufctx = new Cairo.Context(buffer); + bufctx.scale(0.5, 0.5); + bufctx.set_source_surface(sub_surface_idx(ctx, 0, width_request - 1, height_request - 1, 2), 0, 0); + bufctx.paint(); + bufctx.set_source_surface(sub_surface_idx(ctx, 1, width_request - 1, height_request * 2, 2), width_request + 1, 0); + bufctx.paint(); + bufctx.set_source_surface(sub_surface_idx(ctx, 2, width_request - 1, height_request - 1, 2), 0, width_request + 1); + bufctx.paint(); + + ctx.set_source_surface(buffer, 0, 0); + ctx.paint(); + } else if (this.model.tiles.get_n_items() == 2) { + Cairo.Surface buffer = new Cairo.Surface.similar(ctx.get_target(), Cairo.Content.COLOR_ALPHA, width_request, height_request); + Cairo.Context bufctx = new Cairo.Context(buffer); + bufctx.scale(0.5, 0.5); + bufctx.set_source_surface(sub_surface_idx(ctx, 0, width_request - 1, height_request * 2, 2), 0, 0); + bufctx.paint(); + bufctx.set_source_surface(sub_surface_idx(ctx, 1, width_request - 1, height_request * 2, 2), width_request + 1, 0); + bufctx.paint(); + + ctx.set_source_surface(buffer, 0, 0); + ctx.paint(); + } else if (this.model.tiles.get_n_items() == 1) { + ctx.set_source_surface(sub_surface_idx(ctx, 0, width_request, height_request, 1), 0, 0); + ctx.paint(); + } else if (this.model.tiles.get_n_items() == 0) { + ctx.set_source_surface(sub_surface_idx(ctx, -1, width_request, height_request, 1), 0, 0); + ctx.paint(); + } + ctx.set_source_rgb(0, 0, 0); + } + + private Cairo.Surface sub_surface_idx(Cairo.Context ctx, int idx, int width, int height, int font_factor = 1) { + ViewModel.AvatarPictureTileModel tile = (ViewModel.AvatarPictureTileModel) this.model.tiles.get_item(idx); + Gdk.Pixbuf? avatar = new Gdk.Pixbuf.from_file(tile.image_file.get_path()); + string? name = idx >= 0 ? tile.display_text : ""; + Gdk.RGBA hex_color = tile.background_color; + return sub_surface(ctx, font_family, avatar, name, hex_color, width, height, font_factor); + } + + private static Cairo.Surface sub_surface(Cairo.Context ctx, string font_family, Gdk.Pixbuf? avatar, string? name, Gdk.RGBA background_color, 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 (avatar == null) { + Gdk.cairo_set_source_rgba(bufctx, background_color); + bufctx.rectangle(0, 0, width, height); + bufctx.fill(); + + string text = name == null ? "…" : name.get_char(0).toupper().to_string(); + bufctx.select_font_face(font_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 / avatar.width; + double h_scale = (double) height / avatar.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 - avatar.width) / 2.0; + } else { + y_off = (height / scale - avatar.height) / 2.0; + } + + Gdk.cairo_set_source_pixbuf(bufctx, avatar, x_off, y_off); + bufctx.get_source().set_filter(Cairo.Filter.BEST); + bufctx.paint(); + } + return buffer; + } +} + +public class Dino.Ui.AvatarPicture : Gtk.Widget { + public float radius_percent { get; set; default = 0.2f; } + public ViewModel.AvatarPictureModel? model { get; set; } + private Gee.List<Tile> tiles = new Gee.ArrayList<Tile>(); + + private ViewModel.AvatarPictureModel? old_model; + private ulong model_tiles_items_changed_handler; + + construct { + height_request = 35; + width_request = 35; + set_css_name("picture"); + add_css_class("avatar"); + notify["radius-percent"].connect(queue_draw); + notify["model"].connect(on_model_changed); + } + + private void on_model_changed() { + if (old_model != null) { + old_model.tiles.disconnect(model_tiles_items_changed_handler); + } + foreach (Tile tile in tiles) { + tile.unparent(); + tile.dispose(); + } + tiles.clear(); + old_model = model; + if (model != null) { + model_tiles_items_changed_handler = model.tiles.items_changed.connect(on_model_items_changed); + for(int i = 0; i < model.tiles.get_n_items(); i++) { + Tile tile = new Tile(); + tile.model = model.tiles.get_item(i) as ViewModel.AvatarPictureTileModel; + tile.insert_after(this, tiles.is_empty ? null : tiles.last()); + tiles.add(tile); + } + } + } + + private void on_model_items_changed(uint position, uint removed, uint added) { + while (removed > 0) { + Tile old = tiles.remove_at((int) position); + old.unparent(); + old.dispose(); + removed--; + } + while (added > 0) { + Tile tile = new Tile(); + tile.model = model.tiles.get_item(position) as ViewModel.AvatarPictureTileModel; + tile.insert_after(this, position == 0 ? null : tiles[(int) position - 1]); + tiles.insert((int) position, tile); + position++; + added--; + } + queue_allocate(); + } + + public override void measure(Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) { + minimum_baseline = natural_baseline = -1; + if (orientation == Orientation.HORIZONTAL) { + minimum = natural = width_request; + } else { + minimum = natural = height_request; + } + } + + public override void size_allocate(int width, int height, int baseline) { + int half_width_size = width / 2; + int half_height_size = height / 2; + int half_width_offset = (width % 2 == 0) ? half_width_size : half_width_size + 1; + int half_height_offset = (height % 2 == 0) ? half_height_size : half_height_size + 1; + if (tiles.size == 1) { + tiles[0].allocate(width, height, baseline, null); + } else if (tiles.size == 2) { + tiles[0].allocate_size(Allocation() { x = 0, y = 0, width = half_width_size, height = height }, baseline); + tiles[1].allocate_size(Allocation() { x = half_width_offset, y = 0, width = half_width_size, height = height }, baseline); + } else if (tiles.size == 3) { + tiles[0].allocate_size(Allocation() { x = 0, y = 0, width = half_width_size, height = height }, baseline); + tiles[1].allocate_size(Allocation() { x = half_width_offset, y = 0, width = half_width_size, height = half_height_size }, baseline); + tiles[2].allocate_size(Allocation() { x = half_width_offset, y = half_height_offset, width = half_width_size, height = half_height_size }, baseline); + } else if (tiles.size == 4) { + tiles[0].allocate_size(Allocation() { x = 0, y = 0, width = half_width_size, height = half_height_size }, baseline); + tiles[1].allocate_size(Allocation() { x = half_width_offset, y = 0, width = half_width_size, height = half_height_size }, baseline); + tiles[2].allocate_size(Allocation() { x = 0, y = half_height_offset, width = half_width_size, height = half_height_size }, baseline); + tiles[3].allocate_size(Allocation() { x = half_width_offset, y = half_height_offset, width = half_width_size, height = half_height_size }, baseline); + } + } + + public override SizeRequestMode get_request_mode() { + return SizeRequestMode.CONSTANT_SIZE; + } + + public override void snapshot(Gtk.Snapshot snapshot) { + Graphene.Rect bounds = Graphene.Rect(); + bounds.init(0, 0, get_width(), get_height()); + Gsk.RoundedRect rounded_rect = Gsk.RoundedRect(); + rounded_rect.init_from_rect(bounds, (get_width() + get_height()) * 0.25f * radius_percent); + snapshot.push_rounded_clip(rounded_rect); + base.snapshot(snapshot); + snapshot.pop(); + } + + public override void dispose() { + model = null; + on_model_changed(); + base.dispose(); + } + + private class Tile : Gtk.Widget { + public ViewModel.AvatarPictureTileModel? model { get; set; } + public Gdk.RGBA background_color { get; set; default = Gdk.RGBA(){ red = 1.0f, green = 1.0f, blue = 1.0f, alpha = 0.0f }; } + public string display_text { get { return label.get_text(); } set { label.set_text(value); } } + public File? image_file { get { return picture.file; } set { picture.file = value; } } + + private Binding? background_color_binding; + private Binding? display_text_binding; + private Binding? image_file_binding; + + private Label label = new Label(""); + private Picture picture = new Picture(); + + construct { + label.insert_after(this, null); + label.attributes = new Pango.AttrList(); + label.attributes.insert(Pango.attr_foreground_new(uint16.MAX, uint16.MAX, uint16.MAX)); +#if GTK_4_8 && VALA_0_58 + picture.content_fit = Gtk.ContentFit.COVER; +#elif GTK_4_8 + picture.@set("content-fit", 2); +#endif + picture.insert_after(this, label); + this.notify["model"].connect(on_model_changed); + } + + private void on_model_changed() { + if (background_color_binding != null) background_color_binding.unbind(); + if (display_text_binding != null) display_text_binding.unbind(); + if (image_file_binding != null) image_file_binding.unbind(); + if (model != null) { + background_color_binding = model.bind_property("background-color", this, "background-color", BindingFlags.SYNC_CREATE); + display_text_binding = model.bind_property("display-text", this, "display-text", BindingFlags.SYNC_CREATE); + image_file_binding = model.bind_property("image-file", this, "image-file", BindingFlags.SYNC_CREATE); + } else { + background_color_binding = null; + display_text_binding = null; + image_file_binding = null; + } + } + + public override void dispose() { + if (background_color_binding != null) background_color_binding.unbind(); + if (display_text_binding != null) display_text_binding.unbind(); + if (image_file_binding != null) image_file_binding.unbind(); + background_color_binding = null; + display_text_binding = null; + image_file_binding = null; + label.unparent(); + picture.unparent(); + base.dispose(); + } + + public override void size_allocate(int width, int height, int baseline) { + int min, nat, bl_min, bl_nat; + picture.measure(Orientation.HORIZONTAL, -1, out min, out nat, out bl_min, out bl_nat); + if (nat > 0) { + picture.allocate(width, height, baseline, null); + label.visible = false; + } else { + picture.allocate(0, 0, 0, null); + label.attributes = new Pango.AttrList(); + label.attributes.insert(Pango.attr_foreground_new(uint16.MAX, uint16.MAX, uint16.MAX)); + label.attributes.insert(Pango.attr_scale_new(double.min((double)width, (double)height) * 0.05)); + label.margin_bottom = height/40; + label.visible = true; + label.allocate(width, height, baseline, null); + } + } + + public override void snapshot(Gtk.Snapshot snapshot) { + if (label.visible) { + Graphene.Rect bounds = Graphene.Rect(); + bounds.init(0, 0, get_width(), get_height()); + snapshot.append_node(new Gsk.ColorNode(background_color, bounds)); + } + base.snapshot(snapshot); + } + } +}
\ No newline at end of file |