diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css index 82b8440f9..c5b6b54c0 100644 --- a/data/theme/gnome-shell.css +++ b/data/theme/gnome-shell.css @@ -847,6 +847,10 @@ StTooltip StLabel { color: #cccccc; } +.url-highlighter { + link-color: #ccccff; +} + /* Message Tray */ #message-tray { background-gradient-direction: vertical; diff --git a/js/ui/dnd.js b/js/ui/dnd.js index 66f468083..71d8c04bc 100644 --- a/js/ui/dnd.js +++ b/js/ui/dnd.js @@ -26,9 +26,9 @@ const DragMotionResult = { }; const DRAG_CURSOR_MAP = { - 0: Shell.Cursor.UNSUPPORTED_TARGET, - 1: Shell.Cursor.COPY, - 2: Shell.Cursor.MOVE + 0: Shell.Cursor.DND_UNSUPPORTED_TARGET, + 1: Shell.Cursor.DND_COPY, + 2: Shell.Cursor.DND_MOVE }; const DragDropResult = { @@ -221,7 +221,7 @@ _Draggable.prototype = { if (this._onEventId) this._ungrabActor(); this._grabEvents(); - global.set_cursor(Shell.Cursor.IN_DRAG); + global.set_cursor(Shell.Cursor.DND_IN_DRAG); this._dragX = this._dragStartX = stageX; this._dragY = this._dragStartY = stageY; @@ -382,7 +382,7 @@ _Draggable.prototype = { } target = target.get_parent(); } - global.set_cursor(Shell.Cursor.IN_DRAG); + global.set_cursor(Shell.Cursor.DND_IN_DRAG); } return true; diff --git a/js/ui/messageTray.js b/js/ui/messageTray.js index e0c4ecc86..2b6226a78 100644 --- a/js/ui/messageTray.js +++ b/js/ui/messageTray.js @@ -1,6 +1,8 @@ /* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */ const Clutter = imports.gi.Clutter; +const GLib = imports.gi.GLib; +const Gio = imports.gi.Gio; const Gtk = imports.gi.Gtk; const Lang = imports.lang; const Mainloop = imports.mainloop; @@ -14,6 +16,7 @@ const Tweener = imports.ui.tweener; const Main = imports.ui.main; const BoxPointer = imports.ui.boxpointer; const Params = imports.misc.params; +const Utils = imports.misc.utils; const ANIMATION_TIME = 0.2; const NOTIFICATION_TIMEOUT = 4; @@ -44,6 +47,117 @@ function _cleanMarkup(text) { return _text.replace(/<(\/?[^biu]>|[^>\/][^>])/g, '<$1'); } +function URLHighlighter(text, lineWrap) { + this._init(text, lineWrap); +} + +URLHighlighter.prototype = { + _init: function(text, lineWrap) { + 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 color = new Clutter.Color(); + let hasColor = this.actor.get_theme_node().get_color('link-color', color); + if (hasColor) { + let linkColor = color.to_string().substr(0, 7); + if (linkColor != this._linkColor) { + this._linkColor = linkColor; + this._highlightUrls(); + } + } + })); + if (lineWrap) { + this.actor.clutter_text.line_wrap = true; + this.actor.clutter_text.line_wrap_mode = Pango.WrapMode.WORD_CHAR; + this.actor.clutter_text.ellipsize = Pango.EllipsizeMode.NONE; + } + + this.setMarkup(text); + this.actor.connect('button-release-event', Lang.bind(this, function (actor, event) { + let urlId = this._findUrlAtPos(event); + if (urlId != -1) { + let url = this._urls[urlId].url; + if (url.indexOf(':') == -1) + url = 'http://' + url; + try { + Gio.app_info_launch_default_for_uri(url, global.create_app_launch_context()); + return true; + } catch (e) { + // TODO: remove this after gnome 3 release + let p = new Shell.Process({ 'args' : ['gvfs-open', url] }); + p.run(); + return true; + } + } + return false; + })); + this.actor.connect('motion-event', Lang.bind(this, function(actor, event) { + let urlId = this._findUrlAtPos(event); + if (urlId != -1 && !this._cursorChanged) { + global.set_cursor(Shell.Cursor.POINTING_HAND); + this._cursorChanged = true; + } else if (urlId == -1) { + global.unset_cursor(); + this._cursorChanged = false; + } + return false; + })); + this.actor.connect('leave-event', Lang.bind(this, function() { + if (this._cursorChanged) { + this._cursorChanged = false; + global.unset_cursor(); + } + })); + }, + + setMarkup: function(text) { + text = text ? _cleanMarkup(text) : ''; + this._text = text; + + this.actor.clutter_text.set_markup(text); + /* clutter_text.text contain text without markup */ + this._urls = Utils.findUrls(this.actor.clutter_text.text); + this._highlightUrls(); + }, + + _highlightUrls: function() { + // text here contain markup + let urls = Utils.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; + } +}; + // Notification: // @source: the notification's Source // @title: the title @@ -148,7 +262,8 @@ Notification.prototype = { this._titleLabel = new St.Label(); this._bannerBox.add_actor(this._titleLabel); - this._bannerLabel = new St.Label(); + this._bannerUrlHighlighter = new URLHighlighter(); + this._bannerLabel = this._bannerUrlHighlighter.actor; this._bannerBox.add_actor(this._bannerLabel); this.update(title, banner, params); @@ -214,8 +329,9 @@ Notification.prototype = { // not fitting fully in the single-line mode. this._bannerBodyText = this._customContent ? null : banner; - banner = banner ? _cleanMarkup(banner.replace(/\n/g, ' ')) : ''; - this._bannerLabel.clutter_text.set_markup(banner); + banner = banner ? banner.replace(/\n/g, ' ') : ''; + + this._bannerUrlHighlighter.setMarkup(banner); this._bannerLabel.queue_relayout(); // Add the bannerBody now if we know for sure we'll need it @@ -259,16 +375,10 @@ Notification.prototype = { // // Return value: the newly-added label addBody: function(text) { - let body = new St.Label(); - body.clutter_text.line_wrap = true; - body.clutter_text.line_wrap_mode = Pango.WrapMode.WORD_CHAR; - body.clutter_text.ellipsize = Pango.EllipsizeMode.NONE; + let label = new URLHighlighter(text, true); - text = text ? _cleanMarkup(text) : ''; - body.clutter_text.set_markup(text); - - this.addActor(body); - return body; + this.addActor(label.actor); + return label.actor; }, _addBannerBody: function() { diff --git a/src/shell-global.c b/src/shell-global.c index dad0b2068..77e2d851a 100644 --- a/src/shell-global.c +++ b/src/shell-global.c @@ -497,6 +497,9 @@ shell_global_set_cursor (ShellGlobal *global, case SHELL_CURSOR_DND_UNSUPPORTED_TARGET: name = "dnd-none"; break; + case SHELL_CURSOR_POINTING_HAND: + name = "hand"; + break; default: g_return_if_reached (); } @@ -516,6 +519,8 @@ shell_global_set_cursor (ShellGlobal *global, case SHELL_CURSOR_DND_COPY: cursor_type = GDK_PLUS; break; + case SHELL_CURSOR_POINTING_HAND: + cursor_type = GDK_HAND2; case SHELL_CURSOR_DND_UNSUPPORTED_TARGET: cursor_type = GDK_X_CURSOR; break; diff --git a/src/shell-global.h b/src/shell-global.h index b8c5af7c6..91a82c454 100644 --- a/src/shell-global.h +++ b/src/shell-global.h @@ -38,7 +38,8 @@ typedef enum { SHELL_CURSOR_DND_IN_DRAG, SHELL_CURSOR_DND_UNSUPPORTED_TARGET, SHELL_CURSOR_DND_MOVE, - SHELL_CURSOR_DND_COPY + SHELL_CURSOR_DND_COPY, + SHELL_CURSOR_POINTING_HAND } ShellCursor; void shell_global_set_cursor (ShellGlobal *global,