From 053e54f94445922103c4e79a8fc34b151699d354 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=BCllner?= Date: Mon, 16 Feb 2015 19:40:00 +0100 Subject: [PATCH] calendar: Copy URLHighlighter and friends here We will start using both URLHighlighter and the _fixMarkup() helper method the same way it's used in MessageTray. Usually we should make fixMarkup() public and call the existing methods, but we are planning for them to go away soon, so just keep two copies until the original one is removed. https://bugzilla.gnome.org/show_bug.cgi?id=744817 --- js/ui/calendar.js | 144 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) diff --git a/js/ui/calendar.js b/js/ui/calendar.js index 95775db04..a09350a13 100644 --- a/js/ui/calendar.js +++ b/js/ui/calendar.js @@ -9,8 +9,11 @@ const Signals = imports.signals; const Pango = imports.gi.Pango; const Gettext_gtk30 = imports.gettext.domain('gtk30'); const Mainloop = imports.mainloop; +const Meta = imports.gi.Meta; const Shell = imports.gi.Shell; +const Util = imports.misc.util; + const MSECS_IN_DAY = 24 * 60 * 60 * 1000; const SHOW_WEEKDATE_KEY = 'show-weekdate'; const ELLIPSIS_CHAR = '\u2026'; @@ -137,6 +140,147 @@ function _getEventDayAbbreviation(dayNumber) { return abbreviations[dayNumber]; } +function _fixMarkup(text, allowMarkup) { + if (allowMarkup) { + // Support &, ", ', < and >, escape all other + // occurrences of '&'. + let _text = text.replace(/&(?!amp;|quot;|apos;|lt;|gt;)/g, '&'); + + // Support , , and , escape anything else + // so it displays as raw markup. + _text = _text.replace(/<(?!\/?[biu]>)/g, '<'); + + try { + Pango.parse_markup(_text, -1, ''); + return _text; + } catch (e) {} + } + + // !allowMarkup, or invalid markup + return GLib.markup_escape_text(text, -1); +} + +const URLHighlighter = new Lang.Class({ + Name: 'URLHighlighter', + + _init: function(text, lineWrap, allowMarkup) { + if (!text) + text = ''; + this.actor = new St.Label({ reactive: true, style_class: 'url-highlighter' }); + this._linkColor = '#ccccff'; + this.actor.connect('style-changed', Lang.bind(this, function() { + let [hasColor, color] = this.actor.get_theme_node().lookup_color('link-color', false); + if (hasColor) { + let linkColor = color.to_string().substr(0, 7); + if (linkColor != this._linkColor) { + this._linkColor = linkColor; + this._highlightUrls(); + } + } + })); + this.actor.clutter_text.line_wrap = lineWrap; + this.actor.clutter_text.line_wrap_mode = Pango.WrapMode.WORD_CHAR; + + this.setMarkup(text, allowMarkup); + this.actor.connect('button-press-event', Lang.bind(this, function(actor, event) { + // Don't try to URL highlight when invisible. + // The MessageTray doesn't actually hide us, so + // we need to check for paint opacities as well. + if (!actor.visible || actor.get_paint_opacity() == 0) + return Clutter.EVENT_PROPAGATE; + + // Keep Notification.actor from seeing this and taking + // a pointer grab, which would block our button-release-event + // handler, if an URL is clicked + return this._findUrlAtPos(event) != -1; + })); + this.actor.connect('button-release-event', Lang.bind(this, function (actor, event) { + if (!actor.visible || actor.get_paint_opacity() == 0) + return Clutter.EVENT_PROPAGATE; + + let urlId = this._findUrlAtPos(event); + if (urlId != -1) { + let url = this._urls[urlId].url; + if (url.indexOf(':') == -1) + url = 'http://' + url; + + Gio.app_info_launch_default_for_uri(url, global.create_app_launch_context(0, -1)); + return Clutter.EVENT_STOP; + } + return Clutter.EVENT_PROPAGATE; + })); + this.actor.connect('motion-event', Lang.bind(this, function(actor, event) { + if (!actor.visible || actor.get_paint_opacity() == 0) + return Clutter.EVENT_PROPAGATE; + + let urlId = this._findUrlAtPos(event); + if (urlId != -1 && !this._cursorChanged) { + global.screen.set_cursor(Meta.Cursor.POINTING_HAND); + this._cursorChanged = true; + } else if (urlId == -1) { + global.screen.set_cursor(Meta.Cursor.DEFAULT); + this._cursorChanged = false; + } + return Clutter.EVENT_PROPAGATE; + })); + this.actor.connect('leave-event', Lang.bind(this, function() { + if (!this.actor.visible || this.actor.get_paint_opacity() == 0) + return Clutter.EVENT_PROPAGATE; + + if (this._cursorChanged) { + this._cursorChanged = false; + global.screen.set_cursor(Meta.Cursor.DEFAULT); + } + return Clutter.EVENT_PROPAGATE; + })); + }, + + setMarkup: function(text, allowMarkup) { + text = text ? _fixMarkup(text, allowMarkup) : ''; + this._text = text; + + this.actor.clutter_text.set_markup(text); + /* clutter_text.text contain text without markup */ + this._urls = Util.findUrls(this.actor.clutter_text.text); + this._highlightUrls(); + }, + + _highlightUrls: function() { + // text here contain markup + let urls = Util.findUrls(this._text); + let markup = ''; + let pos = 0; + for (let i = 0; i < urls.length; i++) { + let url = urls[i]; + let str = this._text.substr(pos, url.pos - pos); + markup += str + '' + url.url + ''; + pos = url.pos + url.url.length; + } + markup += this._text.substr(pos); + this.actor.clutter_text.set_markup(markup); + }, + + _findUrlAtPos: function(event) { + let success; + let [x, y] = event.get_coords(); + [success, x, y] = this.actor.transform_stage_point(x, y); + let find_pos = -1; + for (let i = 0; i < this.actor.clutter_text.text.length; i++) { + let [success, px, py, line_height] = this.actor.clutter_text.position_to_coords(i); + if (py > y || py + line_height < y || x < px) + continue; + find_pos = i; + } + if (find_pos != -1) { + for (let i = 0; i < this._urls.length; i++) + if (find_pos >= this._urls[i].pos && + this._urls[i].pos + this._urls[i].url.length > find_pos) + return i; + } + return -1; + } +}); + // Abstraction for an appointment/event in a calendar const CalendarEvent = new Lang.Class({