diff options
33 files changed, 694 insertions, 645 deletions
diff --git a/libdino/src/service/avatar_manager.vala b/libdino/src/service/avatar_manager.vala index b308aa2b..1296856b 100644 --- a/libdino/src/service/avatar_manager.vala +++ b/libdino/src/service/avatar_manager.vala @@ -12,6 +12,7 @@ public class AvatarManager : StreamInteractionModule, Object { public string id { get { return IDENTITY.id; } } public signal void received_avatar(Jid jid, Account account); + public signal void fetched_avatar(Jid jid, Account account); private enum Source { USER_AVATARS, @@ -25,6 +26,7 @@ public class AvatarManager : StreamInteractionModule, Object { private HashMap<Jid, string> vcard_avatars = new HashMap<Jid, string>(Jid.hash_func, Jid.equals_func); private HashMap<string, Pixbuf> cached_pixbuf = new HashMap<string, Pixbuf>(); private HashMap<string, Gee.List<SourceFuncWrapper>> pending_pixbuf = new HashMap<string, Gee.List<SourceFuncWrapper>>(); + private HashSet<string> pending_fetch = new HashSet<string>(); private const int MAX_PIXEL = 192; public static void start(StreamInteractor stream_interactor, Database db) { @@ -45,6 +47,18 @@ public class AvatarManager : StreamInteractionModule, Object { }); } + public File? get_avatar_file(Account account, Jid jid_) { + string? hash = get_avatar_hash(account, jid_); + if (hash == null) return null; + File file = File.new_for_path(Path.build_filename(folder, hash)); + if (!file.query_exists()) { + fetch_and_store_for_jid(account, jid_); + return null; + } else { + return file; + } + } + private string? get_avatar_hash(Account account, Jid jid_) { Jid jid = jid_; if (!stream_interactor.get_module(MucManager.IDENTITY).is_groupchat_occupant(jid_, account)) { @@ -59,6 +73,7 @@ public class AvatarManager : StreamInteractionModule, Object { } } + [Version (deprecated = true)] public bool has_avatar_cached(Account account, Jid jid) { string? hash = get_avatar_hash(account, jid); return hash != null && cached_pixbuf.has_key(hash); @@ -68,6 +83,7 @@ public class AvatarManager : StreamInteractionModule, Object { return get_avatar_hash(account, jid) != null; } + [Version (deprecated = true)] public Pixbuf? get_cached_avatar(Account account, Jid jid_) { string? hash = get_avatar_hash(account, jid_); if (hash == null) return null; @@ -75,6 +91,7 @@ public class AvatarManager : StreamInteractionModule, Object { return null; } + [Version (deprecated = true)] public async Pixbuf? get_avatar(Account account, Jid jid_) { Jid jid = jid_; if (!stream_interactor.get_module(MucManager.IDENTITY).is_groupchat_occupant(jid_, account)) { @@ -111,17 +128,7 @@ public class AvatarManager : StreamInteractionModule, Object { if (image != null) { cached_pixbuf[hash] = image; } else { - Bytes? bytes = null; - if (source == 1) { - bytes = yield Xmpp.Xep.UserAvatars.fetch_image(stream, jid, hash); - } else if (source == 2) { - bytes = yield Xmpp.Xep.VCard.fetch_image(stream, jid, hash); - if (bytes == null && jid.is_bare()) { - db.avatar.delete().with(db.avatar.jid_id, "=", db.get_jid_id(jid)).perform(); - } - } - if (bytes != null) { - store_image(hash, bytes); + if (yield fetch_and_store(stream, account, jid, source, hash)) { image = yield get_image(hash); } cached_pixbuf[hash] = image; @@ -162,7 +169,7 @@ public class AvatarManager : StreamInteractionModule, Object { ); foreach (var entry in get_avatar_hashes(account, Source.USER_AVATARS).entries) { - user_avatars[entry.key] = entry.value; + on_user_avatar_received(account, entry.key, entry.value); } foreach (var entry in get_avatar_hashes(account, Source.VCARD).entries) { @@ -172,7 +179,7 @@ public class AvatarManager : StreamInteractionModule, Object { continue; } - vcard_avatars[entry.key] = entry.value; + on_vcard_avatar_received(account, entry.key, entry.value); } } @@ -218,12 +225,53 @@ public class AvatarManager : StreamInteractionModule, Object { return ret; } - public void store_image(string id, Bytes data) { + public async bool fetch_and_store_for_jid(Account account, Jid jid) { + int source = -1; + string? hash = null; + if (user_avatars.has_key(jid)) { + hash = user_avatars[jid]; + source = 1; + } else if (vcard_avatars.has_key(jid)) { + hash = vcard_avatars[jid]; + source = 2; + } else { + return false; + } + + XmppStream? stream = stream_interactor.get_stream(account); + if (stream == null || !stream.negotiation_complete) return false; + + return yield fetch_and_store(stream, account, jid, source, hash); + } + + private async bool fetch_and_store(XmppStream stream, Account account, Jid jid, int source, string? hash) { + if (hash == null || pending_fetch.contains(hash)) return false; + + pending_fetch.add(hash); + Bytes? bytes = null; + if (source == 1) { + bytes = yield Xmpp.Xep.UserAvatars.fetch_image(stream, jid, hash); + } else if (source == 2) { + bytes = yield Xmpp.Xep.VCard.fetch_image(stream, jid, hash); + if (bytes == null && jid.is_bare()) { + db.avatar.delete().with(db.avatar.jid_id, "=", db.get_jid_id(jid)).perform(); + } + } + + if (bytes != null) { + yield store_image(hash, bytes); + fetched_avatar(jid, account); + } + pending_fetch.remove(hash); + return bytes != null; + } + + private async void store_image(string id, Bytes data) { File file = File.new_for_path(Path.build_filename(folder, id)); try { if (file.query_exists()) file.delete(); //TODO y? DataOutputStream fos = new DataOutputStream(file.create(FileCreateFlags.REPLACE_DESTINATION)); - fos.write_bytes_async.begin(data); + yield fos.write_bytes_async(data); } catch (Error e) { // Ignore: we failed in storing, so we refuse to display later... } diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt index 30b4a52f..9ca7ce81 100644 --- a/main/CMakeLists.txt +++ b/main/CMakeLists.txt @@ -124,8 +124,6 @@ SOURCES src/main.vala src/ui/application.vala - src/ui/avatar_drawer.vala - src/ui/avatar_image.vala src/ui/conversation_list_titlebar.vala src/ui/conversation_view.vala src/ui/conversation_view_controller.vala @@ -209,6 +207,7 @@ SOURCES src/ui/util/sizing_bin.vala src/ui/util/size_request_box.vala + src/ui/widgets/avatar_picture.vala src/ui/widgets/date_separator.vala src/ui/widgets/fixed_ratio_picture.vala src/ui/widgets/natural_size_increase.vala diff --git a/main/data/add_conversation/list_row.ui b/main/data/add_conversation/list_row.ui index c0d7e517..b8a97174 100644 --- a/main/data/add_conversation/list_row.ui +++ b/main/data/add_conversation/list_row.ui @@ -8,10 +8,9 @@ <property name="margin-bottom">3</property> <property name="column-spacing">10</property> <child> - <object class="DinoUiAvatarImage" id="image"> - <property name="allow_gray">False</property> - <property name="height">30</property> - <property name="width">30</property> + <object class="DinoUiAvatarPicture" id="picture"> + <property name="height-request">30</property> + <property name="width-request">30</property> <property name="valign">center</property> </object> </child> diff --git a/main/data/contact_details_dialog.ui b/main/data/contact_details_dialog.ui index 64a0e5fc..4802ae9a 100644 --- a/main/data/contact_details_dialog.ui +++ b/main/data/contact_details_dialog.ui @@ -30,10 +30,10 @@ <property name="margin-start">100</property> <property name="column-spacing">10</property> <child> - <object class="DinoUiAvatarImage" id="avatar"> - <property name="height">50</property> - <property name="width">50</property> - <property name="allow_gray">False</property> + <object class="DinoUiAvatarPicture" id="avatar"> + <property name="height-request">50</property> + <property name="width-request">50</property> + <property name="valign">center</property> <layout> <property name="column">0</property> <property name="row">0</property> diff --git a/main/data/conversation_item_widget.ui b/main/data/conversation_item_widget.ui index cd6c9269..3216232d 100644 --- a/main/data/conversation_item_widget.ui +++ b/main/data/conversation_item_widget.ui @@ -5,9 +5,9 @@ <property name="column-spacing">7</property> <property name="row-spacing">2</property> <child> - <object class="DinoUiAvatarImage" id="avatar_image"> - <property name="height">35</property> - <property name="width">35</property> + <object class="DinoUiAvatarPicture" id="avatar_picture"> + <property name="height-request">35</property> + <property name="width-request">35</property> <property name="valign">start</property> <property name="margin-top">2</property> <layout> diff --git a/main/data/conversation_row.ui b/main/data/conversation_row.ui index 2eb9071b..3bd5527d 100644 --- a/main/data/conversation_row.ui +++ b/main/data/conversation_row.ui @@ -14,9 +14,9 @@ <property name="margin-start">7</property> <property name="margin-end">14</property> <child> - <object class="DinoUiAvatarImage" id="image"> - <property name="height">35</property> - <property name="width">35</property> + <object class="DinoUiAvatarPicture" id="picture"> + <property name="height-request">35</property> + <property name="width-request">35</property> <property name="valign">center</property> </object> </child> diff --git a/main/data/manage_accounts/account_row.ui b/main/data/manage_accounts/account_row.ui index 845010a2..91891b91 100644 --- a/main/data/manage_accounts/account_row.ui +++ b/main/data/manage_accounts/account_row.ui @@ -10,9 +10,9 @@ <property name="margin-bottom">6</property> <property name="column-spacing">6</property> <child> - <object class="DinoUiAvatarImage" id="image"> - <property name="height">40</property> - <property name="width">40</property> + <object class="DinoUiAvatarPicture" id="picture"> + <property name="height-request">40</property> + <property name="width-request">40</property> </object> </child> <child> diff --git a/main/data/manage_accounts/dialog.ui b/main/data/manage_accounts/dialog.ui index 90a36b83..4931507c 100644 --- a/main/data/manage_accounts/dialog.ui +++ b/main/data/manage_accounts/dialog.ui @@ -93,11 +93,9 @@ <class name="image-button"/> </style> <child> - <object class="DinoUiAvatarImage" id="image"> - <property name="height">50</property> - <property name="width">50</property> -<!-- <property name="xalign">1</property>--> - <property name="allow_gray">False</property> + <object class="DinoUiAvatarPicture" id="picture"> + <property name="height-request">50</property> + <property name="width-request">50</property> </object> </child> </object> diff --git a/main/data/occupant_list_item.ui b/main/data/occupant_list_item.ui index 1915aee6..47e63bc9 100644 --- a/main/data/occupant_list_item.ui +++ b/main/data/occupant_list_item.ui @@ -8,9 +8,9 @@ <property name="margin-end">7</property> <property name="column-spacing">10</property> <child> - <object class="DinoUiAvatarImage" id="image"> - <property name="height">30</property> - <property name="width">30</property> + <object class="DinoUiAvatarPicture" id="picture"> + <property name="height-request">30</property> + <property name="width-request">30</property> </object> </child> <child> diff --git a/main/data/quote.ui b/main/data/quote.ui index a7c32ed8..277fc374 100644 --- a/main/data/quote.ui +++ b/main/data/quote.ui @@ -7,10 +7,9 @@ <class name="dino-quote"/> </style> <child> - <object class="DinoUiAvatarImage" id="avatar"> - <property name="allow_gray">False</property> - <property name="height">15</property> - <property name="width">15</property> + <object class="DinoUiAvatarPicture" id="avatar"> + <property name="height-request">15</property> + <property name="width-request">15</property> <property name="valign">center</property> <layout> <property name="column">0</property> diff --git a/main/data/search_autocomplete.ui b/main/data/search_autocomplete.ui index a63bdce9..d607b192 100644 --- a/main/data/search_autocomplete.ui +++ b/main/data/search_autocomplete.ui @@ -3,14 +3,13 @@ <object class="GtkBox" id="root"> <property name="orientation">horizontal</property> <child> - <object class="DinoUiAvatarImage" id="image"> + <object class="DinoUiAvatarPicture" id="picture"> <property name="margin-top">4</property> <property name="margin-bottom">4</property> <property name="margin-start">6</property> <property name="margin-end">6</property> - <property name="height">24</property> - <property name="width">24</property> - <property name="allow_gray">False</property> + <property name="height-request">24</property> + <property name="width-request">24</property> </object> </child> <child> diff --git a/main/data/style.css b/main/data/style.css index fffee8a3..deac24fe 100644 --- a/main/data/style.css +++ b/main/data/style.css @@ -31,8 +31,8 @@ window.dino-main .dino-conversation viewport /* Some themes set this */ { } @keyframes highlight { - from { background: alpha(@warning_color, 0.5); } - to { background: transparent; } + from { background-color: alpha(@accent_color, 0.5); } + to { background-color: transparent; } } window.dino-main .dino-conversation .highlight-once { @@ -42,7 +42,7 @@ window.dino-main .dino-conversation .highlight-once { animation-name: highlight; } -window.dino-main .dino-conversation .message-box.highlight { +window.dino-main .dino-conversation .message-box.highlight:not(.highlight-once) { background: @window_bg_color; } @@ -119,6 +119,10 @@ window.dino-main .dino-quote:hover { background: alpha(@theme_fg_color, 0.08); } +picture.avatar { + border-radius: 3px; +} + /* Overlay Toolbar */ .dino-main .overlay-toolbar { diff --git a/main/src/ui/add_conversation/conference_list.vala b/main/src/ui/add_conversation/conference_list.vala index 14beaf92..0b630ae4 100644 --- a/main/src/ui/add_conversation/conference_list.vala +++ b/main/src/ui/add_conversation/conference_list.vala @@ -112,7 +112,7 @@ internal class ConferenceListRow : ListRow { via_label.visible = false; } - image.set_conversation(stream_interactor, new Conversation(jid, account, Conversation.Type.GROUPCHAT)); + picture.model = new ViewModel.CompatAvatarPictureModel(stream_interactor).set_conversation(new Conversation(jid, account, Conversation.Type.GROUPCHAT)); } } diff --git a/main/src/ui/add_conversation/list_row.vala b/main/src/ui/add_conversation/list_row.vala index 5b3ec49a..c5e344d0 100644 --- a/main/src/ui/add_conversation/list_row.vala +++ b/main/src/ui/add_conversation/list_row.vala @@ -9,7 +9,7 @@ namespace Dino.Ui { public class ListRow : Widget { public Grid outer_grid; - public AvatarImage image; + public AvatarPicture picture; public Label name_label; public Label via_label; @@ -19,7 +19,7 @@ public class ListRow : Widget { construct { Builder builder = new Builder.from_resource("/im/dino/Dino/add_conversation/list_row.ui"); outer_grid = (Grid) builder.get_object("outer_grid"); - image = (AvatarImage) builder.get_object("image"); + picture = (AvatarPicture) builder.get_object("picture"); name_label = (Label) builder.get_object("name_label"); via_label = (Label) builder.get_object("via_label"); @@ -45,7 +45,7 @@ public class ListRow : Widget { via_label.visible = false; } name_label.label = display_name; - image.set_conversation(stream_interactor, conv); + picture.model = new ViewModel.CompatAvatarPictureModel(stream_interactor).set_conversation(conv); } public override void dispose() { diff --git a/main/src/ui/add_conversation/select_jid_fragment.vala b/main/src/ui/add_conversation/select_jid_fragment.vala index 25b0b11f..e0682e29 100644 --- a/main/src/ui/add_conversation/select_jid_fragment.vala +++ b/main/src/ui/add_conversation/select_jid_fragment.vala @@ -132,7 +132,7 @@ public class SelectJidFragment : Gtk.Box { } else { via_label.visible = false; } - image.set_text("?"); + picture.model = new ViewModel.CompatAvatarPictureModel(stream_interactor).add("+"); } } } diff --git a/main/src/ui/avatar_drawer.vala b/main/src/ui/avatar_drawer.vala deleted file mode 100644 index c14d7fda..00000000 --- a/main/src/ui/avatar_drawer.vala +++ /dev/null @@ -1,193 +0,0 @@ -using Cairo; -using Gee; -using Gdk; -using Gtk; -using Xmpp.Util; - -namespace Dino.Ui { - -public class AvatarDrawer { - public const string GRAY = "555753"; - - private Gee.List<AvatarTile> tiles = new ArrayList<AvatarTile>(); - private int height = 35; - private int width = 35; - private bool gray; - private int base_factor = 1; - private string font_family = "Sans"; - - public AvatarDrawer size(int height, int width = height) { - this.height = height; - this.width = width; - return this; - } - - public AvatarDrawer grayscale() { - this.gray = true; - return this; - } - - public AvatarDrawer tile(Pixbuf? image, string? name, string? hex_color) { - tiles.add(new AvatarTile(image, name, hex_color)); - return this; - } - - public AvatarDrawer plus() { - tiles.add(new AvatarTile(null, "…", GRAY)); - return this; - } - - public AvatarDrawer scale(int base_factor) { - this.base_factor = base_factor; - return this; - } - - public AvatarDrawer font(string font_family) { - this.font_family = font_family; - return this; - } - - public ImageSurface draw_image_surface() { - ImageSurface surface = new ImageSurface(Format.ARGB32, width, height); - draw_on_context(new Context(surface)); - return surface; - } - - public void draw_on_context(Cairo.Context ctx) { - 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 (this.tiles.size == 4) { - 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_idx(ctx, 0, width - 1, height - 1, 2 * base_factor), 0, 0); - bufctx.paint(); - bufctx.set_source_surface(sub_surface_idx(ctx, 1, width - 1, height - 1, 2 * base_factor), width + 1, 0); - bufctx.paint(); - bufctx.set_source_surface(sub_surface_idx(ctx, 2, width - 1, height - 1, 2 * base_factor), 0, height + 1); - bufctx.paint(); - bufctx.set_source_surface(sub_surface_idx(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 (this.tiles.size == 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_idx(ctx, 0, width - 1, height - 1, 2 * base_factor), 0, 0); - bufctx.paint(); - bufctx.set_source_surface(sub_surface_idx(ctx, 1, width - 1, height * 2, 2 * base_factor), width + 1, 0); - bufctx.paint(); - bufctx.set_source_surface(sub_surface_idx(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 (this.tiles.size == 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_idx(ctx, 0, width - 1, height * 2, 2 * base_factor), 0, 0); - bufctx.paint(); - bufctx.set_source_surface(sub_surface_idx(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 (this.tiles.size == 1) { - ctx.set_source_surface(sub_surface_idx(ctx, 0, width, height, base_factor), 0, 0); - ctx.paint(); - } else if (this.tiles.size == 0) { - ctx.set_source_surface(sub_surface_idx(ctx, -1, width, height, base_factor), 0, 0); - ctx.paint(); - } - - 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(); - } - 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) { - Gdk.Pixbuf? avatar = idx >= 0 ? tiles[idx].image : null; - string? name = idx >= 0 ? tiles[idx].name : ""; - string hex_color = !gray && idx >= 0 ? tiles[idx].hex_color : GRAY; - 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, string? hex_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) { - set_source_hex_color(bufctx, hex_color ?? GRAY); - 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; - } - - private static void set_source_hex_color(Cairo.Context ctx, string hex_color) { - ctx.set_source_rgba((double) from_hex(hex_color.substring(0, 2)) / 255, - (double) from_hex(hex_color.substring(2, 2)) / 255, - (double) from_hex(hex_color.substring(4, 2)) / 255, - hex_color.length > 6 ? (double) from_hex(hex_color.substring(6, 2)) / 255 : 1); - } -} - -private class AvatarTile { - public Pixbuf? image { get; private set; } - public string? name { get; private set; } - public string? hex_color { get; private set; } - - public AvatarTile(Pixbuf? image, string? name, string? hex_color) { - this.image = image; - this.name = name; - this.hex_color = hex_color; - } -} - -}
\ No newline at end of file diff --git a/main/src/ui/avatar_generator.vala b/main/src/ui/avatar_generator.vala deleted file mode 100644 index e69de29b..00000000 --- a/main/src/ui/avatar_generator.vala +++ /dev/null diff --git a/main/src/ui/avatar_image.vala b/main/src/ui/avatar_image.vala deleted file mode 100644 index f348dd4b..00000000 --- a/main/src/ui/avatar_image.vala +++ /dev/null @@ -1,267 +0,0 @@ -using Gtk; -using Dino.Entities; -using Xmpp; -using Xmpp.Util; - -namespace Dino.Ui { - -public class AvatarImage : Widget { - public int height { get; set; default = 35; } - public int width { get; set; default = 35; } - public bool allow_gray { get; set; default = true; } - public bool force_gray { get; set; default = false; } - public StreamInteractor? stream_interactor { get; set; } - public AvatarManager? avatar_manager { owned get { return stream_interactor == null ? null : stream_interactor.get_module(AvatarManager.IDENTITY); } } - public MucManager? muc_manager { owned get { return stream_interactor == null ? null : stream_interactor.get_module(MucManager.IDENTITY); } } - public PresenceManager? presence_manager { owned get { return stream_interactor == null ? null : stream_interactor.get_module(PresenceManager.IDENTITY); } } - public ConnectionManager? connection_manager { owned get { return stream_interactor == null ? null : stream_interactor.connection_manager; } } - public Account account { get { return conversation.account; } } - private AvatarDrawer? drawer; - private Conversation conversation; - private Jid[] jids; - private Cairo.ImageSurface? cached_surface; - private static int8 use_image_surface = -1; - - public AvatarImage() { - can_focus = false; - add_css_class("avatar"); - } - - public override void dispose() { - base.dispose(); - drawer = null; - cached_surface = null; - disconnect_stream_interactor(); - } - - public override void measure(Orientation orientation, int for_size, out int minimum, out int natural, out int minimum_baseline, out int natural_baseline) { - if (orientation == Orientation.HORIZONTAL) { - minimum = width; - natural = width; - } else { - minimum = height; - natural = height; - } - minimum_baseline = natural_baseline = -1; - } - - public override void snapshot(Snapshot snapshot) { - Cairo.Context context = snapshot.append_cairo(Graphene.Rect.alloc().init(0, 0, width, height)); - draw(context); - } - - public bool draw(Cairo.Context ctx_in) { - 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(); - ctx_in.set_source_rgb(0, 0, 0); - 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); - } - - AvatarDrawer drawer = this.drawer; - Jid[] jids = this.jids; - if (drawer == null && jids.length == 0) { - switch (conversation.type_) { - case Conversation.Type.CHAT: - case Conversation.Type.GROUPCHAT_PM: - // In direct chats or group chats, conversation avatar is same as counterpart avatar - jids = { conversation.counterpart }; - break; - case Conversation.Type.GROUPCHAT: - string user_color = Util.get_avatar_hex_color(stream_interactor, account, conversation.counterpart, conversation); - if (avatar_manager.has_avatar_cached(account, conversation.counterpart)) { - drawer = new AvatarDrawer().tile(avatar_manager.get_cached_avatar(account, conversation.counterpart), "#", user_color); - if (force_gray || allow_gray && (!is_self_online() || !is_counterpart_online())) drawer.grayscale(); - } else { - Gee.List<Jid>? occupants = muc_manager.get_other_offline_members(conversation.counterpart, account); - if (muc_manager.is_private_room(account, conversation.counterpart) && occupants != null && occupants.size > 0) { - jids = occupants.to_array(); - } else { - drawer = new AvatarDrawer().tile(null, "#", user_color); - if (force_gray || allow_gray && (!is_self_online() || !is_counterpart_online())) drawer.grayscale(); - } - try_load_avatar_async(conversation.counterpart); - } - break; - } - } - if (drawer == null && jids.length > 0) { - drawer = new AvatarDrawer(); - for (int i = 0; i < (jids.length <= 4 ? jids.length : 3); i++) { - Jid avatar_jid = jids[i]; - Jid? real_avatar_jid = null; - if (conversation.type_ != Conversation.Type.CHAT && avatar_jid.equals_bare(conversation.counterpart) && muc_manager.is_private_room(account, conversation.counterpart.bare_jid)) { - // In private room, consider real jid - real_avatar_jid = muc_manager.get_real_jid(avatar_jid, account) ?? avatar_jid; - } - string display_name = Util.get_participant_display_name(stream_interactor, conversation, jids[i]); - string user_color = Util.get_avatar_hex_color(stream_interactor, account, jids[i], conversation); - if (avatar_manager.has_avatar_cached(account, avatar_jid)) { - drawer.tile(avatar_manager.get_cached_avatar(account, avatar_jid), display_name, user_color); - } else if (real_avatar_jid != null && avatar_manager.has_avatar_cached(account, real_avatar_jid)) { - drawer.tile(avatar_manager.get_cached_avatar(account, real_avatar_jid), display_name, user_color); - } else { - drawer.tile(null, display_name, user_color); - try_load_avatar_async(avatar_jid); - if (real_avatar_jid != null) try_load_avatar_async(real_avatar_jid); - } - } - if (jids.length > 4) { - drawer.plus(); - } - if (force_gray || allow_gray && (!is_self_online() || !is_counterpart_online())) drawer.grayscale(); - } - - - if (drawer == null) return false; - drawer.size(height, width) - .scale(base_factor) - .font(get_pango_context().get_font_description().get_family()) - .draw_on_context(ctx); - - if (use_image_surface == 1) { - ctx_in.set_source_surface(ctx.get_target(), 0, 0); - ctx_in.paint(); - ctx_in.set_source_rgb(0, 0, 0); - } - - return true; - } - - private void try_load_avatar_async(Jid jid) { - if (avatar_manager.has_avatar(account, jid)) { - avatar_manager.get_avatar.begin(account, jid, (_, res) => { - var avatar = avatar_manager.get_avatar.end(res); - if (avatar != null) force_redraw(); - }); - } - } - - private void force_redraw() { - this.cached_surface = null; - queue_draw(); - } - - private void disconnect_stream_interactor() { - if (stream_interactor != null) { - presence_manager.show_received.disconnect(on_show_received); - presence_manager.received_offline_presence.disconnect(on_show_received); - avatar_manager.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); - muc_manager.private_room_occupant_updated.disconnect(on_private_room_occupant_updated); - muc_manager.room_info_updated.disconnect(on_room_info_updated); - stream_interactor = null; - } - } - - private void on_show_received(Jid jid, Account account) { - if (!account.equals(this.account)) return; - update_avatar_if_jid(jid); - } - - private void on_received_avatar(Jid jid, Account account) { - if (!account.equals(this.account)) return; - update_avatar_if_jid(jid); - } - - private void update_avatar_if_jid(Jid jid) { - if (jid.equals_bare(this.conversation.counterpart)) { - force_redraw(); - return; - } - foreach (Jid ours in this.jids) { - if (jid.equals_bare(ours)) { - force_redraw(); - return; - } - } - } - - private void on_connection_changed(Account account, ConnectionManager.ConnectionState state) { - if (!account.equals(this.account)) return; - force_redraw(); - } - - private void on_roster_updated(Account account, Jid jid, Roster.Item roster_item) { - if (!account.equals(this.account)) return; - update_avatar_if_jid(jid); - } - - private void on_private_room_occupant_updated(Account account, Jid room, Jid occupant) { - if (!account.equals(this.account)) return; - update_avatar_if_jid(room); - } - - private void on_room_info_updated(Account account, Jid muc_jid) { - if (!account.equals(this.account)) return; - update_avatar_if_jid(muc_jid); - } - - private bool is_self_online() { - if (connection_manager != null) { - return connection_manager.get_state(account) == ConnectionManager.ConnectionState.CONNECTED; - } - return false; - } - - private bool is_counterpart_online() { - return presence_manager.get_full_jids(conversation.counterpart, account) != null; - } - - public void set_conversation(StreamInteractor stream_interactor, Conversation conversation) { - set_avatar(stream_interactor, conversation, new Jid[0]); - } - - public void set_conversation_participant(StreamInteractor stream_interactor, Conversation conversation, Jid sub_jid) { - set_avatar(stream_interactor, conversation, new Jid[] {sub_jid}); - } - - public void set_conversation_participants(StreamInteractor stream_interactor, Conversation conversation, Jid[] sub_jids) { - set_avatar(stream_interactor, conversation, sub_jids); - } - - private void set_avatar(StreamInteractor stream_interactor, Conversation conversation, Jid[] jids) { - if (this.stream_interactor != null && stream_interactor != this.stream_interactor) { - disconnect_stream_interactor(); - } - if (this.stream_interactor != stream_interactor) { - this.stream_interactor = stream_interactor; - presence_manager.show_received.connect(on_show_received); - presence_manager.received_offline_presence.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); - muc_manager.private_room_occupant_updated.connect(on_private_room_occupant_updated); - muc_manager.room_info_updated.connect(on_room_info_updated); - } - this.cached_surface = null; - this.conversation = conversation; - this.jids = jids; - - force_redraw(); - } - - public void set_text(string text, bool gray = true) { - disconnect_stream_interactor(); - this.drawer = new AvatarDrawer().tile(null, text, null); - if (gray) drawer.grayscale(); - force_redraw(); - } -} - -} diff --git a/main/src/ui/call_window/participant_widget.vala b/main/src/ui/call_window/participant_widget.vala index 180923f1..8ec1f5ea 100644 --- a/main/src/ui/call_window/participant_widget.vala +++ b/main/src/ui/call_window/participant_widget.vala @@ -96,11 +96,11 @@ namespace Dino.Ui { shows_video = false; Box box = new Box(Orientation.HORIZONTAL, 0); box.add_css_class("video-placeholder-box"); - AvatarImage avatar = new AvatarImage() { allow_gray=false, hexpand=true, vexpand=true, halign=Align.CENTER, valign=Align.CENTER, height=100, width=100 }; + AvatarPicture avatar = new AvatarPicture() { hexpand=true, vexpand=true, halign=Align.CENTER, valign=Align.CENTER, height_request=100, width_request=100 }; if (conversation != null) { - avatar.set_conversation(stream_interactor, conversation); + avatar.model = new ViewModel.CompatAvatarPictureModel(stream_interactor).set_conversation(conversation); } else { - avatar.set_text("?", false); + avatar.model = new ViewModel.CompatAvatarPictureModel(stream_interactor).add("?"); } box.append(avatar); diff --git a/main/src/ui/contact_details/dialog.vala b/main/src/ui/contact_details/dialog.vala index 134bb559..c897fe4e 100644 --- a/main/src/ui/contact_details/dialog.vala +++ b/main/src/ui/contact_details/dialog.vala @@ -10,7 +10,7 @@ namespace Dino.Ui.ContactDetails { [GtkTemplate (ui = "/im/dino/Dino/contact_details_dialog.ui")] public class Dialog : Gtk.Dialog { - [GtkChild] public unowned AvatarImage avatar; + [GtkChild] public unowned AvatarPicture avatar; [GtkChild] public unowned Util.EntryLabelHybrid name_hybrid; [GtkChild] public unowned Label name_label; [GtkChild] public unowned Label jid_label; @@ -87,7 +87,7 @@ public class Dialog : Gtk.Dialog { } jid_label.label = conversation.counterpart.to_string(); account_label.label = "via " + conversation.account.bare_jid.to_string(); - avatar.set_conversation(stream_interactor, conversation); + avatar.model = new ViewModel.CompatAvatarPictureModel(stream_interactor).set_conversation(conversation); } private void add_entry(string category, string label, string? description, Object wo) { diff --git a/main/src/ui/conversation_content_view/call_widget.vala b/main/src/ui/conversation_content_view/call_widget.vala index 4f7e2953..ab047196 100644 --- a/main/src/ui/conversation_content_view/call_widget.vala +++ b/main/src/ui/conversation_content_view/call_widget.vala @@ -94,15 +94,15 @@ namespace Dino.Ui { } foreach (Jid counterpart in call.counterparts) { - AvatarImage image = new AvatarImage() { force_gray=true, margin_top=2 }; - image.set_conversation_participant(stream_interactor, conversation, counterpart.bare_jid); - multiparty_peer_box.append(image); - multiparty_peer_widgets.add(image); + AvatarPicture picture = new AvatarPicture() { margin_top=2 }; + picture.model = new ViewModel.CompatAvatarPictureModel(stream_interactor).add_participant(conversation, counterpart.bare_jid); + multiparty_peer_box.append(picture); + multiparty_peer_widgets.add(picture); } - AvatarImage image2 = new AvatarImage() { force_gray=true, margin_top=2 }; - image2.set_conversation_participant(stream_interactor, conversation, call.account.bare_jid); - multiparty_peer_box.append(image2); - multiparty_peer_widgets.add(image2); + AvatarPicture picture2 = new AvatarPicture() { margin_top=2 }; + picture2.model = new ViewModel.CompatAvatarPictureModel(stream_interactor).add_participant(conversation, call.account.bare_jid); + multiparty_peer_box.append(picture2); + multiparty_peer_widgets.add(picture2); outer_additional_box.add_css_class("multiparty-participants"); diff --git a/main/src/ui/conversation_content_view/chat_state_populator.vala b/main/src/ui/conversation_content_view/chat_state_populator.vala index 803739c8..2f02c635 100644 --- a/main/src/ui/conversation_content_view/chat_state_populator.vala +++ b/main/src/ui/conversation_content_view/chat_state_populator.vala @@ -68,7 +68,7 @@ private class MetaChatStateItem : Plugins.MetaConversationItem { private Conversation conversation; private Gee.List<Jid> jids = new ArrayList<Jid>(); private Label label; - private AvatarImage image; + private AvatarPicture picture; public MetaChatStateItem(StreamInteractor stream_interactor, Conversation conversation, Gee.List<Jid> jids) { this.stream_interactor = stream_interactor; @@ -79,10 +79,10 @@ private class MetaChatStateItem : Plugins.MetaConversationItem { public override Object? get_widget(Plugins.ConversationItemWidgetInterface outer, Plugins.WidgetType widget_type) { label = new Label("") { xalign=0, vexpand=true }; label.add_css_class("dim-label"); - image = new AvatarImage() { margin_top=2, valign=Align.START }; + picture = new AvatarPicture() { margin_top=2, valign=Align.START }; Box image_content_box = new Box(Orientation.HORIZONTAL, 8); - image_content_box.append(image); + image_content_box.append(picture); image_content_box.append(label); update(); @@ -97,9 +97,7 @@ private class MetaChatStateItem : Plugins.MetaConversationItem { } private void update() { - if (image == null || label == null) return; - - image.set_conversation_participants(stream_interactor, conversation, jids.to_array()); + if (picture == null || label == null) return; Gee.List<string> display_names = new ArrayList<string>(); foreach (Jid jid in jids) { @@ -108,12 +106,26 @@ private class MetaChatStateItem : Plugins.MetaConversationItem { string new_text = ""; if (jids.size > 3) { new_text = _("%s, %s and %i others are typing…").printf(display_names[0], display_names[1], jids.size - 2); + picture.model = new ViewModel.CompatAvatarPictureModel(stream_interactor) + .add_participant(conversation, jids[0]) + .add_participant(conversation, jids[1]) + .add_participant(conversation, jids[2]) + .add("+"); } else if (jids.size == 3) { new_text = _("%s, %s and %s are typing…").printf(display_names[0], display_names[1], display_names[2]); + picture.model = new ViewModel.CompatAvatarPictureModel(stream_interactor) + .add_participant(conversation, jids[0]) + .add_participant(conversation, jids[1]) + .add_participant(conversation, jids[2]); } else if (jids.size == 2) { new_text =_("%s and %s are typing…").printf(display_names[0], display_names[1]); + picture.model = new ViewModel.CompatAvatarPictureModel(stream_interactor) + .add_participant(conversation, jids[0]) + .add_participant(conversation, jids[1]); } else { new_text = _("%s is typing…").printf(display_names[0]); + picture.model = new ViewModel.CompatAvatarPictureModel(stream_interactor) + .add_participant(conversation, jids[0]); } label.label = new_text; diff --git a/main/src/ui/conversation_content_view/conversation_item_skeleton.vala b/main/src/ui/conversation_content_view/conversation_item_skeleton.vala index bbde76b1..5d86f6c7 100644 --- a/main/src/ui/conversation_content_view/conversation_item_skeleton.vala +++ b/main/src/ui/conversation_content_view/conversation_item_skeleton.vala @@ -12,7 +12,7 @@ public class ConversationItemSkeleton : Plugins.ConversationItemWidgetInterface, public Grid main_grid { get; set; } public Label name_label { get; set; } public Label time_label { get; set; } - public AvatarImage avatar_image { get; set; } + public AvatarPicture avatar_picture { get; set; } public Image encryption_image { get; set; } public Image received_image { get; set; } @@ -51,7 +51,7 @@ public class ConversationItemSkeleton : Plugins.ConversationItemWidgetInterface, main_grid.add_css_class("message-box"); name_label = (Label) builder.get_object("name_label"); time_label = (Label) builder.get_object("time_label"); - avatar_image = (AvatarImage) builder.get_object("avatar_image"); + avatar_picture = (AvatarPicture) builder.get_object("avatar_picture"); encryption_image = (Image) builder.get_object("encrypted_image"); received_image = (Image) builder.get_object("marked_image"); @@ -62,7 +62,8 @@ public class ConversationItemSkeleton : Plugins.ConversationItemWidgetInterface, } if (item.requires_header) { - avatar_image.set_conversation_participant(stream_interactor, conversation, item.jid); + // TODO: For MUC messags, use real jid from message if known + avatar_picture.model = new ViewModel.CompatAvatarPictureModel(stream_interactor).add_participant(conversation, item.jid); } this.notify["show-skeleton"].connect(update_margin); @@ -116,7 +117,7 @@ public class ConversationItemSkeleton : Plugins.ConversationItemWidgetInterface, } private void update_margin() { - avatar_image.visible = show_skeleton; + avatar_picture.visible = show_skeleton; name_label.visible = show_skeleton; time_label.visible = show_skeleton; encryption_image.visible = show_skeleton; @@ -286,10 +287,10 @@ public class ConversationItemSkeleton : Plugins.ConversationItemWidgetInterface, time_label.dispose(); time_label = null; } - if (avatar_image != null) { - avatar_image.unparent(); - avatar_image.dispose(); - avatar_image = null; + if (avatar_picture != null) { + avatar_picture.unparent(); + avatar_picture.dispose(); + avatar_picture = null; } if (encryption_image != null) { encryption_image.unparent(); diff --git a/main/src/ui/conversation_content_view/quote_widget.vala b/main/src/ui/conversation_content_view/quote_widget.vala index 6dbf459c..23b62e6a 100644 --- a/main/src/ui/conversation_content_view/quote_widget.vala +++ b/main/src/ui/conversation_content_view/quote_widget.vala @@ -61,13 +61,13 @@ namespace Dino.Ui.Quote { public Widget get_widget(Model model) { Builder builder = new Builder.from_resource("/im/dino/Dino/quote.ui"); - AvatarImage avatar = (AvatarImage) builder.get_object("avatar"); + AvatarPicture avatar = (AvatarPicture) builder.get_object("avatar"); Label author = (Label) builder.get_object("author"); Label time = (Label) builder.get_object("time"); Label message = (Label) builder.get_object("message"); Button abort_button = (Button) builder.get_object("abort-button"); - avatar.set_conversation_participant(model.stream_interactor, model.conversation, model.author_jid); + avatar.model = new ViewModel.CompatAvatarPictureModel(model.stream_interactor).add_participant(model.conversation, model.author_jid); model.bind_property("display-name", author, "label", BindingFlags.SYNC_CREATE); model.bind_property("display-time", time, "label", BindingFlags.SYNC_CREATE); model.bind_property("message", message, "label", BindingFlags.SYNC_CREATE); diff --git a/main/src/ui/conversation_selector/conversation_selector_row.vala b/main/src/ui/conversation_selector/conversation_selector_row.vala index e71176aa..6ef61b3c 100644 --- a/main/src/ui/conversation_selector/conversation_selector_row.vala +++ b/main/src/ui/conversation_selector/conversation_selector_row.vala @@ -12,7 +12,7 @@ namespace Dino.Ui { [GtkTemplate (ui = "/im/dino/Dino/conversation_row.ui")] public class ConversationSelectorRow : ListBoxRow { - [GtkChild] protected unowned AvatarImage image; + [GtkChild] protected unowned AvatarPicture picture; [GtkChild] protected unowned Label name_label; [GtkChild] protected unowned Label time_label; [GtkChild] protected unowned Label nick_label; @@ -101,7 +101,7 @@ public class ConversationSelectorRow : ListBoxRow { x_button.clicked.connect(() => { stream_interactor.get_module(ConversationManager.IDENTITY).close_conversation(conversation); }); - image.set_conversation(stream_interactor, conversation); + picture.model = new ViewModel.CompatAvatarPictureModel(stream_interactor).set_conversation(conversation); conversation.notify["read-up-to-item"].connect(() => update_read()); conversation.notify["pinned"].connect(() => { update_pinned_icon(); }); diff --git a/main/src/ui/global_search.vala b/main/src/ui/global_search.vala index d206220d..6872f631 100644 --- a/main/src/ui/global_search.vala +++ b/main/src/ui/global_search.vala @@ -116,15 +116,15 @@ public class GlobalSearch { // Populate new suggestions foreach(SearchSuggestion suggestion in suggestions) { Builder builder = new Builder.from_resource("/im/dino/Dino/search_autocomplete.ui"); - AvatarImage avatar = (AvatarImage)builder.get_object("image"); + AvatarPicture avatar = (AvatarPicture)builder.get_object("picture"); Label label = (Label)builder.get_object("label"); string display_name; if (suggestion.conversation.type_ == Conversation.Type.GROUPCHAT && !suggestion.conversation.counterpart.equals(suggestion.jid) || suggestion.conversation.type_ == Conversation.Type.GROUPCHAT_PM) { display_name = Util.get_participant_display_name(stream_interactor, suggestion.conversation, suggestion.jid); - avatar.set_conversation_participant(stream_interactor, suggestion.conversation, suggestion.jid); + avatar.model = new ViewModel.CompatAvatarPictureModel(stream_interactor).add_participant(suggestion.conversation, suggestion.jid); } else { display_name = Util.get_conversation_display_name(stream_interactor, suggestion.conversation); - avatar.set_conversation(stream_interactor, suggestion.conversation); + avatar.model = new ViewModel.CompatAvatarPictureModel(stream_interactor).set_conversation(suggestion.conversation); } if (display_name != suggestion.jid.to_string()) { label.set_markup("%s <span font_weight='light' fgalpha='80%%'>%s</span>".printf(Markup.escape_text(display_name), Markup.escape_text(suggestion.jid.to_string()))); @@ -289,10 +289,10 @@ public class GlobalSearch { } private Grid get_skeleton(MessageItem item) { - AvatarImage image = new AvatarImage() { height=32, width=32, margin_end=7, valign=Align.START, allow_gray = false }; - image.set_conversation_participant(stream_interactor, item.conversation, item.jid); + AvatarPicture picture = new AvatarPicture() { height_request=32, width_request=32, margin_end=7, valign=Align.START }; + picture.model = new ViewModel.CompatAvatarPictureModel(stream_interactor).add_participant(item.conversation, item.jid); Grid grid = new Grid() { row_homogeneous=false }; - grid.attach(image, 0, 0, 1, 2); + grid.attach(picture, 0, 0, 1, 2); string display_name = Util.get_participant_display_name(stream_interactor, item.conversation, item.jid); Label name_label = new Label(display_name) { ellipsize=EllipsizeMode.END, xalign=0 }; diff --git a/main/src/ui/manage_accounts/account_row.vala b/main/src/ui/manage_accounts/account_row.vala index b3a33eae..ae734b83 100644 --- a/main/src/ui/manage_accounts/account_row.vala +++ b/main/src/ui/manage_accounts/account_row.vala @@ -7,7 +7,7 @@ namespace Dino.Ui.ManageAccounts { [GtkTemplate (ui = "/im/dino/Dino/manage_accounts/account_row.ui")] public class AccountRow : Gtk.ListBoxRow { - [GtkChild] public unowned AvatarImage image; + [GtkChild] public unowned AvatarPicture picture; [GtkChild] public unowned Label jid_label; [GtkChild] public unowned Image icon; @@ -17,7 +17,7 @@ public class AccountRow : Gtk.ListBoxRow { public AccountRow(StreamInteractor stream_interactor, Account account) { this.stream_interactor = stream_interactor; this.account = account; - image.set_conversation(stream_interactor, new Conversation(account.bare_jid, account, Conversation.Type.CHAT)); + picture.model = new ViewModel.CompatAvatarPictureModel(stream_interactor).add_participant(new Conversation(account.bare_jid, account, Conversation.Type.CHAT), account.bare_jid); jid_label.set_label(account.bare_jid.to_string()); stream_interactor.connection_manager.connection_error.connect((account, error) => { diff --git a/main/src/ui/manage_accounts/dialog.vala b/main/src/ui/manage_accounts/dialog.vala index 0a37b052..a326aeff 100644 --- a/main/src/ui/manage_accounts/dialog.vala +++ b/main/src/ui/manage_accounts/dialog.vala @@ -19,7 +19,7 @@ public class Dialog : Gtk.Dialog { [GtkChild] public unowned Button no_accounts_add; [GtkChild] public unowned Button add_account_button; [GtkChild] public unowned Button remove_account_button; - [GtkChild] public unowned AvatarImage image; + [GtkChild] public unowned AvatarPicture picture; [GtkChild] public unowned Button image_button; [GtkChild] public unowned Label jid_label; [GtkChild] public unowned Label state_label; @@ -178,14 +178,14 @@ public class Dialog : Gtk.Dialog { private void on_received_avatar(Jid jid, Account account) { if (selected_account.equals(account) && jid.equals(account.bare_jid)) { - image.set_conversation(stream_interactor, new Conversation(account.bare_jid, account, Conversation.Type.CHAT)); + picture.model = new ViewModel.CompatAvatarPictureModel(stream_interactor).add_participant(new Conversation(account.bare_jid, account, Conversation.Type.CHAT), account.bare_jid); } } private void populate_grid_data(Account account) { active_switch.state_set.disconnect(change_account_state); - image.set_conversation(stream_interactor, new Conversation(account.bare_jid, account, Conversation.Type.CHAT)); + picture.model = new ViewModel.CompatAvatarPictureModel(stream_interactor).add_participant(new Conversation(account.bare_jid, account, Conversation.Type.CHAT), account.bare_jid); active_switch.set_active(account.enabled); jid_label.label = account.bare_jid.to_string(); diff --git a/main/src/ui/notifier_freedesktop.vala b/main/src/ui/notifier_freedesktop.vala index b6b31d34..8af96975 100644 --- a/main/src/ui/notifier_freedesktop.vala +++ b/main/src/ui/notifier_freedesktop.vala @@ -287,8 +287,12 @@ public class Dino.Ui.FreeDesktopNotifier : NotificationProvider, Object { } private async Variant get_conversation_icon(Conversation conversation) { - AvatarDrawer drawer = yield Util.get_conversation_avatar_drawer(stream_interactor, conversation); - Cairo.ImageSurface surface = drawer.size(40, 40).draw_image_surface(); + CompatAvatarDrawer drawer = new CompatAvatarDrawer() { + model = new ViewModel.CompatAvatarPictureModel(stream_interactor).set_conversation(conversation), + width_request = 40, + height_request = 40 + }; + Cairo.ImageSurface surface = drawer.draw_image_surface(); Gdk.Pixbuf avatar = Gdk.pixbuf_get_from_surface(surface, 0, 0, surface.get_width(), surface.get_height()); var bytes = avatar.pixel_bytes; var image_bytes = Variant.new_from_data<Bytes>(new VariantType("ay"), bytes.get_data(), true, bytes); diff --git a/main/src/ui/notifier_gnotifications.vala b/main/src/ui/notifier_gnotifications.vala index d569a358..90c8ca8c 100644 --- a/main/src/ui/notifier_gnotifications.vala +++ b/main/src/ui/notifier_gnotifications.vala @@ -171,8 +171,12 @@ namespace Dino.Ui { } private async Icon get_conversation_icon(Conversation conversation) throws Error { - AvatarDrawer drawer = yield Util.get_conversation_avatar_drawer(stream_interactor, conversation); - Cairo.ImageSurface surface = drawer.size(40, 40).draw_image_surface(); + CompatAvatarDrawer drawer = new CompatAvatarDrawer() { + model = new ViewModel.CompatAvatarPictureModel(stream_interactor).set_conversation(conversation), + width_request = 40, + height_request = 40 + }; + Cairo.ImageSurface surface = drawer.draw_image_surface(); Gdk.Pixbuf avatar = Gdk.pixbuf_get_from_surface(surface, 0, 0, surface.get_width(), surface.get_height()); uint8[] buffer; avatar.save_to_buffer(out buffer, "png"); diff --git a/main/src/ui/occupant_menu/list_row.vala b/main/src/ui/occupant_menu/list_row.vala index 6b43fe7f..476d9e61 100644 --- a/main/src/ui/occupant_menu/list_row.vala +++ b/main/src/ui/occupant_menu/list_row.vala @@ -8,7 +8,7 @@ namespace Dino.Ui.OccupantMenu { public class ListRow : Object { private Grid main_grid; - private AvatarImage image; + private AvatarPicture picture; public Label name_label; public Conversation? conversation; @@ -17,7 +17,7 @@ public class ListRow : Object { construct { Builder builder = new Builder.from_resource("/im/dino/Dino/occupant_list_item.ui"); main_grid = (Grid) builder.get_object("main_grid"); - image = (AvatarImage) builder.get_object("image"); + picture = (AvatarPicture) builder.get_object("picture"); name_label = (Label) builder.get_object("name_label"); } @@ -26,12 +26,12 @@ public class ListRow : Object { this.jid = jid; name_label.label = Util.get_participant_display_name(stream_interactor, conversation, jid); - image.set_conversation_participant(stream_interactor, conversation, jid); + picture.model = new ViewModel.CompatAvatarPictureModel(stream_interactor).add_participant(conversation, jid); } public ListRow.label(string c, string text) { name_label.label = text; - image.set_text(c); + picture.model = new ViewModel.CompatAvatarPictureModel(null).add(c); } public Widget get_widget() { diff --git a/main/src/ui/util/helper.vala b/main/src/ui/util/helper.vala index 35e16426..d6da72dd 100644 --- a/main/src/ui/util/helper.vala +++ b/main/src/ui/util/helper.vala @@ -61,62 +61,6 @@ public static string color_for_show(string show) { } } -public static async AvatarDrawer get_conversation_avatar_drawer(StreamInteractor stream_interactor, Conversation conversation) { - return yield get_conversation_participants_avatar_drawer(stream_interactor, conversation, new Jid[0]); -} - -public static async AvatarDrawer get_conversation_participants_avatar_drawer(StreamInteractor stream_interactor, Conversation conversation, owned Jid[] jids) { - AvatarManager avatar_manager = stream_interactor.get_module(AvatarManager.IDENTITY); - MucManager muc_manager = stream_interactor.get_module(MucManager.IDENTITY); - if (conversation.type_ != Conversation.Type.GROUPCHAT) { - Jid jid = jids.length == 1 ? jids[0] : conversation.counterpart; - Jid avatar_jid = jid; - if (conversation.type_ == Conversation.Type.GROUPCHAT_PM) avatar_jid = muc_manager.get_real_jid(avatar_jid, conversation.account) ?? avatar_jid; - return new AvatarDrawer().tile(yield avatar_manager.get_avatar(conversation.account, avatar_jid), jids.length == 1 ? - get_participant_display_name(stream_interactor, conversation, jid) : - get_conversation_display_name(stream_interactor, conversation), - Util.get_avatar_hex_color(stream_interactor, conversation.account, jid, conversation)); - } - if (jids.length > 0) { - AvatarDrawer drawer = new AvatarDrawer(); - for (int i = 0; i < (jids.length <= 4 ? jids.length : 3); i++) { - Jid avatar_jid = jids[i]; - Gdk.Pixbuf? part_avatar = yield avatar_manager.get_avatar(conversation.account, avatar_jid); - if (part_avatar == null && avatar_jid.equals_bare(conversation.counterpart) && muc_manager.is_private_room(conversation.account, conversation.counterpart)) { - avatar_jid = muc_manager.get_real_jid(avatar_jid, conversation.account) ?? avatar_jid; - part_avatar = yield avatar_manager.get_avatar(conversation.account, avatar_jid); - } - drawer.tile(part_avatar, get_participant_display_name(stream_interactor, conversation, jids[i]), - Util.get_avatar_hex_color(stream_interactor, conversation.account, jids[i], conversation)); - } - if (jids.length > 4) { - drawer.plus(); - } - return drawer; - } - Gdk.Pixbuf? room_avatar = yield avatar_manager.get_avatar(conversation.account, conversation.counterpart); - Gee.List<Jid>? occupants = muc_manager.get_other_offline_members(conversation.counterpart, conversation.account); - if (room_avatar != null || !muc_manager.is_private_room(conversation.account, conversation.counterpart) || occupants == null || occupants.size == 0) { - return new AvatarDrawer().tile(room_avatar, "#", Util.get_avatar_hex_color(stream_interactor, conversation.account, conversation.counterpart, conversation)); - } - AvatarDrawer drawer = new AvatarDrawer(); - for (int i = 0; i < (occupants.size <= 4 ? occupants.size : 3); i++) { - Jid jid = occupants[i]; - Jid avatar_jid = jid; - Gdk.Pixbuf? part_avatar = yield avatar_manager.get_avatar(conversation.account, avatar_jid); - if (part_avatar == null && avatar_jid.equals_bare(conversation.counterpart) && muc_manager.is_private_room(conversation.account, conversation.counterpart)) { - avatar_jid = muc_manager.get_real_jid(avatar_jid, conversation.account) ?? avatar_jid; - part_avatar = yield avatar_manager.get_avatar(conversation.account, avatar_jid); - } - drawer.tile(part_avatar, get_participant_display_name(stream_interactor, conversation, jid), - Util.get_avatar_hex_color(stream_interactor, conversation.account, jid, conversation)); - } - if (occupants.size > 4) { - drawer.plus(); - } - return drawer; -} - public static string get_conversation_display_name(StreamInteractor stream_interactor, Conversation conversation) { return Dino.get_conversation_display_name(stream_interactor, conversation, _("%s from %s")); } @@ -137,27 +81,6 @@ public static string get_occupant_display_name(StreamInteractor stream_interacto return Dino.get_occupant_display_name(stream_interactor, conversation, jid, me_is_me ? _("Me") : null); } -// TODO this has no usages? -//public static void image_set_from_scaled_pixbuf(Image image, Gdk.Pixbuf pixbuf, int scale = 0, int width = 0, int height = 0) { -// if (scale == 0) scale = image.scale_factor; -// Cairo.Surface surface = Gdk.cairo_surface_create_from_pixbuf(pixbuf, scale, image.get_window()); -// if (height == 0 && width != 0) { -// height = (int) ((double) width / pixbuf.width * pixbuf.height); -// } else if (height != 0 && width == 0) { -// width = (int) ((double) height / pixbuf.height * pixbuf.width); -// } -// if (width != 0) { -// Cairo.Surface surface_new = new Cairo.Surface.similar_image(surface, Cairo.Format.ARGB32, width, height); -// Cairo.Context context = new Cairo.Context(surface_new); -// context.scale((double) width * scale / pixbuf.width, (double) height * scale / pixbuf.height); -// context.set_source_surface(surface, 0, 0); -// context.get_source().set_filter(Cairo.Filter.BEST); -// context.paint(); -// surface = surface_new; -// } -// image.set_from_surface(surface); -//} - public static Gdk.RGBA get_label_pango_color(Label label, string css_color) { Gtk.CssProvider provider = force_color(label, css_color); Gdk.RGBA color_rgba = label.get_style_context().get_color(); 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 |