[gnome-shell] messageTray: make links in message banners clickable



commit 65f0b483f801378cdb3c5f808badc9822ca0e784
Author: Maxim Ermilov <zaspire rambler ru>
Date:   Wed Nov 24 02:27:47 2010 +0300

    messageTray: make links in message banners clickable
    
    https://bugzilla.gnome.org/show_bug.cgi?id=610219

 data/theme/gnome-shell.css |    4 +
 js/ui/dnd.js               |   10 ++--
 js/ui/messageTray.js       |  134 ++++++++++++++++++++++++++++++++++++++++----
 src/shell-global.c         |    5 ++
 src/shell-global.h         |    3 +-
 5 files changed, 138 insertions(+), 18 deletions(-)
---
diff --git a/data/theme/gnome-shell.css b/data/theme/gnome-shell.css
index 82b8440..c5b6b54 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 66f4680..71d8c04 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 e0c4ecc..2b6226a 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, '&lt;$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 + '<span foreground="' + this._linkColor + '"><u>' + url.url + '</u></span>';
+            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;
-
-        text = text ? _cleanMarkup(text) : '';
-        body.clutter_text.set_markup(text);
+        let label = new URLHighlighter(text, true);
 
-        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 dad0b20..77e2d85 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 b8c5af7..91a82c4 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,



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]