Introduce MarkupView widget (#264)
Co-authored-by: Bleak Grey <bleakgrey@gmail.com>
This commit is contained in:
parent
3cf8d9ddd6
commit
02f168e19d
|
@ -19,6 +19,7 @@ Simple [Mastodon](https://github.com/tootsuite/mastodon) client for Linux
|
||||||
valac | 0.48
|
valac | 0.48
|
||||||
libglib-2.0-dev | 2.30.0
|
libglib-2.0-dev | 2.30.0
|
||||||
libjson-glib-dev | 1.4.4
|
libjson-glib-dev | 1.4.4
|
||||||
|
libxml2-dev | 2.9.10
|
||||||
libgee-0.8-dev | 0.8.5
|
libgee-0.8-dev | 0.8.5
|
||||||
libsoup2.4-dev | 2.64
|
libsoup2.4-dev | 2.64
|
||||||
libgtk-3-dev | 3.22.0
|
libgtk-3-dev | 3.22.0
|
||||||
|
|
|
@ -58,3 +58,10 @@
|
||||||
.ttl-large-body {
|
.ttl-large-body {
|
||||||
font-size: 110%;
|
font-size: 110%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ttl-code {
|
||||||
|
font-family: monospace;
|
||||||
|
padding: 12px;
|
||||||
|
background: rgba(150,150,150,.1);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
|
@ -67,6 +67,7 @@
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="vhomogeneous">False</property>
|
<property name="vhomogeneous">False</property>
|
||||||
<property name="transition_type">crossfade</property>
|
<property name="transition_type">crossfade</property>
|
||||||
|
<property name="interpolate_size">True</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkBox" id="status">
|
<object class="GtkBox" id="status">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
|
|
|
@ -100,16 +100,14 @@
|
||||||
<property name="activatable">False</property>
|
<property name="activatable">False</property>
|
||||||
<property name="selectable">False</property>
|
<property name="selectable">False</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="TootleWidgetsRichLabel" id="note">
|
<object class="TootleWidgetsMarkupView" id="note">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="wrap">True</property>
|
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="margin_left">8</property>
|
<property name="margin_left">8</property>
|
||||||
<property name="margin_right">8</property>
|
<property name="margin_right">8</property>
|
||||||
<property name="margin_top">8</property>
|
<property name="margin_top">8</property>
|
||||||
<property name="margin_bottom">8</property>
|
<property name="margin_bottom">8</property>
|
||||||
<property name="selectable">True</property>
|
<property name="selectable">True</property>
|
||||||
<property name="width_chars">25</property>
|
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
|
|
|
@ -48,6 +48,7 @@
|
||||||
<property name="wrap">True</property>
|
<property name="wrap">True</property>
|
||||||
<property name="wrap_mode">word-char</property>
|
<property name="wrap_mode">word-char</property>
|
||||||
<property name="xalign">0</property>
|
<property name="xalign">0</property>
|
||||||
|
<property name="markup">allow</property>
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="left_attach">2</property>
|
<property name="left_attach">2</property>
|
||||||
|
|
|
@ -57,6 +57,7 @@
|
||||||
<property name="xalign">0</property>
|
<property name="xalign">0</property>
|
||||||
<property name="margin-bottom">8</property>
|
<property name="margin-bottom">8</property>
|
||||||
<property name="track_visited_links">false</property>
|
<property name="track_visited_links">false</property>
|
||||||
|
<property name="markup">trust</property>
|
||||||
<style>
|
<style>
|
||||||
<class name="title-4"/>
|
<class name="title-4"/>
|
||||||
</style>
|
</style>
|
||||||
|
@ -91,13 +92,14 @@
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="TootleWidgetsRichLabel" id="handle_label">
|
<object class="GtkLabel" id="handle_label">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="label" translatable="no">Handle</property>
|
<property name="label" translatable="no">Handle</property>
|
||||||
<property name="opacity">0.5</property>
|
<property name="opacity">0.5</property>
|
||||||
<property name="ellipsize">end</property>
|
<property name="ellipsize">end</property>
|
||||||
<property name="single_line_mode">True</property>
|
<property name="single_line_mode">True</property>
|
||||||
|
<property name="xalign">0</property>
|
||||||
<style>
|
<style>
|
||||||
<class name="body"/>
|
<class name="body"/>
|
||||||
</style>
|
</style>
|
||||||
|
@ -143,11 +145,12 @@
|
||||||
</packing>
|
</packing>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="TootleWidgetsRichLabel" id="date_label">
|
<object class="GtkLabel" id="date_label">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="label" translatable="no">Yesterday</property>
|
<property name="label" translatable="no">Yesterday</property>
|
||||||
<property name="opacity">0.5</property>
|
<property name="opacity">0.5</property>
|
||||||
|
<property name="xalign">0</property>
|
||||||
<style>
|
<style>
|
||||||
<class name="body"/>
|
<class name="body"/>
|
||||||
</style>
|
</style>
|
||||||
|
@ -241,15 +244,10 @@
|
||||||
<property name="orientation">vertical</property>
|
<property name="orientation">vertical</property>
|
||||||
<property name="spacing">8</property>
|
<property name="spacing">8</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="TootleWidgetsRichLabel" id="content">
|
<object class="TootleWidgetsMarkupView" id="content">
|
||||||
<property name="visible">True</property>
|
<property name="visible">True</property>
|
||||||
<property name="can_focus">False</property>
|
<property name="can_focus">False</property>
|
||||||
<property name="hexpand">True</property>
|
<property name="hexpand">True</property>
|
||||||
<property name="label">Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged.</property>
|
|
||||||
<property name="wrap">True</property>
|
|
||||||
<property name="wrap_mode">word-char</property>
|
|
||||||
<property name="width_chars">15</property>
|
|
||||||
<property name="xalign">0</property>
|
|
||||||
</object>
|
</object>
|
||||||
<packing>
|
<packing>
|
||||||
<property name="expand">False</property>
|
<property name="expand">False</property>
|
||||||
|
|
|
@ -96,6 +96,8 @@ executable(
|
||||||
'src/Widgets/Attachment/Slot.vala',
|
'src/Widgets/Attachment/Slot.vala',
|
||||||
'src/Widgets/Attachment/Picture.vala',
|
'src/Widgets/Attachment/Picture.vala',
|
||||||
'src/Widgets/AdaptiveButton.vala',
|
'src/Widgets/AdaptiveButton.vala',
|
||||||
|
'src/Widgets/MarkupPolicy.vala',
|
||||||
|
'src/Widgets/MarkupView.vala',
|
||||||
'src/Dialogs/ISavedWindow.vala',
|
'src/Dialogs/ISavedWindow.vala',
|
||||||
'src/Dialogs/NewAccount.vala',
|
'src/Dialogs/NewAccount.vala',
|
||||||
'src/Dialogs/MainWindow.vala',
|
'src/Dialogs/MainWindow.vala',
|
||||||
|
@ -126,6 +128,7 @@ executable(
|
||||||
dependency('gee-0.8', version: '>=0.8.5'),
|
dependency('gee-0.8', version: '>=0.8.5'),
|
||||||
dependency('libsoup-2.4'),
|
dependency('libsoup-2.4'),
|
||||||
dependency('json-glib-1.0', version: '>=1.4.4'),
|
dependency('json-glib-1.0', version: '>=1.4.4'),
|
||||||
|
dependency('libxml-2.0'),
|
||||||
libhandy_dep,
|
libhandy_dep,
|
||||||
],
|
],
|
||||||
install: true,
|
install: true,
|
||||||
|
|
|
@ -63,7 +63,7 @@ public class Tootle.API.Attachment : Entity {
|
||||||
|
|
||||||
var descr_param = "";
|
var descr_param = "";
|
||||||
if (descr != null && descr.replace (" ", "") != "") {
|
if (descr != null && descr.replace (" ", "") != "") {
|
||||||
descr_param = "?description=" + Html.uri_encode (descr);
|
descr_param = "?description=" + HtmlUtils.uri_encode (descr);
|
||||||
}
|
}
|
||||||
|
|
||||||
var buffer = new Soup.Buffer.take (contents);
|
var buffer = new Soup.Buffer.take (contents);
|
||||||
|
|
|
@ -68,9 +68,9 @@ public class Tootle.API.Status : Entity, Widgetizable {
|
||||||
if (account.note == "")
|
if (account.note == "")
|
||||||
content = "";
|
content = "";
|
||||||
else if ("\n" in account.note)
|
else if ("\n" in account.note)
|
||||||
content = Html.remove_tags (account.note.split ("\n")[0]);
|
content = account.note.split ("\n")[0];
|
||||||
else
|
else
|
||||||
content = Html.remove_tags (account.note);
|
content = account.note;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Gtk.Widget to_widget () {
|
public override Gtk.Widget to_widget () {
|
||||||
|
|
|
@ -110,7 +110,7 @@ public class Tootle.Dialogs.Compose : Hdy.Window {
|
||||||
cw.text = status.spoiler_text;
|
cw.text = status.spoiler_text;
|
||||||
cw_button.active = true;
|
cw_button.active = true;
|
||||||
}
|
}
|
||||||
content.buffer.text = Html.remove_tags (status.content);
|
content.buffer.text = HtmlUtils.remove_tags (status.content);
|
||||||
|
|
||||||
validate ();
|
validate ();
|
||||||
set_media_mode (status.has_media ());
|
set_media_mode (status.has_media ());
|
||||||
|
@ -291,11 +291,11 @@ public class Tootle.Dialogs.Compose : Hdy.Window {
|
||||||
var req = new Request.POST (@"/api/v1/statuses?$media_param")
|
var req = new Request.POST (@"/api/v1/statuses?$media_param")
|
||||||
.with_account (accounts.active)
|
.with_account (accounts.active)
|
||||||
.with_param ("visibility", visibility_popover.selected.to_string ())
|
.with_param ("visibility", visibility_popover.selected.to_string ())
|
||||||
.with_param ("status", Html.uri_encode (status.content));
|
.with_param ("status", HtmlUtils.uri_encode (status.content));
|
||||||
|
|
||||||
if (cw_button.active) {
|
if (cw_button.active) {
|
||||||
req.with_param ("sensitive", "true");
|
req.with_param ("sensitive", "true");
|
||||||
req.with_param ("spoiler_text", Html.uri_encode (cw.text));
|
req.with_param ("spoiler_text", HtmlUtils.uri_encode (cw.text));
|
||||||
}
|
}
|
||||||
if (status.in_reply_to_id != null)
|
if (status.in_reply_to_id != null)
|
||||||
req.with_param ("in_reply_to_id", status.in_reply_to_id);
|
req.with_param ("in_reply_to_id", status.in_reply_to_id);
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
public class Tootle.Html {
|
public class Tootle.HtmlUtils {
|
||||||
|
|
||||||
public const string FALLBACK_TEXT = _("[ There was an error parsing this text :c ]");
|
public const string FALLBACK_TEXT = _("[ There was an error parsing this text :c ]");
|
||||||
|
|
||||||
public static string remove_tags (string content) {
|
public static string remove_tags (string content) {
|
||||||
try {
|
try {
|
||||||
|
//TODO: remove this when simplify() uses the HTML parsing class
|
||||||
var fixed_paragraphs = simplify (content);
|
var fixed_paragraphs = simplify (content);
|
||||||
|
|
||||||
var all_tags = new Regex ("<(.|\n)*?>", RegexCompileFlags.CASELESS);
|
var all_tags = new Regex ("<(.|\n)*?>", RegexCompileFlags.CASELESS);
|
||||||
return Widgets.RichLabel.restore_entities (all_tags.replace (fixed_paragraphs, -1, 0, ""));
|
return Widgets.RichLabel.restore_entities (all_tags.replace (fixed_paragraphs, -1, 0, ""));
|
||||||
}
|
}
|
||||||
|
@ -14,6 +16,8 @@ public class Tootle.Html {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//TODO: Perhaps this should use the HTML parser class
|
||||||
|
// since we depend on it anyway
|
||||||
public static string simplify (string str) {
|
public static string simplify (string str) {
|
||||||
try {
|
try {
|
||||||
var divided = str
|
var divided = str
|
||||||
|
@ -32,8 +36,8 @@ public class Tootle.Html {
|
||||||
return simplified;
|
return simplified;
|
||||||
}
|
}
|
||||||
catch (Error e) {
|
catch (Error e) {
|
||||||
warning (e.message);
|
warning (@"Can't simplify string \"$str\":\n$(e.message)");
|
||||||
return FALLBACK_TEXT;
|
return remove_tags (str);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -63,13 +63,13 @@ public class Tootle.InstanceAccount : API.Account, IStreamListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
void show_notification (API.Notification obj) {
|
void show_notification (API.Notification obj) {
|
||||||
var title = Html.remove_tags (obj.kind.get_desc (obj.account));
|
var title = HtmlUtils.remove_tags (obj.kind.get_desc (obj.account));
|
||||||
var notification = new GLib.Notification (title);
|
var notification = new GLib.Notification (title);
|
||||||
if (obj.status != null) {
|
if (obj.status != null) {
|
||||||
var body = "";
|
var body = "";
|
||||||
body += domain;
|
body += domain;
|
||||||
body += "\n";
|
body += "\n";
|
||||||
body += Html.remove_tags (obj.status.content);
|
body += HtmlUtils.remove_tags (obj.status.content);
|
||||||
notification.set_body (body);
|
notification.set_body (body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -47,9 +47,9 @@ public class Tootle.Views.Profile : Views.Timeline {
|
||||||
profile.bind_property ("display-name", handle, "text", BindingFlags.SYNC_CREATE);
|
profile.bind_property ("display-name", handle, "text", BindingFlags.SYNC_CREATE);
|
||||||
|
|
||||||
note_row = builder.get_object ("note_row") as ListBoxRow;
|
note_row = builder.get_object ("note_row") as ListBoxRow;
|
||||||
var note = builder.get_object ("note") as Widgets.RichLabel;
|
var note = builder.get_object ("note") as Widgets.MarkupView;
|
||||||
profile.bind_property ("note", note, "text", BindingFlags.SYNC_CREATE, (b, src, ref target) => {
|
profile.bind_property ("note", note, "content", BindingFlags.SYNC_CREATE, (b, src, ref target) => {
|
||||||
var text = Html.simplify ((string) src);
|
var text = (string) src;
|
||||||
target.set_string (text);
|
target.set_string (text);
|
||||||
note_row.visible = text != "";
|
note_row.visible = text != "";
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -19,7 +19,7 @@ public class Tootle.Widgets.Conversation : Widgets.Status {
|
||||||
owned get {
|
owned get {
|
||||||
var label = "";
|
var label = "";
|
||||||
foreach (API.Account account in conversation.accounts) {
|
foreach (API.Account account in conversation.accounts) {
|
||||||
label += "<b>" + Html.simplify (account.display_name) + "</b>";
|
label += account.display_name;
|
||||||
if (conversation.accounts.last () != account)
|
if (conversation.accounts.last () != account)
|
||||||
label += ", ";
|
label += ", ";
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
public enum Tootle.MarkupPolicy {
|
||||||
|
|
||||||
|
// Remove all tags from the string
|
||||||
|
DISALLOW,
|
||||||
|
|
||||||
|
// Allow markup, remove unsupported tags from the input string
|
||||||
|
ALLOW,
|
||||||
|
|
||||||
|
// Allow markup, do nothing with the input string
|
||||||
|
TRUST;
|
||||||
|
|
||||||
|
public string process (string input) {
|
||||||
|
switch (this) {
|
||||||
|
case DISALLOW:
|
||||||
|
return HtmlUtils.remove_tags (input);
|
||||||
|
case ALLOW:
|
||||||
|
return HtmlUtils.simplify (input);
|
||||||
|
default:
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void apply (Widgets.RichLabel w) {
|
||||||
|
w.use_markup = this != DISALLOW;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,174 @@
|
||||||
|
using Gtk;
|
||||||
|
|
||||||
|
public class Tootle.Widgets.MarkupView : Box {
|
||||||
|
|
||||||
|
public delegate void NodeFn (Xml.Node* node);
|
||||||
|
public delegate void NodeHandlerFn (MarkupView view, Xml.Node* node);
|
||||||
|
|
||||||
|
string? current_chunk = null;
|
||||||
|
|
||||||
|
string _content = "";
|
||||||
|
public string content {
|
||||||
|
get {
|
||||||
|
return _content;
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
_content = value;
|
||||||
|
update_content (_content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool _selectable = false;
|
||||||
|
public bool selectable {
|
||||||
|
get { return _selectable; }
|
||||||
|
set {
|
||||||
|
_selectable = value;
|
||||||
|
get_children ().foreach (w => {
|
||||||
|
var label = w as Label;
|
||||||
|
if (label != null) {
|
||||||
|
label.selectable = _selectable;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
construct {
|
||||||
|
orientation = Orientation.VERTICAL;
|
||||||
|
spacing = 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
void update_content (string content) {
|
||||||
|
current_chunk = null;
|
||||||
|
|
||||||
|
get_children ().foreach (w => {
|
||||||
|
w.destroy ();
|
||||||
|
});
|
||||||
|
|
||||||
|
var doc = Html.Doc.read_doc (content, "", "utf8");
|
||||||
|
if (doc != null) {
|
||||||
|
var root = doc->get_root_element ();
|
||||||
|
if (root != null) {
|
||||||
|
default_handler (this, root);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delete doc;
|
||||||
|
|
||||||
|
visible = get_children ().length () > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void traverse (Xml.Node* root, owned NodeFn cb) {
|
||||||
|
for (var iter = root->children; iter != null; iter = iter->next) {
|
||||||
|
cb (iter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void traverse_and_handle (MarkupView v, Xml.Node* root, owned NodeHandlerFn handler) {
|
||||||
|
traverse (root, node => {
|
||||||
|
handler (v, node);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void commit_chunk () {
|
||||||
|
if (current_chunk != null && current_chunk != "") {
|
||||||
|
var label = new RichLabel (current_chunk) {
|
||||||
|
visible = true,
|
||||||
|
markup = MarkupPolicy.TRUST,
|
||||||
|
selectable = _selectable
|
||||||
|
};
|
||||||
|
pack_start (label);
|
||||||
|
}
|
||||||
|
current_chunk = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void write_chunk (string? chunk) {
|
||||||
|
if (chunk == null) return;
|
||||||
|
|
||||||
|
if (current_chunk == null)
|
||||||
|
current_chunk = chunk;
|
||||||
|
else
|
||||||
|
current_chunk += chunk;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void default_handler (MarkupView v, Xml.Node* root) {
|
||||||
|
switch (root->name) {
|
||||||
|
case "html":
|
||||||
|
case "span":
|
||||||
|
case "markup":
|
||||||
|
case "pre":
|
||||||
|
case "ul":
|
||||||
|
case "ol":
|
||||||
|
traverse_and_handle (v, root, default_handler);
|
||||||
|
break;
|
||||||
|
case "body":
|
||||||
|
traverse_and_handle (v, root, default_handler);
|
||||||
|
v.commit_chunk ();
|
||||||
|
break;
|
||||||
|
case "p":
|
||||||
|
// Don't add spacing if this is the first paragraph
|
||||||
|
if (v.current_chunk != "" && v.current_chunk != null)
|
||||||
|
v.write_chunk ("\n\n");
|
||||||
|
|
||||||
|
traverse_and_handle (v, root, default_handler);
|
||||||
|
break;
|
||||||
|
case "code":
|
||||||
|
case "blockquote":
|
||||||
|
v.commit_chunk ();
|
||||||
|
|
||||||
|
var text = "";
|
||||||
|
traverse (root, (node) => {
|
||||||
|
switch (node->name) {
|
||||||
|
case "text":
|
||||||
|
text += node->content;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var label = new RichLabel (text) {
|
||||||
|
visible = true,
|
||||||
|
markup = MarkupPolicy.DISALLOW
|
||||||
|
};
|
||||||
|
label.get_style_context ().add_class ("ttl-code");
|
||||||
|
v.pack_start (label);
|
||||||
|
break;
|
||||||
|
case "a":
|
||||||
|
var href = root->get_prop ("href");
|
||||||
|
if (href != null) {
|
||||||
|
v.write_chunk ("<a href='" + GLib.Markup.escape_text (href) + "'>");
|
||||||
|
traverse_and_handle (v, root, default_handler);
|
||||||
|
v.write_chunk ("</a>");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "b":
|
||||||
|
case "i":
|
||||||
|
case "u":
|
||||||
|
case "s":
|
||||||
|
case "sup":
|
||||||
|
case "sub":
|
||||||
|
v.write_chunk (@"<$(root->name)>");
|
||||||
|
traverse_and_handle (v, root, default_handler);
|
||||||
|
v.write_chunk (@"</$(root->name)>");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "li":
|
||||||
|
v.write_chunk ("\n• ");
|
||||||
|
traverse_and_handle (v, root, default_handler);
|
||||||
|
break;
|
||||||
|
case "br":
|
||||||
|
v.write_chunk ("\n");
|
||||||
|
break;
|
||||||
|
case "text":
|
||||||
|
if (root->content != null)
|
||||||
|
v.write_chunk (GLib.Markup.escape_text (root->content));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
warning (@"Unknown HTML tag: \"$(root->name)\"");
|
||||||
|
traverse_and_handle (v, root, default_handler);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -3,19 +3,30 @@ using Gee;
|
||||||
|
|
||||||
public class Tootle.Widgets.RichLabel : Label {
|
public class Tootle.Widgets.RichLabel : Label {
|
||||||
|
|
||||||
|
// TODO: We can parse <a> tags and extract resolvable URIs now
|
||||||
public weak ArrayList<API.Mention>? mentions;
|
public weak ArrayList<API.Mention>? mentions;
|
||||||
|
|
||||||
|
MarkupPolicy _markup = DISALLOW;
|
||||||
|
public MarkupPolicy markup {
|
||||||
|
get {
|
||||||
|
return _markup;
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
_markup = value;
|
||||||
|
_markup.apply (this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public string text {
|
public string text {
|
||||||
get {
|
get {
|
||||||
return this.label;
|
return this.label;
|
||||||
}
|
}
|
||||||
set {
|
set {
|
||||||
this.label = escape_entities (Html.simplify (value));
|
this.label = markup.process (value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
construct {
|
construct {
|
||||||
use_markup = true;
|
|
||||||
xalign = 0;
|
xalign = 0;
|
||||||
wrap_mode = Pango.WrapMode.WORD_CHAR;
|
wrap_mode = Pango.WrapMode.WORD_CHAR;
|
||||||
justify = Justification.LEFT;
|
justify = Justification.LEFT;
|
||||||
|
|
|
@ -42,16 +42,16 @@ public class Tootle.Widgets.Status : ListBoxRow {
|
||||||
|
|
||||||
[GtkChild] public Widgets.Avatar avatar;
|
[GtkChild] public Widgets.Avatar avatar;
|
||||||
[GtkChild] protected Widgets.RichLabel name_label;
|
[GtkChild] protected Widgets.RichLabel name_label;
|
||||||
[GtkChild] protected Widgets.RichLabel handle_label;
|
[GtkChild] protected Label handle_label;
|
||||||
[GtkChild] protected Box indicators;
|
[GtkChild] protected Box indicators;
|
||||||
[GtkChild] protected Widgets.RichLabel date_label;
|
[GtkChild] protected Label date_label;
|
||||||
[GtkChild] protected Image pin_indicator;
|
[GtkChild] protected Image pin_indicator;
|
||||||
[GtkChild] protected Image indicator;
|
[GtkChild] protected Image indicator;
|
||||||
|
|
||||||
[GtkChild] protected Box content_column;
|
[GtkChild] protected Box content_column;
|
||||||
[GtkChild] protected Stack spoiler_stack;
|
[GtkChild] protected Stack spoiler_stack;
|
||||||
[GtkChild] protected Box content_box;
|
[GtkChild] protected Box content_box;
|
||||||
[GtkChild] protected Widgets.RichLabel content;
|
[GtkChild] protected Widgets.MarkupView content;
|
||||||
[GtkChild] protected Widgets.Attachment.Box attachments;
|
[GtkChild] protected Widgets.Attachment.Box attachments;
|
||||||
[GtkChild] protected Button spoiler_button;
|
[GtkChild] protected Button spoiler_button;
|
||||||
[GtkChild] protected Widgets.RichLabel spoiler_label;
|
[GtkChild] protected Widgets.RichLabel spoiler_label;
|
||||||
|
@ -84,7 +84,7 @@ public class Tootle.Widgets.Status : ListBoxRow {
|
||||||
|
|
||||||
public string title_text {
|
public string title_text {
|
||||||
owned get {
|
owned get {
|
||||||
return Html.simplify (status.formal.account.display_name);
|
return status.formal.account.display_name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -140,9 +140,9 @@ public class Tootle.Widgets.Status : ListBoxRow {
|
||||||
reply_button_icon.icon_name = "mail-reply-sender-symbolic";
|
reply_button_icon.icon_name = "mail-reply-sender-symbolic";
|
||||||
|
|
||||||
bind_property ("spoiler-text", spoiler_label, "text", BindingFlags.SYNC_CREATE);
|
bind_property ("spoiler-text", spoiler_label, "text", BindingFlags.SYNC_CREATE);
|
||||||
status.formal.bind_property ("content", content, "text", BindingFlags.SYNC_CREATE);
|
status.formal.bind_property ("content", content, "content", BindingFlags.SYNC_CREATE);
|
||||||
bind_property ("title_text", name_label, "text", BindingFlags.SYNC_CREATE);
|
bind_property ("title_text", name_label, "text", BindingFlags.SYNC_CREATE);
|
||||||
bind_property ("subtitle_text", handle_label, "text", BindingFlags.SYNC_CREATE);
|
bind_property ("subtitle_text", handle_label, "label", BindingFlags.SYNC_CREATE);
|
||||||
bind_property ("date", date_label, "label", BindingFlags.SYNC_CREATE);
|
bind_property ("date", date_label, "label", BindingFlags.SYNC_CREATE);
|
||||||
status.formal.bind_property ("pinned", pin_indicator, "visible", BindingFlags.SYNC_CREATE);
|
status.formal.bind_property ("pinned", pin_indicator, "visible", BindingFlags.SYNC_CREATE);
|
||||||
status.formal.bind_property ("account", avatar, "account", BindingFlags.SYNC_CREATE);
|
status.formal.bind_property ("account", avatar, "account", BindingFlags.SYNC_CREATE);
|
||||||
|
@ -166,9 +166,11 @@ public class Tootle.Widgets.Status : ListBoxRow {
|
||||||
if (status.id == "") {
|
if (status.id == "") {
|
||||||
actions.destroy ();
|
actions.destroy ();
|
||||||
date_label.destroy ();
|
date_label.destroy ();
|
||||||
content.single_line_mode = true;
|
|
||||||
content.lines = 2;
|
//TODO: this
|
||||||
content.ellipsize = Pango.EllipsizeMode.END;
|
// content.single_line_mode = true;
|
||||||
|
// content.lines = 2;
|
||||||
|
// content.ellipsize = Pango.EllipsizeMode.END;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!attachments.populate (status.formal.media_attachments) || status.id == "") {
|
if (!attachments.populate (status.formal.media_attachments) || status.id == "") {
|
||||||
|
@ -178,7 +180,7 @@ public class Tootle.Widgets.Status : ListBoxRow {
|
||||||
menu_button.clicked.connect (open_menu);
|
menu_button.clicked.connect (open_menu);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Status (API.Status status, API.NotificationType? kind = null) {
|
public Status (owned API.Status status, API.NotificationType? kind = null) {
|
||||||
Object (
|
Object (
|
||||||
status: status,
|
status: status,
|
||||||
kind: kind
|
kind: kind
|
||||||
|
@ -216,7 +218,7 @@ public class Tootle.Widgets.Status : ListBoxRow {
|
||||||
item_copy_link.activate.connect (() => Desktop.copy (status.formal.url));
|
item_copy_link.activate.connect (() => Desktop.copy (status.formal.url));
|
||||||
var item_copy = new Gtk.MenuItem.with_label (_("Copy Text"));
|
var item_copy = new Gtk.MenuItem.with_label (_("Copy Text"));
|
||||||
item_copy.activate.connect (() => {
|
item_copy.activate.connect (() => {
|
||||||
var sanitized = Html.remove_tags (status.formal.content);
|
var sanitized = HtmlUtils.remove_tags (status.formal.content);
|
||||||
Desktop.copy (sanitized);
|
Desktop.copy (sanitized);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -261,14 +263,14 @@ public class Tootle.Widgets.Status : ListBoxRow {
|
||||||
|
|
||||||
public void expand_root () {
|
public void expand_root () {
|
||||||
activatable = false;
|
activatable = false;
|
||||||
content.selectable = true;
|
content.selectable = true;
|
||||||
content.get_style_context ().add_class ("ttl-large-body");
|
content.get_style_context ().add_class ("ttl-large-body");
|
||||||
|
|
||||||
var parent = content_column.get_parent () as Container;
|
var parent = content_column.get_parent () as Container;
|
||||||
var left_attach = parent.find_child_property ("left-attach");
|
var left_attach = parent.find_child_property ("left-attach");
|
||||||
var width = parent.find_child_property ("width");
|
var width = parent.find_child_property ("width");
|
||||||
parent.set_child_property (content_column, 1, 0, left_attach);
|
parent.set_child_property (content_column, 1, 0, left_attach);
|
||||||
parent.set_child_property (content_column, 3, 2, width);
|
parent.set_child_property (content_column, 3, 2, width);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void install_thread_line () {
|
public void install_thread_line () {
|
||||||
|
|
Loading…
Reference in New Issue