2 * Copyright (C) 2008-2012 Collabora Ltd.
3 * Copyright (C) 2012 Red Hat, Inc.
5 * This library is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU Lesser General Public
7 * License as published by the Free Software Foundation; either
8 * version 2.1 of the License, or (at your option) any later version.
10 * This library is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 * Lesser General Public License for more details.
15 * You should have received a copy of the GNU Lesser General Public
16 * License along with this library; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
19 * Authors: Xavier Claessens <xclaesse@gmail.com>
23 #include "empathy-theme-adium.h"
25 #include <glib/gi18n-lib.h>
26 #include <tp-account-widgets/tpaw-images.h>
27 #include <tp-account-widgets/tpaw-time.h>
28 #include <tp-account-widgets/tpaw-pixbuf-utils.h>
29 #include <tp-account-widgets/tpaw-utils.h>
31 #include "empathy-gsettings.h"
32 #include "empathy-images.h"
33 #include "empathy-plist.h"
34 #include "empathy-smiley-manager.h"
35 #include "empathy-ui-utils.h"
36 #include "empathy-utils.h"
37 #include "empathy-webkit-utils.h"
39 #define DEBUG_FLAG EMPATHY_DEBUG_CHAT
40 #include "empathy-debug.h"
42 #define BORING_DPI_DEFAULT 96
44 /* "Join" consecutive messages with timestamps within five minutes */
45 #define MESSAGE_JOIN_PERIOD 5*60
47 struct _EmpathyThemeAdiumPriv
49 EmpathyAdiumData
*data
;
50 EmpathySmileyManager
*smiley_manager
;
51 EmpathyContact
*first_contact
;
52 EmpathyContact
*last_contact
;
53 gint64 first_timestamp
;
54 gint64 last_timestamp
;
55 gboolean first_is_backlog
;
56 gboolean last_is_backlog
;
58 /* Queue of QueuedItem*s containing an EmpathyMessage or string */
60 /* Queue of guint32 of pending message id to remove unread
61 * marker for when we lose focus. */
62 GQueue acked_messages
;
63 GtkWidget
*inspector_window
;
65 GSettings
*gsettings_chat
;
66 GSettings
*gsettings_desktop
;
69 gboolean has_unread_message
;
70 gboolean allow_scrolling
;
72 gboolean in_construction
;
73 gboolean show_avatars
;
76 struct _EmpathyAdiumData
81 gchar
*default_avatar_filename
;
82 gchar
*default_incoming_avatar_filename
;
83 gchar
*default_outgoing_avatar_filename
;
86 gboolean custom_template
;
87 /* gchar* -> gchar* both owned */
88 GHashTable
*date_format_cache
;
91 const gchar
*template_html
;
92 const gchar
*content_html
;
93 const gchar
*in_content_html
;
94 const gchar
*in_context_html
;
95 const gchar
*in_nextcontent_html
;
96 const gchar
*in_nextcontext_html
;
97 const gchar
*out_content_html
;
98 const gchar
*out_context_html
;
99 const gchar
*out_nextcontent_html
;
100 const gchar
*out_nextcontext_html
;
101 const gchar
*status_html
;
103 /* Above html strings are pointers to strings stored in this array.
104 * We do this because of fallbacks, some htmls could be pointing the
106 GPtrArray
*strings_to_free
;
109 static gchar
* adium_info_dup_path_for_variant (GHashTable
*info
,
110 const gchar
*variant
);
119 G_DEFINE_TYPE (EmpathyThemeAdium
, empathy_theme_adium
,
120 WEBKIT_TYPE_WEB_VIEW
)
134 gboolean should_highlight
;
138 queue_item (GQueue
*queue
,
142 gboolean should_highlight
,
145 QueuedItem
*item
= g_slice_new0 (QueuedItem
);
149 item
->msg
= g_object_ref (msg
);
150 item
->str
= g_strdup (str
);
151 item
->should_highlight
= should_highlight
;
154 g_queue_push_head (queue
, item
);
156 g_queue_push_tail (queue
, item
);
162 free_queued_item (QueuedItem
*item
)
164 tp_clear_object (&item
->msg
);
167 g_slice_free (QueuedItem
, item
);
171 theme_adium_policy_decision_requested_cb (WebKitWebView
*view
,
172 WebKitPolicyDecision
*decision
,
173 WebKitPolicyDecisionType decision_type
,
176 if (decision_type
!= WEBKIT_POLICY_DECISION_TYPE_NAVIGATION_ACTION
)
179 return empathy_webkit_handle_navigation (view
, WEBKIT_NAVIGATION_POLICY_DECISION (decision
));
182 /* Replace each %@ in format with string passed in args */
184 string_with_format (const gchar
*format
,
185 const gchar
*first_string
,
192 va_start (args
, first_string
);
193 result
= g_string_sized_new (strlen (format
));
194 for (str
= first_string
; str
!= NULL
; str
= va_arg (args
, const gchar
*))
198 next
= strstr (format
, "%@");
202 g_string_append_len (result
, format
, next
- format
);
203 g_string_append (result
, str
);
206 g_string_append (result
, format
);
209 return g_string_free (result
, FALSE
);
213 theme_adium_load_template (EmpathyThemeAdium
*self
)
219 self
->priv
->pages_loading
++;
220 basedir_uri
= g_strconcat ("file://", self
->priv
->data
->basedir
, NULL
);
222 variant_path
= adium_info_dup_path_for_variant (self
->priv
->data
->info
,
223 self
->priv
->variant
);
225 template = string_with_format (self
->priv
->data
->template_html
,
228 webkit_web_view_load_html (WEBKIT_WEB_VIEW (self
),
229 template, basedir_uri
);
231 g_free (basedir_uri
);
232 g_free (variant_path
);
237 theme_adium_parse_body (EmpathyThemeAdium
*self
,
241 TpawStringParser
*parsers
;
244 /* Check if we have to parse smileys */
245 parsers
= empathy_webkit_get_string_parser (
246 g_settings_get_boolean (self
->priv
->gsettings_chat
,
247 EMPATHY_PREFS_CHAT_SHOW_SMILEYS
));
249 /* Parse text and construct string with links and smileys replaced
250 * by html tags. Also escape text to make sure html code is
251 * displayed verbatim. */
252 string
= g_string_sized_new (strlen (text
));
254 /* wrap this in HTML that allows us to find the message for later
256 if (!tp_str_empty (token
))
257 g_string_append_printf (string
,
258 "<span id=\"message-token-%s\">",
261 tpaw_string_parser_substr (text
, -1, parsers
, string
);
263 if (!tp_str_empty (token
))
264 g_string_append (string
, "</span>");
266 /* Wrap body in order to make tabs and multiple spaces displayed
267 * properly. See bug #625745. */
268 g_string_prepend (string
, "<div style=\"display: inline; "
269 "white-space: pre-wrap\"'>");
270 g_string_append (string
, "</div>");
272 return g_string_free (string
, FALSE
);
276 escape_and_append_len (GString
*string
, const gchar
*str
, gint len
)
278 while (str
!= NULL
&& *str
!= '\0' && len
!= 0)
284 g_string_append (string
, "\\\\");
288 g_string_append (string
, "\\\"");
291 /* Remove end of lines */
294 g_string_append_c (string
, *str
);
302 /* If *str starts with match, returns TRUE and move pointer to the end */
304 theme_adium_match (const gchar
**str
,
309 len
= strlen (match
);
310 if (strncmp (*str
, match
, len
) == 0)
319 /* Like theme_adium_match() but also return the X part if match is
322 theme_adium_match_with_format (const gchar
**str
,
326 const gchar
*cur
= *str
;
329 if (!theme_adium_match (&cur
, match
))
334 end
= strstr (cur
, "}%");
338 *format
= g_strndup (cur
, end
- cur
);
343 /* List of colors used by %senderColor%. Copied from
344 * adium/Frameworks/AIUtilities\ Framework/Source/AIColorAdditions.m
346 static gchar
*colors
[] = {
347 "aqua", "aquamarine", "blue", "blueviolet", "brown", "burlywood", "cadetblue",
348 "chartreuse", "chocolate", "coral", "cornflowerblue", "crimson", "cyan",
349 "darkblue", "darkcyan", "darkgoldenrod", "darkgreen", "darkgrey", "darkkhaki",
350 "darkmagenta", "darkolivegreen", "darkorange", "darkorchid", "darkred",
351 "darksalmon", "darkseagreen", "darkslateblue", "darkslategrey",
352 "darkturquoise", "darkviolet", "deeppink", "deepskyblue", "dimgrey",
353 "dodgerblue", "firebrick", "forestgreen", "fuchsia", "gold", "goldenrod",
354 "green", "greenyellow", "grey", "hotpink", "indianred", "indigo", "lawngreen",
355 "lightblue", "lightcoral",
356 "lightgreen", "lightgrey", "lightpink", "lightsalmon", "lightseagreen",
357 "lightskyblue", "lightslategrey", "lightsteelblue", "lime", "limegreen",
358 "magenta", "maroon", "mediumaquamarine", "mediumblue", "mediumorchid",
359 "mediumpurple", "mediumseagreen", "mediumslateblue", "mediumspringgreen",
360 "mediumturquoise", "mediumvioletred", "midnightblue", "navy", "olive",
361 "olivedrab", "orange", "orangered", "orchid", "palegreen", "paleturquoise",
362 "palevioletred", "peru", "pink", "plum", "powderblue", "purple", "red",
363 "rosybrown", "royalblue", "saddlebrown", "salmon", "sandybrown", "seagreen",
364 "sienna", "silver", "skyblue", "slateblue", "slategrey", "springgreen",
365 "steelblue", "tan", "teal", "thistle", "tomato", "turquoise", "violet",
370 nsdate_to_strftime (EmpathyAdiumData
*data
, const gchar
*nsdate
)
372 /* Convert from NSDateFormatter
373 * (http://www.stepcase.com/blog/2008/12/02/format-string-for-the-iphone-nsdateformatter/)
374 * to strftime supported by g_date_time_format.
375 * FIXME: table is incomplete, doc of g_date_time_format has a table of
377 * FIXME: g_date_time_format in GLib 2.28 does 0 padding by default, but
378 * in 2.29.x we have to explictely request padding with %0x */
379 static const gchar
*convert_table
[] = {
381 "A", NULL
, // 0~86399999 (Millisecond of Day)
383 "cccc", "%A", // Sunday/Monday/Tuesday/Wednesday/Thursday/Friday/Saturday
384 "ccc", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
385 "cc", "%u", // 1~7 (Day of Week)
386 "c", "%u", // 1~7 (Day of Week)
388 "dd", "%d", // 1~31 (0 padded Day of Month)
389 "d", "%d", // 1~31 (0 padded Day of Month)
390 "D", "%j", // 1~366 (0 padded Day of Year)
392 "e", "%u", // 1~7 (0 padded Day of Week)
393 "EEEE", "%A", // Sunday/Monday/Tuesday/Wednesday/Thursday/Friday/Saturday
394 "EEE", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
395 "EE", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
396 "E", "%a", // Sun/Mon/Tue/Wed/Thu/Fri/Sat
398 "F", NULL
, // 1~5 (0 padded Week of Month, first day of week = Monday)
400 "g", NULL
, // Julian Day Number (number of days since 4713 BC January 1)
401 "GGGG", NULL
, // Before Christ/Anno Domini
402 "GGG", NULL
, // BC/AD (Era Designator Abbreviated)
403 "GG", NULL
, // BC/AD (Era Designator Abbreviated)
404 "G", NULL
, // BC/AD (Era Designator Abbreviated)
406 "h", "%I", // 1~12 (0 padded Hour (12hr))
407 "H", "%H", // 0~23 (0 padded Hour (24hr))
409 "k", NULL
, // 1~24 (0 padded Hour (24hr)
410 "K", NULL
, // 0~11 (0 padded Hour (12hr))
412 "LLLL", "%B", // January/February/March/April/May/June/July/August/September/October/November/December
413 "LLL", "%b", // Jan/Feb/Mar/Apr/May/Jun/Jul/Aug/Sep/Oct/Nov/Dec
414 "LL", "%m", // 1~12 (0 padded Month)
415 "L", "%m", // 1~12 (0 padded Month)
417 "m", "%M", // 0~59 (0 padded Minute)
418 "MMMM", "%B", // January/February/March/April/May/June/July/August/September/October/November/December
419 "MMM", "%b", // Jan/Feb/Mar/Apr/May/Jun/Jul/Aug/Sep/Oct/Nov/Dec
420 "MM", "%m", // 1~12 (0 padded Month)
421 "M", "%m", // 1~12 (0 padded Month)
423 "qqqq", NULL
, // 1st quarter/2nd quarter/3rd quarter/4th quarter
424 "qqq", NULL
, // Q1/Q2/Q3/Q4
425 "qq", NULL
, // 1~4 (0 padded Quarter)
426 "q", NULL
, // 1~4 (0 padded Quarter)
427 "QQQQ", NULL
, // 1st quarter/2nd quarter/3rd quarter/4th quarter
428 "QQQ", NULL
, // Q1/Q2/Q3/Q4
429 "QQ", NULL
, // 1~4 (0 padded Quarter)
430 "Q", NULL
, // 1~4 (0 padded Quarter)
432 "s", "%S", // 0~59 (0 padded Second)
433 "S", NULL
, // (rounded Sub-Second)
435 "u", "%Y", // (0 padded Year)
437 "vvvv", "%Z", // (General GMT Timezone Name)
438 "vvv", "%Z", // (General GMT Timezone Abbreviation)
439 "vv", "%Z", // (General GMT Timezone Abbreviation)
440 "v", "%Z", // (General GMT Timezone Abbreviation)
442 "w", "%W", // 1~53 (0 padded Week of Year, 1st day of week = Sunday, NB, 1st week of year starts from the last Sunday of last year)
443 "W", NULL
, // 1~5 (0 padded Week of Month, 1st day of week = Sunday)
445 "yyyy", "%Y", // (Full Year)
446 "yyy", "%y", // (2 Digits Year)
447 "yy", "%y", // (2 Digits Year)
448 "y", "%Y", // (Full Year)
449 "YYYY", NULL
, // (Full Year, starting from the Sunday of the 1st week of year)
450 "YYY", NULL
, // (2 Digits Year, starting from the Sunday of the 1st week of year)
451 "YY", NULL
, // (2 Digits Year, starting from the Sunday of the 1st week of year)
452 "Y", NULL
, // (Full Year, starting from the Sunday of the 1st week of year)
454 "zzzz", NULL
, // (Specific GMT Timezone Name)
455 "zzz", NULL
, // (Specific GMT Timezone Abbreviation)
456 "zz", NULL
, // (Specific GMT Timezone Abbreviation)
457 "z", NULL
, // (Specific GMT Timezone Abbreviation)
458 "Z", "%z", // +0000 (RFC 822 Timezone)
467 str
= g_hash_table_lookup (data
->date_format_cache
, nsdate
);
472 /* Copy nsdate into string, replacing occurences of NSDateFormatter tags
473 * by corresponding strftime tag. */
474 string
= g_string_sized_new (strlen (nsdate
));
475 for (i
= 0; nsdate
[i
] != '\0'; i
++)
477 gboolean found
= FALSE
;
479 /* even indexes are NSDateFormatter tag, odd indexes are the
480 * corresponding strftime tag */
481 for (j
= 0; j
< G_N_ELEMENTS (convert_table
); j
+= 2)
483 if (g_str_has_prefix (nsdate
+ i
, convert_table
[j
]))
492 /* If we don't have a replacement, just ignore that tag */
493 if (convert_table
[j
+ 1] != NULL
)
494 g_string_append (string
, convert_table
[j
+ 1]);
496 i
+= strlen (convert_table
[j
]) - 1;
500 g_string_append_c (string
, nsdate
[i
]);
504 DEBUG ("Date format converted '%s' → '%s'", nsdate
, string
->str
);
506 /* The cache takes ownership of string->str */
507 g_hash_table_insert (data
->date_format_cache
, g_strdup (nsdate
), string
->str
);
508 return g_string_free (string
, FALSE
);
512 theme_adium_add_html (EmpathyThemeAdium
*self
,
515 const gchar
*message
,
516 const gchar
*avatar_filename
,
518 const gchar
*contact_id
,
519 const gchar
*service_name
,
520 const gchar
*message_classes
,
524 PangoDirection direction
)
528 const gchar
*cur
= NULL
;
532 /* Make some search-and-replace in the html code */
533 string
= g_string_sized_new (strlen (html
) + strlen (message
));
534 g_string_append_printf (string
, "%s(\"", func
);
536 for (cur
= html
; *cur
!= '\0'; cur
++)
538 const gchar
*replace
= NULL
;
539 gchar
*dup_replace
= NULL
;
540 gchar
*format
= NULL
;
542 /* Those are all well known keywords that needs replacement in
543 * html files. Please keep them in the same order than the adium
544 * spec. See http://trac.adium.im/wiki/CreatingMessageStyles */
545 if (theme_adium_match (&cur
, "%userIconPath%"))
547 replace
= avatar_filename
;
549 else if (theme_adium_match (&cur
, "%senderScreenName%"))
551 replace
= contact_id
;
553 else if (theme_adium_match (&cur
, "%sender%"))
557 else if (theme_adium_match (&cur
, "%senderColor%"))
559 /* A color derived from the user's name.
560 * FIXME: If a colon separated list of HTML colors is at
561 * Incoming/SenderColors.txt it will be used instead of
562 * the default colors.
565 /* Ensure we always use the same color when sending messages
571 else if (contact_id
!= NULL
)
573 guint hash
= g_str_hash (contact_id
);
574 replace
= colors
[hash
% G_N_ELEMENTS (colors
)];
577 else if (theme_adium_match (&cur
, "%senderStatusIcon%"))
579 /* FIXME: The path to the status icon of the sender
580 * (available, away, etc...)
583 else if (theme_adium_match (&cur
, "%messageDirection%"))
587 case PANGO_DIRECTION_LTR
:
588 case PANGO_DIRECTION_TTB_LTR
:
589 case PANGO_DIRECTION_WEAK_LTR
:
592 case PANGO_DIRECTION_RTL
:
593 case PANGO_DIRECTION_TTB_RTL
:
594 case PANGO_DIRECTION_WEAK_RTL
:
597 case PANGO_DIRECTION_NEUTRAL
:
602 else if (theme_adium_match (&cur
, "%senderDisplayName%"))
604 /* FIXME: The serverside (remotely set) name of the
605 * sender, such as an MSN display name.
607 * We don't have access to that yet so we use
608 * local alias instead.
612 else if (theme_adium_match (&cur
, "%senderPrefix%"))
614 /* FIXME: If we supported IRC user mode flags, this
615 * would be replaced with @ if the user is an op, + if
616 * the user has voice, etc. as per
617 * http://hg.adium.im/adium/rev/b586b027de42. But we
618 * don't, so for now we just strip it. */
620 else if (theme_adium_match_with_format (&cur
, "%textbackgroundcolor{",
623 /* FIXME: This keyword is used to represent the
624 * highlight background color. "X" is the opacity of the
625 * background, ranges from 0 to 1 and can be any decimal
629 else if (theme_adium_match (&cur
, "%message%"))
633 else if (theme_adium_match (&cur
, "%time%") ||
634 theme_adium_match_with_format (&cur
, "%time{", &format
))
636 const gchar
*strftime_format
;
638 strftime_format
= nsdate_to_strftime (self
->priv
->data
, format
);
640 dup_replace
= tpaw_time_to_string_local (timestamp
,
641 strftime_format
? strftime_format
:
642 TPAW_TIME_DATE_FORMAT_DISPLAY_SHORT
);
644 dup_replace
= tpaw_time_to_string_local (timestamp
,
645 strftime_format
? strftime_format
:
646 TPAW_TIME_FORMAT_DISPLAY_SHORT
);
648 replace
= dup_replace
;
650 else if (theme_adium_match (&cur
, "%shortTime%"))
652 dup_replace
= tpaw_time_to_string_local (timestamp
,
653 TPAW_TIME_FORMAT_DISPLAY_SHORT
);
654 replace
= dup_replace
;
656 else if (theme_adium_match (&cur
, "%service%"))
658 replace
= service_name
;
660 else if (theme_adium_match (&cur
, "%variant%"))
662 /* FIXME: The name of the active message style variant,
663 * with all spaces replaced with an underscore.
664 * A variant named "Alternating Messages - Blue Red"
665 * will become "Alternating_Messages_-_Blue_Red".
668 else if (theme_adium_match (&cur
, "%userIcons%"))
670 replace
= self
->priv
->show_avatars
? "showIcons" : "hideIcons";
672 else if (theme_adium_match (&cur
, "%messageClasses%"))
674 replace
= message_classes
;
676 else if (theme_adium_match (&cur
, "%status%"))
678 /* FIXME: A description of the status event. This is
679 * neither in the user's local language nor expected to
680 * be displayed; it may be useful to use a different div
681 * class to present different types of status messages.
682 * The following is a list of some of the more important
683 * status messages; your message style should be able to
684 * handle being shown a status message not in this list,
685 * as even at present the list is incomplete and is
686 * certain to become out of date in the future:
695 * contact_joined (group chats)
699 * encryption (all OTR messages use this status)
700 * purple (all IRC topic and join/part messages use this status)
701 * fileTransferStarted
702 * fileTransferCompleted
707 escape_and_append_len (string
, cur
, 1);
711 /* Here we have a replacement to make */
712 escape_and_append_len (string
, replace
, -1);
714 g_free (dup_replace
);
717 g_string_append (string
, "\")");
719 bytes
= g_resources_lookup_data ("/org/gnome/Empathy/Chat/empathy-chat.js",
720 G_RESOURCE_LOOKUP_FLAGS_NONE
,
725 js
= (const gchar
*) g_bytes_get_data (bytes
, NULL
);
726 g_string_prepend (string
, js
);
727 g_bytes_unref (bytes
);
730 script
= g_string_free (string
, FALSE
);
731 webkit_web_view_run_javascript (WEBKIT_WEB_VIEW (self
), script
, NULL
, NULL
, NULL
);
736 theme_adium_append_event_escaped (EmpathyThemeAdium
*self
,
737 const gchar
*escaped
,
738 PangoDirection direction
)
740 theme_adium_add_html (self
, "appendMessage",
741 self
->priv
->data
->status_html
, escaped
, NULL
, NULL
, NULL
,
742 NULL
, "event", tpaw_time_get_current (), FALSE
, FALSE
, direction
);
744 /* There is no last contact */
745 if (self
->priv
->last_contact
)
747 g_object_unref (self
->priv
->last_contact
);
748 self
->priv
->last_contact
= NULL
;
752 /* FIXME: check what this is for and port to WebKit2 */
754 theme_adium_remove_focus_marks (EmpathyThemeAdium
*self
,
755 WebKitDOMNodeList
*nodes
)
759 /* Remove focus and firstFocus class */
760 for (i
= 0; i
< webkit_dom_node_list_get_length (nodes
); i
++)
762 WebKitDOMNode
*node
= webkit_dom_node_list_item (nodes
, i
);
763 WebKitDOMElement
*element
= WEBKIT_DOM_ELEMENT (node
);
765 gchar
**classes
, **iter
;
766 GString
*new_class_name
;
767 gboolean first
= TRUE
;
772 class_name
= webkit_dom_element_get_class_name (element
);
773 classes
= g_strsplit (class_name
, " ", -1);
774 new_class_name
= g_string_sized_new (strlen (class_name
));
776 for (iter
= classes
; *iter
!= NULL
; iter
++)
778 if (tp_strdiff (*iter
, "focus") &&
779 tp_strdiff (*iter
, "firstFocus"))
782 g_string_append_c (new_class_name
, ' ');
784 g_string_append (new_class_name
, *iter
);
789 webkit_dom_element_set_class_name (element
, new_class_name
->str
);
792 g_strfreev (classes
);
793 g_string_free (new_class_name
, TRUE
);
798 theme_adium_remove_all_focus_marks (EmpathyThemeAdium
*self
)
801 /* FIXME: check what this is for and port to WebKit2 */
802 WebKitDOMDocument
*dom
;
803 WebKitDOMNodeList
*nodes
;
804 GError
*error
= NULL
;
806 if (!self
->priv
->has_unread_message
)
809 self
->priv
->has_unread_message
= FALSE
;
811 dom
= webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self
));
815 /* Get all nodes with focus class */
816 nodes
= webkit_dom_document_query_selector_all (dom
, ".focus", &error
);
820 DEBUG ("Error getting focus nodes: %s",
821 error
? error
->message
: "No error");
822 g_clear_error (&error
);
826 theme_adium_remove_focus_marks (self
, nodes
);
832 ADD_CONSECUTIVE_MSG_SCROLL
= 0,
833 ADD_CONSECUTIVE_MSG_NO_SCROLL
= 1,
835 ADD_MSG_NO_SCROLL
= 3
839 * theme_adium_add_message:
840 * @self: The #EmpathyThemeAdium used by the view.
841 * @msg: An #EmpathyMessage that is to be added to the view.
842 * @prev_contact: (out): The #EmpathyContact that sent the previous message.
843 * @prev_timestamp: (out): Timestamp of the previous message.
844 * @prev_is_backlog: (out): Whether the previous message was fetched
846 * @should_highlight: Whether the message should be highlighted. eg.,
847 * if it matches the user's username in multi-user chat.
848 * @js_funcs: An array of JavaScript function names
850 * Shows @msg in the chat view by adding to @self. Addition is defined
851 * by the JavaScript functions listed in @js_funcs. Common examples
852 * are appending new incoming messages or prepending old messages from
855 * @js_funcs should be an array with exactly 4 entries. The entries
856 * should be the names of JavaScript functions that take the raw HTML
857 * that is to be added to the view as an argument and take the following
858 * actions, in this order:
859 * - add a new consecutive message and scroll to it if needed,
860 * - add a new consecutive message and do not scroll,
861 * - add a new non-consecutive message and scroll to it if needed, and
862 * - add a new non-consecutive message and do not scroll
864 * A message is considered to be consecutive with the previous one if
865 * all the following conditions are met:
866 * - senders are the same contact,
867 * - last message was recieved recently,
868 * - last message and this message both are/aren't backlog, and
869 * - DisableCombineConsecutive is not set in theme's settings
872 theme_adium_add_message (EmpathyThemeAdium
*self
,
874 EmpathyContact
**prev_contact
,
875 gint64
*prev_timestamp
,
876 gboolean
*prev_is_backlog
,
877 gboolean should_highlight
,
878 const gchar
*js_funcs
[])
880 EmpathyContact
*sender
;
883 gchar
*body_escaped
, *name_escaped
;
885 const gchar
*contact_id
;
886 EmpathyAvatar
*avatar
;
887 const gchar
*avatar_filename
= NULL
;
889 const gchar
*html
= NULL
;
891 const gchar
*service_name
;
892 GString
*message_classes
= NULL
;
894 gboolean consecutive
;
896 PangoDirection direction
;
899 /* Get information */
900 sender
= empathy_message_get_sender (msg
);
901 account
= empathy_contact_get_account (sender
);
902 service_name
= tpaw_protocol_name_to_display_name
903 (tp_account_get_protocol_name (account
));
904 if (service_name
== NULL
)
905 service_name
= tp_account_get_protocol_name (account
);
906 timestamp
= empathy_message_get_timestamp (msg
);
907 body_escaped
= theme_adium_parse_body (self
,
908 empathy_message_get_body (msg
),
909 empathy_message_get_token (msg
));
910 name
= empathy_contact_get_logged_alias (sender
);
911 contact_id
= empathy_contact_get_id (sender
);
912 action
= (empathy_message_get_tptype (msg
) ==
913 TP_CHANNEL_TEXT_MESSAGE_TYPE_ACTION
);
915 name_escaped
= g_markup_escape_text (name
, -1);
917 /* If this is a /me probably */
922 if (self
->priv
->data
->version
>= 4 || !self
->priv
->data
->custom_template
)
924 str
= g_strdup_printf ("<span class='actionMessageUserName'>%s</span>"
925 "<span class='actionMessageBody'>%s</span>",
926 name_escaped
, body_escaped
);
930 str
= g_strdup_printf ("*%s*", body_escaped
);
933 g_free (body_escaped
);
937 /* Get the avatar filename, or a fallback */
938 avatar
= empathy_contact_get_avatar (sender
);
940 avatar_filename
= avatar
->filename
;
942 if (!avatar_filename
)
944 if (empathy_contact_is_user (sender
))
945 avatar_filename
= self
->priv
->data
->default_outgoing_avatar_filename
;
947 avatar_filename
= self
->priv
->data
->default_incoming_avatar_filename
;
949 if (!avatar_filename
)
951 if (!self
->priv
->data
->default_avatar_filename
)
952 self
->priv
->data
->default_avatar_filename
=
953 tpaw_filename_from_icon_name (TPAW_IMAGE_AVATAR_DEFAULT
,
954 GTK_ICON_SIZE_DIALOG
);
956 avatar_filename
= self
->priv
->data
->default_avatar_filename
;
960 is_backlog
= empathy_message_is_backlog (msg
);
961 consecutive
= empathy_contact_equal (*prev_contact
, sender
) &&
962 (ABS (timestamp
- *prev_timestamp
) < MESSAGE_JOIN_PERIOD
) &&
963 (is_backlog
== *prev_is_backlog
) &&
964 !tp_asv_get_boolean (self
->priv
->data
->info
,
965 "DisableCombineConsecutive", NULL
);
967 /* Define message classes */
968 message_classes
= g_string_new ("message");
969 if (!self
->priv
->has_focus
&& !is_backlog
)
971 if (!self
->priv
->has_unread_message
)
973 g_string_append (message_classes
, " firstFocus");
974 self
->priv
->has_unread_message
= TRUE
;
976 g_string_append (message_classes
, " focus");
980 g_string_append (message_classes
, " history");
983 g_string_append (message_classes
, " consecutive");
985 if (empathy_contact_is_user (sender
))
986 g_string_append (message_classes
, " outgoing");
988 g_string_append (message_classes
, " incoming");
990 if (should_highlight
)
991 g_string_append (message_classes
, " mention");
993 if (empathy_message_get_tptype (msg
) ==
994 TP_CHANNEL_TEXT_MESSAGE_TYPE_AUTO_REPLY
)
995 g_string_append (message_classes
, " autoreply");
998 g_string_append (message_classes
, " action");
1000 /* FIXME: other classes:
1001 * status - the message is a status change
1002 * event - the message is a notification of something happening
1003 * (for example, encryption being turned on)
1004 * %status% - See %status% in theme_adium_add_html ()
1007 /* This is slightly a hack, but it's the only way to add
1008 * arbitrary data to messages in the HTML. We add another
1009 * class called "x-empathy-message-id-*" to the message. This
1010 * way, we can remove the unread marker for this specific
1012 tp_msg
= empathy_message_get_tp_message (msg
);
1018 id
= tp_message_get_pending_message_id (tp_msg
, &valid
);
1020 g_string_append_printf (message_classes
,
1021 " x-empathy-message-id-%u", id
);
1024 /* Define javascript function to use */
1026 func
= self
->priv
->allow_scrolling
? js_funcs
[ADD_CONSECUTIVE_MSG_SCROLL
] :
1027 js_funcs
[ADD_CONSECUTIVE_MSG_NO_SCROLL
];
1029 func
= self
->priv
->allow_scrolling
? js_funcs
[ADD_MSG_SCROLL
] :
1030 js_funcs
[ADD_MSG_NO_SCROLL
];
1032 if (empathy_contact_is_user (sender
))
1037 html
= consecutive
? self
->priv
->data
->out_nextcontext_html
:
1038 self
->priv
->data
->out_context_html
;
1041 html
= consecutive
? self
->priv
->data
->out_nextcontent_html
:
1042 self
->priv
->data
->out_content_html
;
1044 /* remove all the unread marks when we are sending a message */
1045 theme_adium_remove_all_focus_marks (self
);
1052 html
= consecutive
? self
->priv
->data
->in_nextcontext_html
:
1053 self
->priv
->data
->in_context_html
;
1056 html
= consecutive
? self
->priv
->data
->in_nextcontent_html
:
1057 self
->priv
->data
->in_content_html
;
1060 direction
= pango_find_base_dir (empathy_message_get_body (msg
), -1);
1062 theme_adium_add_html (self
, func
, html
, body_escaped
,
1063 avatar_filename
, name_escaped
, contact_id
,
1064 service_name
, message_classes
->str
,
1065 timestamp
, is_backlog
, empathy_contact_is_user (sender
), direction
);
1067 /* Keep the sender of the last displayed message */
1069 g_object_unref (*prev_contact
);
1071 *prev_contact
= g_object_ref (sender
);
1072 *prev_timestamp
= timestamp
;
1073 *prev_is_backlog
= is_backlog
;
1075 g_free (body_escaped
);
1076 g_free (name_escaped
);
1077 g_string_free (message_classes
, TRUE
);
1081 empathy_theme_adium_append_message (EmpathyThemeAdium
*self
,
1082 EmpathyMessage
*msg
,
1083 gboolean should_highlight
)
1085 const gchar
*js_funcs
[] = { "appendNextMessage",
1086 "appendNextMessageNoScroll",
1088 "appendMessageNoScroll" };
1090 if (self
->priv
->pages_loading
!= 0)
1092 queue_item (&self
->priv
->message_queue
, QUEUED_MESSAGE
, msg
, NULL
,
1093 should_highlight
, FALSE
);
1097 theme_adium_add_message (self
, msg
, &self
->priv
->last_contact
,
1098 &self
->priv
->last_timestamp
, &self
->priv
->last_is_backlog
,
1099 should_highlight
, js_funcs
);
1103 empathy_theme_adium_append_event (EmpathyThemeAdium
*self
,
1107 PangoDirection direction
;
1109 if (self
->priv
->pages_loading
!= 0)
1111 queue_item (&self
->priv
->message_queue
, QUEUED_EVENT
, NULL
, str
, FALSE
, FALSE
);
1115 direction
= pango_find_base_dir (str
, -1);
1116 str_escaped
= g_markup_escape_text (str
, -1);
1117 theme_adium_append_event_escaped (self
, str_escaped
, direction
);
1118 g_free (str_escaped
);
1122 empathy_theme_adium_append_event_markup (EmpathyThemeAdium
*self
,
1123 const gchar
*markup_text
,
1124 const gchar
*fallback_text
)
1126 PangoDirection direction
;
1128 direction
= pango_find_base_dir (fallback_text
, -1);
1129 theme_adium_append_event_escaped (self
, markup_text
, direction
);
1133 empathy_theme_adium_prepend_message (EmpathyThemeAdium
*self
,
1134 EmpathyMessage
*msg
,
1135 gboolean should_highlight
)
1137 const gchar
*js_funcs
[] = { "prependPrev",
1142 if (self
->priv
->pages_loading
!= 0)
1144 queue_item (&self
->priv
->message_queue
, QUEUED_MESSAGE
, msg
, NULL
,
1145 should_highlight
, TRUE
);
1149 theme_adium_add_message (self
, msg
, &self
->priv
->first_contact
,
1150 &self
->priv
->first_timestamp
, &self
->priv
->first_is_backlog
,
1151 should_highlight
, js_funcs
);
1155 empathy_theme_adium_edit_message (EmpathyThemeAdium
*self
,
1156 EmpathyMessage
*message
)
1159 /* FIXME: this needs to be ported to WebKit2, but I have no idea what this is for */
1160 WebKitDOMDocument
*doc
;
1161 WebKitDOMElement
*span
;
1162 gchar
*id
, *parsed_body
;
1163 gchar
*tooltip
, *timestamp
;
1164 GtkIconInfo
*icon_info
;
1165 GError
*error
= NULL
;
1167 if (self
->priv
->pages_loading
!= 0)
1169 queue_item (&self
->priv
->message_queue
, QUEUED_EDIT
, message
, NULL
, FALSE
, FALSE
);
1173 id
= g_strdup_printf ("message-token-%s",
1174 empathy_message_get_supersedes (message
));
1175 /* we don't pass a token here, because doing so will return another
1176 * <span> element, and we don't want nested <span> elements */
1177 parsed_body
= theme_adium_parse_body (self
,
1178 empathy_message_get_body (message
), NULL
);
1180 /* find the element */
1181 doc
= webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self
));
1182 span
= webkit_dom_document_get_element_by_id (doc
, id
);
1186 DEBUG ("Failed to find id '%s'", id
);
1190 if (!WEBKIT_DOM_IS_HTML_ELEMENT (span
))
1192 DEBUG ("Not a HTML element");
1196 /* update the HTML */
1197 webkit_dom_html_element_set_inner_html (WEBKIT_DOM_HTML_ELEMENT (span
),
1198 parsed_body
, &error
);
1202 DEBUG ("Error setting new inner-HTML: %s", error
->message
);
1203 g_error_free (error
);
1208 timestamp
= tpaw_time_to_string_local (
1209 empathy_message_get_timestamp (message
),
1211 tooltip
= g_strdup_printf (_("Message edited at %s"), timestamp
);
1213 webkit_dom_html_element_set_title (WEBKIT_DOM_HTML_ELEMENT (span
),
1219 /* mark this message as edited */
1220 icon_info
= gtk_icon_theme_lookup_icon (gtk_icon_theme_get_default (),
1221 EMPATHY_IMAGE_EDIT_MESSAGE
, 16, 0);
1223 if (icon_info
!= NULL
)
1225 /* set the icon as a background image using CSS
1226 * FIXME: the icon won't update in response to theme changes */
1227 gchar
*style
= g_strdup_printf (
1228 "background-image:url('%s');"
1229 "background-repeat:no-repeat;"
1230 "background-position:left center;"
1231 "padding-left:19px;", /* 16px icon + 3px padding */
1232 gtk_icon_info_get_filename (icon_info
));
1234 webkit_dom_element_set_attribute (span
, "style", style
, &error
);
1238 DEBUG ("Error setting element style: %s",
1240 g_clear_error (&error
);
1245 g_object_unref (icon_info
);
1251 DEBUG ("Could not find message to edit with: %s",
1252 empathy_message_get_body (message
));
1256 g_free (parsed_body
);
1261 empathy_theme_adium_scroll (EmpathyThemeAdium
*self
,
1262 gboolean allow_scrolling
)
1264 self
->priv
->allow_scrolling
= allow_scrolling
;
1266 if (allow_scrolling
)
1267 empathy_theme_adium_scroll_down (self
);
1271 empathy_theme_adium_scroll_down (EmpathyThemeAdium
*self
)
1273 webkit_web_view_run_javascript (WEBKIT_WEB_VIEW (self
), "alignChat(true);", NULL
, NULL
, NULL
);
1277 can_copy_cb (WebKitWebView
*web_view
,
1278 GAsyncResult
*result
,
1281 g_task_return_boolean (task
,
1282 webkit_web_view_can_execute_editing_command_finish (web_view
, result
, NULL
));
1283 g_object_unref (task
);
1287 empathy_theme_adium_can_copy (EmpathyThemeAdium
*self
,
1288 GCancellable
* cancellable
,
1289 GAsyncReadyCallback callback
,
1294 task
= g_task_new (self
, cancellable
, callback
, user_data
);
1295 webkit_web_view_can_execute_editing_command (WEBKIT_WEB_VIEW (self
), WEBKIT_EDITING_COMMAND_COPY
,
1296 cancellable
, (GAsyncReadyCallback
)can_copy_cb
, task
);
1300 empathy_theme_adium_can_copy_finish (EmpathyThemeAdium
*self
,
1301 GAsyncResult
* result
,
1304 if (!g_task_is_valid (result
, self
))
1307 return g_task_propagate_boolean (G_TASK (result
), error
);
1311 empathy_theme_adium_clear (EmpathyThemeAdium
*self
)
1313 webkit_web_view_run_javascript (WEBKIT_WEB_VIEW (self
), "clearPage()", NULL
, NULL
, NULL
);
1314 empathy_theme_adium_scroll_down (self
);
1316 /* Clear last contact to avoid trying to add a 'joined'
1317 * message when we don't have an insertion point. */
1318 if (self
->priv
->last_contact
)
1320 g_object_unref (self
->priv
->last_contact
);
1321 self
->priv
->last_contact
= NULL
;
1326 empathy_theme_adium_find_previous (EmpathyThemeAdium
*self
)
1328 webkit_find_controller_search_previous (webkit_web_view_get_find_controller (WEBKIT_WEB_VIEW (self
)));
1332 empathy_theme_adium_find_next (EmpathyThemeAdium
*self
)
1334 webkit_find_controller_search_next (webkit_web_view_get_find_controller (WEBKIT_WEB_VIEW (self
)));
1338 empathy_theme_adium_find_abilities (EmpathyThemeAdium
*self
,
1339 const gchar
*search_criteria
,
1340 gboolean match_case
,
1341 gboolean
*can_do_previous
,
1342 gboolean
*can_do_next
)
1344 /* FIXME: Does webkit provide an API for that? We have wrap=true in
1345 * find_next and find_previous to work around this problem. */
1346 if (can_do_previous
)
1347 *can_do_previous
= TRUE
;
1349 *can_do_next
= TRUE
;
1353 empathy_theme_adium_search (EmpathyThemeAdium
*self
,
1355 gboolean match_case
)
1357 WebKitFindController
*find_controller
;
1358 WebKitFindOptions options
= WEBKIT_FIND_OPTIONS_NONE
;
1360 find_controller
= webkit_web_view_get_find_controller (WEBKIT_WEB_VIEW (self
));
1361 if (!text
|| !*text
)
1363 webkit_find_controller_search_finish (find_controller
);
1368 options
|= WEBKIT_FIND_OPTIONS_CASE_INSENSITIVE
;
1370 webkit_find_controller_search (find_controller
, text
, options
, G_MAXUINT
);
1374 empathy_theme_adium_copy_clipboard (EmpathyThemeAdium
*self
)
1376 webkit_web_view_execute_editing_command (WEBKIT_WEB_VIEW (self
),
1377 WEBKIT_EDITING_COMMAND_COPY
);
1381 theme_adium_remove_mark_from_message (EmpathyThemeAdium
*self
,
1385 /* FIXME: check what this is for and port to WebKit2 */
1386 WebKitDOMDocument
*dom
;
1387 WebKitDOMNodeList
*nodes
;
1389 GError
*error
= NULL
;
1391 dom
= webkit_web_view_get_dom_document (WEBKIT_WEB_VIEW (self
));
1395 class = g_strdup_printf (".x-empathy-message-id-%u", id
);
1397 /* Get all nodes with focus class */
1398 nodes
= webkit_dom_document_query_selector_all (dom
, class, &error
);
1403 DEBUG ("Error getting focus nodes: %s",
1404 error
? error
->message
: "No error");
1405 g_clear_error (&error
);
1409 theme_adium_remove_focus_marks (self
, nodes
);
1414 theme_adium_remove_acked_message_unread_mark_foreach (gpointer data
,
1417 EmpathyThemeAdium
*self
= user_data
;
1418 guint32 id
= GPOINTER_TO_UINT (data
);
1420 theme_adium_remove_mark_from_message (self
, id
);
1424 empathy_theme_adium_focus_toggled (EmpathyThemeAdium
*self
,
1427 self
->priv
->has_focus
= has_focus
;
1428 if (!self
->priv
->has_focus
)
1430 /* We've lost focus, so let's make sure all the acked
1431 * messages have lost their unread marker. */
1432 g_queue_foreach (&self
->priv
->acked_messages
,
1433 theme_adium_remove_acked_message_unread_mark_foreach
, self
);
1434 g_queue_clear (&self
->priv
->acked_messages
);
1436 self
->priv
->has_unread_message
= FALSE
;
1441 empathy_theme_adium_message_acknowledged (EmpathyThemeAdium
*self
,
1442 EmpathyMessage
*message
)
1448 tp_msg
= empathy_message_get_tp_message (message
);
1453 id
= tp_message_get_pending_message_id (tp_msg
, &valid
);
1456 g_warning ("Acknoledged message doesn't have a pending ID");
1460 /* We only want to actually remove the unread marker if the
1461 * view doesn't have focus. If we did it all the time we would
1462 * never see the unread markers, ever! So, we'll queue these
1463 * up, and when we lose focus, we'll remove the markers. */
1464 if (self
->priv
->has_focus
)
1466 g_queue_push_tail (&self
->priv
->acked_messages
,
1467 GUINT_TO_POINTER (id
));
1471 theme_adium_remove_mark_from_message (self
, id
);
1475 theme_adium_context_menu_cb (EmpathyThemeAdium
*self
,
1476 WebKitContextMenu
*context_menu
,
1478 WebKitHitTestResult
*hit_test_result
,
1481 EmpathyWebKitMenuFlags flags
= EMPATHY_WEBKIT_MENU_CLEAR
;
1483 if (g_settings_get_boolean (self
->priv
->gsettings_chat
,
1484 EMPATHY_PREFS_CHAT_WEBKIT_DEVELOPER_TOOLS
))
1485 flags
|= EMPATHY_WEBKIT_MENU_INSPECT
;
1487 empathy_webkit_populate_context_menu (WEBKIT_WEB_VIEW (self
), context_menu
, hit_test_result
, flags
);
1492 empathy_theme_adium_set_show_avatars (EmpathyThemeAdium
*self
,
1493 gboolean show_avatars
)
1495 self
->priv
->show_avatars
= show_avatars
;
1499 theme_adium_load_changed_cb (WebKitWebView
*view
,
1500 WebKitLoadEvent event
,
1503 EmpathyThemeAdium
*self
= EMPATHY_THEME_ADIUM (view
);
1506 if (event
!= WEBKIT_LOAD_FINISHED
)
1509 DEBUG ("Page loaded");
1510 self
->priv
->pages_loading
--;
1512 if (self
->priv
->pages_loading
!= 0)
1515 /* Display queued messages */
1516 for (l
= self
->priv
->message_queue
.head
; l
!= NULL
; l
= l
->next
)
1518 QueuedItem
*item
= l
->data
;
1522 case QUEUED_MESSAGE
:
1523 empathy_theme_adium_append_message (self
, item
->msg
,
1524 item
->should_highlight
);
1528 empathy_theme_adium_edit_message (self
, item
->msg
);
1532 empathy_theme_adium_append_event (self
, item
->str
);
1536 free_queued_item (item
);
1539 g_queue_clear (&self
->priv
->message_queue
);
1543 theme_adium_finalize (GObject
*object
)
1545 EmpathyThemeAdium
*self
= EMPATHY_THEME_ADIUM (object
);
1547 empathy_adium_data_unref (self
->priv
->data
);
1549 g_object_unref (self
->priv
->gsettings_chat
);
1550 g_object_unref (self
->priv
->gsettings_desktop
);
1552 g_free (self
->priv
->variant
);
1554 G_OBJECT_CLASS (empathy_theme_adium_parent_class
)->finalize (object
);
1558 theme_adium_dispose (GObject
*object
)
1560 EmpathyThemeAdium
*self
= EMPATHY_THEME_ADIUM (object
);
1562 if (self
->priv
->smiley_manager
)
1564 g_object_unref (self
->priv
->smiley_manager
);
1565 self
->priv
->smiley_manager
= NULL
;
1568 g_clear_object (&self
->priv
->first_contact
);
1570 if (self
->priv
->last_contact
)
1572 g_object_unref (self
->priv
->last_contact
);
1573 self
->priv
->last_contact
= NULL
;
1576 if (self
->priv
->inspector_window
)
1578 gtk_widget_destroy (self
->priv
->inspector_window
);
1579 self
->priv
->inspector_window
= NULL
;
1582 if (self
->priv
->acked_messages
.length
> 0)
1584 g_queue_clear (&self
->priv
->acked_messages
);
1587 G_OBJECT_CLASS (empathy_theme_adium_parent_class
)->dispose (object
);
1591 theme_adium_constructed (GObject
*object
)
1593 EmpathyThemeAdium
*self
= EMPATHY_THEME_ADIUM (object
);
1594 const gchar
*font_family
= NULL
;
1596 WebKitWebView
*webkit_view
= WEBKIT_WEB_VIEW (object
);
1598 G_OBJECT_CLASS (empathy_theme_adium_parent_class
)->constructed (object
);
1600 /* Set default settings */
1601 font_family
= tp_asv_get_string (self
->priv
->data
->info
, "DefaultFontFamily");
1602 font_size
= tp_asv_get_int32 (self
->priv
->data
->info
, "DefaultFontSize", NULL
);
1604 if (font_family
&& font_size
)
1606 g_object_set (webkit_web_view_get_settings (webkit_view
),
1607 "default-font-family", font_family
,
1608 "default-font-size", font_size
,
1613 empathy_webkit_bind_font_setting (webkit_view
,
1614 self
->priv
->gsettings_desktop
,
1615 EMPATHY_PREFS_DESKTOP_INTERFACE_DOCUMENT_FONT_NAME
);
1618 g_object_set (webkit_web_view_get_settings (webkit_view
),
1619 "default-charset", "utf8",
1623 theme_adium_load_template (EMPATHY_THEME_ADIUM (object
));
1625 self
->priv
->in_construction
= FALSE
;
1629 theme_adium_get_property (GObject
*object
,
1634 EmpathyThemeAdium
*self
= EMPATHY_THEME_ADIUM (object
);
1638 case PROP_ADIUM_DATA
:
1639 g_value_set_boxed (value
, self
->priv
->data
);
1642 g_value_set_string (value
, self
->priv
->variant
);
1645 G_OBJECT_WARN_INVALID_PROPERTY_ID (object
, param_id
, pspec
);
1651 theme_adium_set_property (GObject
*object
,
1653 const GValue
*value
,
1656 EmpathyThemeAdium
*self
= EMPATHY_THEME_ADIUM (object
);
1660 case PROP_ADIUM_DATA
:
1661 g_assert (self
->priv
->data
== NULL
);
1662 self
->priv
->data
= g_value_dup_boxed (value
);
1665 empathy_theme_adium_set_variant (self
, g_value_get_string (value
));
1668 G_OBJECT_WARN_INVALID_PROPERTY_ID (object
, param_id
, pspec
);
1674 empathy_theme_adium_class_init (EmpathyThemeAdiumClass
*klass
)
1676 GObjectClass
*object_class
= G_OBJECT_CLASS (klass
);
1678 object_class
->finalize
= theme_adium_finalize
;
1679 object_class
->dispose
= theme_adium_dispose
;
1680 object_class
->constructed
= theme_adium_constructed
;
1681 object_class
->get_property
= theme_adium_get_property
;
1682 object_class
->set_property
= theme_adium_set_property
;
1684 g_object_class_install_property (object_class
, PROP_ADIUM_DATA
,
1685 g_param_spec_boxed ("adium-data",
1687 "Data for the adium theme",
1688 EMPATHY_TYPE_ADIUM_DATA
,
1689 G_PARAM_CONSTRUCT_ONLY
|
1691 G_PARAM_STATIC_STRINGS
));
1693 g_object_class_install_property (object_class
, PROP_VARIANT
,
1694 g_param_spec_string ("variant",
1695 "The theme variant",
1696 "Variant name for the theme",
1700 G_PARAM_STATIC_STRINGS
));
1702 g_type_class_add_private (object_class
, sizeof (EmpathyThemeAdiumPriv
));
1706 empathy_theme_adium_init (EmpathyThemeAdium
*self
)
1708 self
->priv
= G_TYPE_INSTANCE_GET_PRIVATE (self
,
1709 EMPATHY_TYPE_THEME_ADIUM
, EmpathyThemeAdiumPriv
);
1711 self
->priv
->in_construction
= TRUE
;
1712 g_queue_init (&self
->priv
->message_queue
);
1713 self
->priv
->allow_scrolling
= TRUE
;
1714 self
->priv
->smiley_manager
= empathy_smiley_manager_dup_singleton ();
1716 /* Show avatars by default. */
1717 self
->priv
->show_avatars
= TRUE
;
1718 g_signal_connect (self
, "load-changed",
1719 G_CALLBACK (theme_adium_load_changed_cb
), NULL
);
1720 g_signal_connect (self
, "decide-policy",
1721 G_CALLBACK (theme_adium_policy_decision_requested_cb
), NULL
);
1722 g_signal_connect (self
, "context-menu",
1723 G_CALLBACK (theme_adium_context_menu_cb
), NULL
);
1725 self
->priv
->gsettings_chat
= g_settings_new (EMPATHY_PREFS_CHAT_SCHEMA
);
1726 self
->priv
->gsettings_desktop
= g_settings_new (
1727 EMPATHY_PREFS_DESKTOP_INTERFACE_SCHEMA
);
1731 empathy_theme_adium_new (EmpathyAdiumData
*data
,
1732 const gchar
*variant
)
1734 g_return_val_if_fail (data
!= NULL
, NULL
);
1736 return g_object_new (EMPATHY_TYPE_THEME_ADIUM
,
1737 "web-context", empathy_webkit_get_web_context (),
1738 "settings", empathy_webkit_get_web_settings (),
1745 empathy_theme_adium_set_variant (EmpathyThemeAdium
*self
,
1746 const gchar
*variant
)
1748 gchar
*variant_path
;
1751 if (!tp_strdiff (self
->priv
->variant
, variant
))
1754 g_free (self
->priv
->variant
);
1755 self
->priv
->variant
= g_strdup (variant
);
1757 if (self
->priv
->in_construction
)
1760 DEBUG ("Update view with variant: '%s'", variant
);
1761 variant_path
= adium_info_dup_path_for_variant (self
->priv
->data
->info
,
1762 self
->priv
->variant
);
1763 script
= g_strdup_printf ("setStylesheet(\"mainStyle\",\"%s\");",
1766 webkit_web_view_run_javascript (WEBKIT_WEB_VIEW (self
), script
, NULL
, NULL
, NULL
);
1768 g_free (variant_path
);
1771 g_object_notify (G_OBJECT (self
), "variant");
1775 empathy_theme_adium_show_inspector (EmpathyThemeAdium
*self
)
1777 webkit_web_inspector_show (webkit_web_view_get_inspector (WEBKIT_WEB_VIEW (self
)));
1781 empathy_adium_path_is_valid (const gchar
*path
)
1791 /* The directory has to be *.AdiumMessageStyle per the Adium spec */
1792 tmp
= g_strsplit (path
, "/", 0);
1796 dir
= tmp
[g_strv_length (tmp
) - 1];
1798 if (!g_str_has_suffix (dir
, ".AdiumMessageStyle"))
1806 /* The theme is not valid if there is no Info.plist */
1807 file
= g_build_filename (path
, "Contents", "Info.plist",
1809 ret
= g_file_test (file
, G_FILE_TEST_EXISTS
);
1815 /* We ship a default Template.html as fallback if there is any problem
1816 * with the one inside the theme. The only other required file is
1817 * Content.html OR Incoming/Content.html*/
1818 file
= g_build_filename (path
, "Contents", "Resources", "Content.html",
1820 ret
= g_file_test (file
, G_FILE_TEST_EXISTS
);
1825 file
= g_build_filename (path
, "Contents", "Resources", "Incoming",
1826 "Content.html", NULL
);
1827 ret
= g_file_test (file
, G_FILE_TEST_EXISTS
);
1835 empathy_adium_info_new (const gchar
*path
)
1839 GHashTable
*info
= NULL
;
1841 g_return_val_if_fail (empathy_adium_path_is_valid (path
), NULL
);
1843 file
= g_build_filename (path
, "Contents", "Info.plist", NULL
);
1844 value
= empathy_plist_parse_from_file (file
);
1850 info
= g_value_dup_boxed (value
);
1851 tp_g_value_slice_free (value
);
1853 /* Insert the theme's path into the hash table,
1854 * keys have to be dupped */
1855 tp_asv_set_string (info
, g_strdup ("path"), path
);
1861 adium_info_get_version (GHashTable
*info
)
1863 return tp_asv_get_int32 (info
, "MessageViewVersion", NULL
);
1866 static const gchar
*
1867 adium_info_get_no_variant_name (GHashTable
*info
)
1869 const gchar
*name
= tp_asv_get_string (info
, "DisplayNameForNoVariant");
1870 return name
? name
: _("Normal");
1874 adium_info_dup_path_for_variant (GHashTable
*info
,
1875 const gchar
*variant
)
1877 guint version
= adium_info_get_version (info
);
1878 const gchar
*no_variant
= adium_info_get_no_variant_name (info
);
1879 GPtrArray
*variants
;
1882 if (version
<= 2 && !tp_strdiff (variant
, no_variant
))
1883 return g_strdup ("main.css");
1885 variants
= empathy_adium_info_get_available_variants (info
);
1886 if (variants
->len
== 0)
1887 return g_strdup ("main.css");
1889 /* Verify the variant exists, fallback to the first one */
1890 for (i
= 0; i
< variants
->len
; i
++)
1892 if (!tp_strdiff (variant
, g_ptr_array_index (variants
, i
)))
1896 if (i
== variants
->len
)
1898 DEBUG ("Variant %s does not exist", variant
);
1899 variant
= g_ptr_array_index (variants
, 0);
1902 return g_strdup_printf ("Variants/%s.css", variant
);
1907 empathy_adium_info_get_default_variant (GHashTable
*info
)
1909 if (adium_info_get_version (info
) <= 2)
1910 return adium_info_get_no_variant_name (info
);
1912 return tp_asv_get_string (info
, "DefaultVariant");
1916 empathy_adium_info_get_available_variants (GHashTable
*info
)
1918 GPtrArray
*variants
;
1923 variants
= tp_asv_get_boxed (info
, "AvailableVariants", G_TYPE_PTR_ARRAY
);
1924 if (variants
!= NULL
)
1927 variants
= g_ptr_array_new_with_free_func (g_free
);
1928 tp_asv_take_boxed (info
, g_strdup ("AvailableVariants"),
1929 G_TYPE_PTR_ARRAY
, variants
);
1931 path
= tp_asv_get_string (info
, "path");
1932 dirpath
= g_build_filename (path
, "Contents", "Resources", "Variants", NULL
);
1933 dir
= g_dir_open (dirpath
, 0, NULL
);
1938 for (name
= g_dir_read_name (dir
);
1940 name
= g_dir_read_name (dir
))
1942 gchar
*display_name
;
1944 if (!g_str_has_suffix (name
, ".css"))
1947 display_name
= g_strdup (name
);
1948 strstr (display_name
, ".css")[0] = '\0';
1949 g_ptr_array_add (variants
, display_name
);
1956 if (adium_info_get_version (info
) <= 2)
1957 g_ptr_array_add (variants
,
1958 g_strdup (adium_info_get_no_variant_name (info
)));
1964 empathy_adium_data_get_type (void)
1966 static GType type_id
= 0;
1970 type_id
= g_boxed_type_register_static ("EmpathyAdiumData",
1971 (GBoxedCopyFunc
) empathy_adium_data_ref
,
1972 (GBoxedFreeFunc
) empathy_adium_data_unref
);
1979 empathy_adium_data_new_with_info (const gchar
*path
,
1982 EmpathyAdiumData
*data
;
1983 gchar
*template_html
= NULL
;
1984 gchar
*footer_html
= NULL
;
1987 g_return_val_if_fail (empathy_adium_path_is_valid (path
), NULL
);
1989 data
= g_slice_new0 (EmpathyAdiumData
);
1990 data
->ref_count
= 1;
1991 data
->path
= g_strdup (path
);
1992 data
->basedir
= g_strconcat (path
, G_DIR_SEPARATOR_S
"Contents"
1993 G_DIR_SEPARATOR_S
"Resources" G_DIR_SEPARATOR_S
, NULL
);
1994 data
->info
= g_hash_table_ref (info
);
1995 data
->version
= adium_info_get_version (info
);
1996 data
->strings_to_free
= g_ptr_array_new_with_free_func (g_free
);
1997 data
->date_format_cache
= g_hash_table_new_full (g_str_hash
,
1998 g_str_equal
, g_free
, g_free
);
2000 DEBUG ("Loading theme at %s", path
);
2002 #define LOAD(path, var) \
2003 tmp = g_build_filename (data->basedir, path, NULL); \
2004 g_file_get_contents (tmp, &var, NULL, NULL); \
2007 #define LOAD_CONST(path, var) \
2010 LOAD (path, content); \
2011 if (content != NULL) { \
2012 g_ptr_array_add (data->strings_to_free, content); \
2017 /* Load html files */
2018 LOAD_CONST ("Content.html", data
->content_html
);
2019 LOAD_CONST ("Incoming/Content.html", data
->in_content_html
);
2020 LOAD_CONST ("Incoming/NextContent.html", data
->in_nextcontent_html
);
2021 LOAD_CONST ("Incoming/Context.html", data
->in_context_html
);
2022 LOAD_CONST ("Incoming/NextContext.html", data
->in_nextcontext_html
);
2023 LOAD_CONST ("Outgoing/Content.html", data
->out_content_html
);
2024 LOAD_CONST ("Outgoing/NextContent.html", data
->out_nextcontent_html
);
2025 LOAD_CONST ("Outgoing/Context.html", data
->out_context_html
);
2026 LOAD_CONST ("Outgoing/NextContext.html", data
->out_nextcontext_html
);
2027 LOAD_CONST ("Status.html", data
->status_html
);
2028 LOAD ("Template.html", template_html
);
2029 LOAD ("Footer.html", footer_html
);
2034 /* HTML fallbacks: If we have at least content OR in_content, then
2035 * everything else gets a fallback */
2037 #define FALLBACK(html, fallback) \
2038 if (html == NULL) { \
2042 /* in_nextcontent -> in_content -> content */
2043 FALLBACK (data
->in_content_html
, data
->content_html
);
2044 FALLBACK (data
->in_nextcontent_html
, data
->in_content_html
);
2046 /* context -> content */
2047 FALLBACK (data
->in_context_html
, data
->in_content_html
);
2048 FALLBACK (data
->in_nextcontext_html
, data
->in_nextcontent_html
);
2049 FALLBACK (data
->out_context_html
, data
->out_content_html
);
2050 FALLBACK (data
->out_nextcontext_html
, data
->out_nextcontent_html
);
2053 FALLBACK (data
->out_content_html
, data
->in_content_html
);
2054 FALLBACK (data
->out_nextcontent_html
, data
->in_nextcontent_html
);
2055 FALLBACK (data
->out_context_html
, data
->in_context_html
);
2056 FALLBACK (data
->out_nextcontext_html
, data
->in_nextcontext_html
);
2058 /* status -> in_content */
2059 FALLBACK (data
->status_html
, data
->in_content_html
);
2063 /* template -> empathy's template */
2064 data
->custom_template
= (template_html
!= NULL
);
2065 if (template_html
== NULL
)
2067 GError
*error
= NULL
;
2069 tmp
= empathy_file_lookup ("Template.html", "data");
2071 if (!g_file_get_contents (tmp
, &template_html
, NULL
, &error
)) {
2072 g_warning ("couldn't load Empathy's default theme "
2073 "template: %s", error
->message
);
2074 g_return_val_if_reached (data
);
2080 /* Default avatar */
2081 tmp
= g_build_filename (data
->basedir
, "Incoming", "buddy_icon.png", NULL
);
2082 if (g_file_test (tmp
, G_FILE_TEST_EXISTS
| G_FILE_TEST_IS_REGULAR
))
2084 data
->default_incoming_avatar_filename
= tmp
;
2091 tmp
= g_build_filename (data
->basedir
, "Outgoing", "buddy_icon.png", NULL
);
2092 if (g_file_test (tmp
, G_FILE_TEST_EXISTS
| G_FILE_TEST_IS_REGULAR
))
2094 data
->default_outgoing_avatar_filename
= tmp
;
2101 /* Old custom templates had only 4 parameters.
2102 * New templates have 5 parameters */
2103 if (data
->version
<= 2 && data
->custom_template
)
2105 tmp
= string_with_format (template_html
,
2107 "%@", /* Leave variant unset */
2108 "", /* The header */
2109 footer_html
? footer_html
: "",
2114 tmp
= string_with_format (template_html
,
2116 data
->version
<= 2 ? "" : "@import url( \"main.css\" );",
2117 "%@", /* Leave variant unset */
2118 "", /* The header */
2119 footer_html
? footer_html
: "",
2122 g_ptr_array_add (data
->strings_to_free
, tmp
);
2123 data
->template_html
= tmp
;
2125 g_free (template_html
);
2126 g_free (footer_html
);
2132 empathy_adium_data_new (const gchar
*path
)
2134 EmpathyAdiumData
*data
;
2137 info
= empathy_adium_info_new (path
);
2138 data
= empathy_adium_data_new_with_info (path
, info
);
2139 g_hash_table_unref (info
);
2145 empathy_adium_data_ref (EmpathyAdiumData
*data
)
2147 g_return_val_if_fail (data
!= NULL
, NULL
);
2149 g_atomic_int_inc (&data
->ref_count
);
2155 empathy_adium_data_unref (EmpathyAdiumData
*data
)
2157 g_return_if_fail (data
!= NULL
);
2159 if (g_atomic_int_dec_and_test (&data
->ref_count
)) {
2160 g_free (data
->path
);
2161 g_free (data
->basedir
);
2162 g_free (data
->default_avatar_filename
);
2163 g_free (data
->default_incoming_avatar_filename
);
2164 g_free (data
->default_outgoing_avatar_filename
);
2165 g_hash_table_unref (data
->info
);
2166 g_ptr_array_unref (data
->strings_to_free
);
2167 tp_clear_pointer (&data
->date_format_cache
, g_hash_table_unref
);
2169 g_slice_free (EmpathyAdiumData
, data
);
2174 empathy_adium_data_get_info (EmpathyAdiumData
*data
)
2176 g_return_val_if_fail (data
!= NULL
, NULL
);
2182 empathy_adium_data_get_path (EmpathyAdiumData
*data
)
2184 g_return_val_if_fail (data
!= NULL
, NULL
);