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);
}
}
}