dino/main/src/ui/util/helper.vala

472 lines
22 KiB
Vala
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Gee;
using Gtk;
using Dino.Entities;
using Xmpp;
namespace Dino.Ui.Util {
private static Regex URL_REGEX;
private static Regex CODE_BLOCK_REGEX;
private static Map<unichar, unichar> MATCHING_CHARS;
private const unichar[] NON_TRAILING_CHARS = {'\'', '"', ',', '.', ';', '!', '?', '»', '”', '', '`', '~', '‽', ':', '>', '*', '_'};
private const string[] ALLOWED_SCHEMAS = {"http", "https", "ftp", "ftps", "irc", "ircs", "xmpp", "mailto", "sms", "smsto", "mms", "tel", "geo", "openpgp4fpr", "im", "news", "nntp", "sip", "ssh", "bitcoin", "sftp", "magnet", "vnc"};
private const string[] tango_colors_light = {"FCE94F", "FCAF3E", "E9B96E", "8AE234", "729FCF", "AD7FA8", "EF2929"};
private const string[] tango_colors_medium = {"EDD400", "F57900", "C17D11", "73D216", "3465A4", "75507B", "CC0000"};
private const string[] material_colors_800 = {"D32F2F", "C2185B", "7B1FA2", "512DA8", "303F9F", "1976D2", "0288D1", "0097A7", "00796B", "388E3C", "689F38", "AFB42B", "FFA000", "F57C00", "E64A19", "5D4037"};
private const string[] material_colors_500 = {"F44336", "E91E63", "9C27B0", "673AB7", "3f51B5", "2196F3", "03A9f4", "00BCD4", "009688", "4CAF50", "8BC34a", "CDDC39", "FFC107", "FF9800", "FF5722", "795548"};
private const string[] material_colors_300 = {"E57373", "F06292", "BA68C8", "9575CD", "7986CB", "64B5F6", "4FC3F7", "4DD0E1", "4DB6AC", "81C784", "AED581", "DCE775", "FFD54F", "FFB74D", "FF8A65", "A1887F"};
private const string[] material_colors_200 = {"EF9A9A", "F48FB1", "CE93D8", "B39DDB", "9FA8DA", "90CAF9", "81D4FA", "80DEEA", "80CBC4", "A5D6A7", "C5E1A5", "E6EE9C", "FFE082", "FFCC80", "FFAB91", "BCAAA4"};
public static string get_avatar_hex_color(StreamInteractor stream_interactor, Account account, Jid jid, Conversation? conversation = null) {
uint hash = get_relevant_jid(stream_interactor, account, jid, conversation).to_string().hash();
return material_colors_300[hash % material_colors_300.length];
// return tango_colors_light[name.hash() % tango_colors_light.length];
}
public static string get_name_hex_color(StreamInteractor stream_interactor, Account account, Jid jid, bool dark_theme = false, Conversation? conversation = null) {
uint hash = get_relevant_jid(stream_interactor, account, jid, conversation).to_string().hash();
if (dark_theme) {
return material_colors_300[hash % material_colors_300.length];
} else {
return material_colors_500[hash % material_colors_500.length];
}
// return tango_colors_medium[name.hash() % tango_colors_medium.length];
}
private static Jid get_relevant_jid(StreamInteractor stream_interactor, Account account, Jid jid, Conversation? conversation = null) {
Conversation conversation_ = conversation ?? stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation(jid.bare_jid, account);
if (conversation_ != null && conversation_.type_ == Conversation.Type.GROUPCHAT) {
Jid? real_jid = stream_interactor.get_module(MucManager.IDENTITY).get_real_jid(jid, account);
if (real_jid != null) {
return real_jid.bare_jid;
}
} else {
return jid.bare_jid;
}
return jid;
}
public static string color_for_show(string show) {
switch(show) {
case "online": return "#9CCC65";
case "away": return "#FFCA28";
case "chat": return "#66BB6A";
case "xa": return "#EF5350";
case "dnd": return "#EF5350";
default: return "#BDBDBD";
}
}
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"));
}
public static string get_participant_display_name(StreamInteractor stream_interactor, Conversation conversation, Jid participant, bool me_is_me = false) {
return Dino.get_participant_display_name(stream_interactor, conversation, participant, me_is_me ? _("Me") : null);
}
public static string? get_real_display_name(StreamInteractor stream_interactor, Account account, Jid jid, bool me_is_me = false) {
return Dino.get_real_display_name(stream_interactor, account, jid, me_is_me ? _("Me") : null);
}
public static string get_groupchat_display_name(StreamInteractor stream_interactor, Account account, Jid jid) {
return Dino.get_groupchat_display_name(stream_interactor, account, jid);
}
public static string get_occupant_display_name(StreamInteractor stream_interactor, Conversation conversation, Jid jid, bool me_is_me = false, bool muc_real_name = false) {
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();
label.get_style_context().remove_provider(provider);
return color_rgba;
}
public static string rgba_to_hex(Gdk.RGBA rgba) {
return "#%02x%02x%02x%02x".printf(
(uint)(Math.round(rgba.red*255)),
(uint)(Math.round(rgba.green*255)),
(uint)(Math.round(rgba.blue*255)),
(uint)(Math.round(rgba.alpha*255)))
.up();
}
private const string force_background_css = "%s { background-color: %s; }";
private const string force_color_css = "%s { color: %s; }";
public static Gtk.CssProvider force_css(Gtk.Widget widget, string css) {
var p = new Gtk.CssProvider();
try {
p.load_from_data(css.data);
widget.get_style_context().add_provider(p, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
} catch (GLib.Error err) {
// handle err
}
return p;
}
public static void force_background(Gtk.Widget widget, string color, string selector = "*") {
force_css(widget, force_background_css.printf(selector, color));
}
public static Gtk.CssProvider force_color(Gtk.Widget widget, string color, string selector = "*") {
return force_css(widget, force_color_css.printf(selector, color));
}
public static void force_error_color(Gtk.Widget widget, string selector = "*") {
force_color(widget, "@error_color", selector);
}
public static bool is_dark_theme(Gtk.Widget widget) {
Gdk.RGBA bg = widget.get_style_context().get_color();
return (bg.red > 0.5 && bg.green > 0.5 && bg.blue > 0.5);
}
private static uint8 is24h = 0;
public static bool is_24h_format() {
if (is24h == 0) {
string p_format = " "; // Leaving room to be filled by strftime
Time.local(0).strftime((char[]) p_format.data, "%p");
is24h = p_format.strip() == "" ? 1 : -1;
}
return is24h == 1;
}
public static string format_time(DateTime datetime, string format_24h, string format_12h) {
string format = Util.is_24h_format() ? format_24h : format_12h;
if (!get_charset(null)) {
// No UTF-8 support, use simple colon for time instead
format = format.replace("", ":");
}
return datetime.format(format);
}
public static Regex get_url_regex() {
if (URL_REGEX == null) {
URL_REGEX = /\b(((http|ftp)s?:\/\/|(ircs?|xmpp|mailto|sms|smsto|mms|tel|geo|openpgp4fpr|im|news|nntp|sip|ssh|bitcoin|sftp|magnet|vnc|urn):)\S+)/;
}
return URL_REGEX;
}
public static Map<unichar, unichar> get_matching_chars() {
if (MATCHING_CHARS == null) {
MATCHING_CHARS = new HashMap<unichar, unichar>();
MATCHING_CHARS[")".get_char(0)] = "(".get_char(0);
MATCHING_CHARS["]".get_char(0)] = "[".get_char(0);
MATCHING_CHARS["}".get_char(0)] = "{".get_char(0);
}
return MATCHING_CHARS;
}
public static string parse_add_markup(string s_, string? highlight_word, bool parse_links, bool parse_text_markup) {
bool ignore_out_var = false;
return parse_add_markup_theme(s_, highlight_word, parse_links, parse_text_markup, parse_text_markup, false, ref ignore_out_var);
}
public static string parse_add_markup_theme(string s_, string? highlight_word, bool parse_links, bool parse_text_markup, bool parse_quotes, bool dark_theme, ref bool theme_dependent, bool already_escaped_ = false) {
string s = s_;
bool already_escaped = already_escaped_;
if (parse_quotes) {
string gt = already_escaped ? "&gt;" : ">";
Regex quote_regex = new Regex("((?<=\n)" + gt + ".*(\n|$))|(^" + gt + ".*(\n|$))");
MatchInfo quote_match_info;
quote_regex.match(s.down(), 0, out quote_match_info);
if (quote_match_info.matches()) {
int start, end;
string dim_color = dark_theme ? "#BDBDBD": "#707070";
theme_dependent = true;
quote_match_info.fetch_pos(0, out start, out end);
return parse_add_markup_theme(s[0:start], highlight_word, parse_links, parse_text_markup, parse_quotes, dark_theme, ref theme_dependent, already_escaped) +
@"<span color='$dim_color'>$gt" + parse_add_markup_theme(s[start + gt.length:end], highlight_word, parse_links, parse_text_markup, false, dark_theme, ref theme_dependent, already_escaped) + "</span>" +
parse_add_markup_theme(s[end:s.length], highlight_word, parse_links, parse_text_markup, parse_quotes, dark_theme, ref theme_dependent, already_escaped);
}
}
if (parse_links && !already_escaped) {
MatchInfo match_info;
get_url_regex().match(s.down(), 0, out match_info);
while (match_info.matches()) {
int start, end;
match_info.fetch_pos(0, out start, out end);
string link = s[start:end];
if (GLib.Uri.parse_scheme(link) in ALLOWED_SCHEMAS) {
Map<unichar, unichar> matching_chars = get_matching_chars();
unichar close_char;
int last_char_index = link.length;
while (link.get_prev_char(ref last_char_index, out close_char)) {
if (matching_chars.has_key(close_char)) {
unichar open_char = matching_chars[close_char];
unichar char;
int index = 0;
int open = 0, close = 0;
while (link.get_next_char(ref index, out char)) {
if (char == open_char) {
open++;
} else if (char == close_char) {
close++;
}
}
if (close > open) {
// Remove last char from url
end -= close_char.to_string().length;
link = s[start:end];
} else {
break;
}
} else if (close_char in NON_TRAILING_CHARS) {
// Remove last char from url
end -= close_char.to_string().length;
link = s[start:end];
} else {
break;
}
}
return parse_add_markup_theme(s[0:start], highlight_word, parse_links, parse_text_markup, false, dark_theme, ref theme_dependent, already_escaped) +
"<a href=\"" + Markup.escape_text(link) + "\">" +
parse_add_markup_theme(link, highlight_word, false, false, false, dark_theme, ref theme_dependent, already_escaped) +
"</a>" +
parse_add_markup_theme(s[end:s.length], highlight_word, parse_links, parse_text_markup, false, dark_theme, ref theme_dependent, already_escaped);
}
match_info.next();
}
}
if (!already_escaped) {
s = Markup.escape_text(s);
already_escaped = true;
}
if (highlight_word != null) {
try {
Regex highlight_regex = new Regex("\\b" + Regex.escape_string(highlight_word.down()) + "\\b");
MatchInfo match_info;
highlight_regex.match(s.down(), 0, out match_info);
if (match_info.matches()) {
int start, end;
match_info.fetch_pos(0, out start, out end);
return parse_add_markup_theme(s[0:start], highlight_word, parse_links, parse_text_markup, false, dark_theme, ref theme_dependent, already_escaped) +
"<b>" + s[start:end] + "</b>" +
parse_add_markup_theme(s[end:s.length], highlight_word, parse_links, parse_text_markup, false, dark_theme, ref theme_dependent, already_escaped);
}
} catch (RegexError e) {
assert_not_reached();
}
}
if (parse_text_markup) {
string[] markup_string = new string[]{"`", "_", "*", "~"};
string[] convenience_tag = new string[]{"tt", "i", "b", "s"};
for (int i = 0; i < markup_string.length; i++) {
string markup_esc = Regex.escape_string(markup_string[i]);
try {
Regex regex = new Regex("(^|\\s)" + markup_esc + "(\\S|\\S.*?\\S)" + markup_esc);
MatchInfo match_info;
regex.match(s.down(), 0, out match_info);
if (match_info.matches()) {
int start, end;
match_info.fetch_pos(2, out start, out end);
return parse_add_markup_theme(s[0:start-1], highlight_word, parse_links, parse_text_markup, false, dark_theme, ref theme_dependent, already_escaped) +
"<span color='#9E9E9E'>" + s[start-1:start] + "</span>" +
@"<$(convenience_tag[i])>" + s[start:end] + @"</$(convenience_tag[i])>" +
"<span color='#9E9E9E'>" + s[end:end+1] + "</span>" +
parse_add_markup_theme(s[end+1:s.length], highlight_word, parse_links, parse_text_markup, false, dark_theme, ref theme_dependent, already_escaped);
}
} catch (RegexError e) {
assert_not_reached();
}
}
}
return s;
}
/**
* This is a heuristic to count emojis in a string {@link http://example.com/}
*
* @param markup_text string to search in
* @return number of emojis, or -1 if text includes non-emojis.
*/
public int get_only_emoji_count(string markup_text) {
int emoji_no = 0;
int index_ref = 0;
unichar curchar = 0, altchar = 0;
bool last_was_emoji = false, last_was_modifier_base = false, last_was_keycap = false;
while (markup_text.get_next_char(ref index_ref, out curchar)) {
if (last_was_emoji && last_was_keycap && curchar == 0x20E3) {
// keycap sequence
continue;
}
last_was_keycap = false;
if (last_was_emoji && curchar == 0x200D && markup_text.get_next_char(ref index_ref, out curchar)) {
// zero width joiner
last_was_emoji = false;
emoji_no--;
}
if (last_was_emoji && curchar == 0xFE0F) {
// Variation selector after emoji is useless, ignoring.
} else if (last_was_emoji && last_was_modifier_base && ICU.has_binary_property(curchar, ICU.Property.EMOJI_MODIFIER)) {
// still an emoji, but no longer a modifier base
last_was_modifier_base = false;
} else if (ICU.has_binary_property(curchar, ICU.Property.EMOJI_PRESENTATION)) {
if (ICU.has_binary_property(curchar, ICU.Property.EMOJI_MODIFIER_BASE)) {
last_was_modifier_base = true;
}
emoji_no++;
last_was_emoji = true;
} else if (curchar == ' ') {
last_was_emoji = false;
} else if (markup_text.get_next_char(ref index_ref, out altchar) && altchar == 0xFE0F) {
// U+FE0F = VARIATION SELECTOR-16
emoji_no++;
last_was_emoji = true;
last_was_keycap = (curchar >= 0x30 && curchar <= 0x39) || curchar == 0x23 || curchar == 0x2A;
} else {
return -1;
}
}
return emoji_no;
}
public string summarize_whitespaces_to_space(string s) {
try {
return (/\s+/).replace_literal(s, -1, 0, " ");
} catch (RegexError e) {
critical("RegexError when summarizing whitespaces in '%s': %s", s, e.message);
return s;
}
}
public void present_window(Window window) {
#if GDK3_WITH_X11
Gdk.X11.Window x11window = window.get_window() as Gdk.X11.Window;
if (x11window != null) {
window.present_with_time(Gdk.X11.get_server_time(x11window));
} else {
window.present();
}
#else
window.present();
#endif
}
public bool use_csd() {
return ((Application) GLib.Application.get_default()).use_csd();
}
public Widget? widget_if_tooltips_active(Widget w) {
return use_tooltips() ? w : null;
}
public string? string_if_tooltips_active(string? s) {
return use_tooltips() ? s : null;
}
public bool use_tooltips() {
return Gtk.MINOR_VERSION != 6 || (Gtk.MICRO_VERSION < 4 || Gtk.MICRO_VERSION > 6);
}
public static void menu_button_set_icon_with_size(MenuButton menu_button, string icon_name, int pixel_size) {
#if GTK_4_6 && VALA_0_52
menu_button.set_child(new Image.from_icon_name(icon_name) { pixel_size=pixel_size });
#else
menu_button.set_icon_name(icon_name);
var button = menu_button.get_first_child() as Button;
if (button == null) return;
var box = button.child as Box;
if (box == null) return;
var image = box.get_first_child() as Image;
if (image == null) return;
image.pixel_size = pixel_size;
#endif
}
}