From 3296c23e32b74e89cf115db8e7920be6894acdc5 Mon Sep 17 00:00:00 2001
From: lovetox <forenjunkie@chelo.at>
Date: Thu, 17 Nov 2016 03:29:18 +0100
Subject: [PATCH] Refactor Roster Tooltip

---
 data/gui/roster_window.ui          |   3 -
 data/gui/tooltip_roster_contact.ui | 370 ++++++++++++++++++++
 src/gui_interface.py               |  15 +-
 src/roster_window.py               | 194 +++--------
 src/tooltips.py                    | 532 ++++++++++++++++++-----------
 5 files changed, 762 insertions(+), 352 deletions(-)
 create mode 100644 data/gui/tooltip_roster_contact.ui

diff --git a/data/gui/roster_window.ui b/data/gui/roster_window.ui
index c6ea7f004d..f7bd24a56a 100644
--- a/data/gui/roster_window.ui
+++ b/data/gui/roster_window.ui
@@ -366,12 +366,9 @@
                         <signal name="button-press-event" handler="on_roster_treeview_button_press_event" swapped="no"/>
                         <signal name="button-release-event" handler="on_roster_treeview_button_release_event" swapped="no"/>
                         <signal name="key-press-event" handler="on_roster_treeview_key_press_event" swapped="no"/>
-                        <signal name="leave-notify-event" handler="on_roster_treeview_leave_notify_event" swapped="no"/>
-                        <signal name="motion-notify-event" handler="on_roster_treeview_motion_notify_event" swapped="no"/>
                         <signal name="row-activated" handler="on_roster_treeview_row_activated" swapped="no"/>
                         <signal name="row-collapsed" handler="on_roster_treeview_row_collapsed" swapped="no"/>
                         <signal name="row-expanded" handler="on_roster_treeview_row_expanded" swapped="no"/>
-                        <signal name="scroll-event" handler="on_roster_treeview_scroll_event" swapped="no"/>
                         <child internal-child="selection">
                           <object class="GtkTreeSelection" id="treeview-selection1"/>
                         </child>
diff --git a/data/gui/tooltip_roster_contact.ui b/data/gui/tooltip_roster_contact.ui
new file mode 100644
index 0000000000..2d9a3550c4
--- /dev/null
+++ b/data/gui/tooltip_roster_contact.ui
@@ -0,0 +1,370 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Generated with glade 3.18.3 -->
+<interface>
+  <requires lib="gtk+" version="3.12"/>
+  <object class="GtkGrid" id="tooltip_grid">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="halign">start</property>
+    <property name="column_spacing">5</property>
+    <child>
+      <object class="GtkLabel" id="name">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="halign">start</property>
+        <property name="valign">start</property>
+        <property name="use_markup">True</property>
+        <property name="xalign">0</property>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">0</property>
+        <property name="width">2</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="jid_label">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="halign">start</property>
+        <property name="valign">start</property>
+        <property name="label" translatable="yes">Jabber ID:</property>
+        <property name="xalign">0</property>
+        <property name="yalign">0</property>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">4</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkImage" id="avatar">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="xalign">0</property>
+        <property name="stock">gtk-missing-image</property>
+      </object>
+      <packing>
+        <property name="left_attach">2</property>
+        <property name="top_attach">0</property>
+        <property name="height">14</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="resource_label">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="halign">start</property>
+        <property name="label" translatable="yes">Resource:</property>
+        <property name="xalign">0</property>
+        <property name="yalign">0</property>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">5</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="user_show">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="halign">start</property>
+        <property name="valign">start</property>
+        <property name="xalign">0</property>
+        <property name="yalign">0</property>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">1</property>
+        <property name="width">2</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="resource">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="hexpand">True</property>
+        <property name="xalign">0</property>
+        <property name="yalign">0</property>
+      </object>
+      <packing>
+        <property name="left_attach">1</property>
+        <property name="top_attach">5</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="jid">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="hexpand">True</property>
+        <property name="xalign">0</property>
+        <property name="yalign">0</property>
+        <attributes>
+          <attribute name="weight" value="bold"/>
+        </attributes>
+      </object>
+      <packing>
+        <property name="left_attach">1</property>
+        <property name="top_attach">4</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="status_label">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="halign">start</property>
+        <property name="valign">start</property>
+        <property name="label" translatable="yes">Status:</property>
+        <property name="wrap">True</property>
+        <property name="max_width_chars">40</property>
+        <property name="xalign">0</property>
+        <property name="yalign">0</property>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">2</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="idle_since_label">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="halign">start</property>
+        <property name="label" translatable="yes">Idle since:</property>
+        <property name="xalign">0</property>
+        <property name="yalign">0</property>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">12</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="idle_for_label">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="halign">start</property>
+        <property name="label" translatable="yes">Idle for:</property>
+        <property name="xalign">0</property>
+        <property name="yalign">0</property>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">13</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="idle_since">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="hexpand">True</property>
+        <property name="xalign">0</property>
+        <property name="yalign">0</property>
+      </object>
+      <packing>
+        <property name="left_attach">1</property>
+        <property name="top_attach">12</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="idle_for">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="hexpand">True</property>
+        <property name="xalign">0</property>
+        <property name="yalign">0</property>
+      </object>
+      <packing>
+        <property name="left_attach">1</property>
+        <property name="top_attach">13</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="mood_label">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="halign">start</property>
+        <property name="label" translatable="yes">Mood:</property>
+        <property name="xalign">0</property>
+        <property name="yalign">0</property>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">6</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="activity_label">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="halign">start</property>
+        <property name="label" translatable="yes">Activity:</property>
+        <property name="lines">2</property>
+        <property name="xalign">0</property>
+        <property name="yalign">0</property>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">7</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="tune_label">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="halign">start</property>
+        <property name="label" translatable="yes">Tune:</property>
+        <property name="xalign">0</property>
+        <property name="yalign">0</property>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">8</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="location_label">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="halign">start</property>
+        <property name="label" translatable="yes">Location:</property>
+        <property name="xalign">0</property>
+        <property name="yalign">0</property>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">9</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="mood">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="hexpand">True</property>
+        <property name="xalign">0</property>
+        <property name="yalign">0</property>
+      </object>
+      <packing>
+        <property name="left_attach">1</property>
+        <property name="top_attach">6</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="activity">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="hexpand">True</property>
+        <property name="xalign">0</property>
+        <property name="yalign">0</property>
+      </object>
+      <packing>
+        <property name="left_attach">1</property>
+        <property name="top_attach">7</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="tune">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="hexpand">True</property>
+        <property name="xalign">0</property>
+        <property name="yalign">0</property>
+      </object>
+      <packing>
+        <property name="left_attach">1</property>
+        <property name="top_attach">8</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="location">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="hexpand">True</property>
+        <property name="xalign">0</property>
+        <property name="yalign">0</property>
+      </object>
+      <packing>
+        <property name="left_attach">1</property>
+        <property name="top_attach">9</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="pgp_label">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="halign">start</property>
+        <property name="label" translatable="yes">OpenPGP:</property>
+        <property name="xalign">0</property>
+        <property name="yalign">0</property>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">10</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="pgp">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="hexpand">True</property>
+        <property name="xalign">0</property>
+        <property name="yalign">0</property>
+      </object>
+      <packing>
+        <property name="left_attach">1</property>
+        <property name="top_attach">10</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="sub_label">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="halign">start</property>
+        <property name="label" translatable="yes">Subscription:</property>
+        <property name="xalign">0</property>
+        <property name="yalign">0</property>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">11</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="sub">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="hexpand">True</property>
+        <property name="xalign">0</property>
+        <property name="yalign">0</property>
+      </object>
+      <packing>
+        <property name="left_attach">1</property>
+        <property name="top_attach">11</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="status">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="hexpand">True</property>
+        <property name="wrap">True</property>
+        <property name="max_width_chars">30</property>
+        <property name="xalign">0</property>
+        <property name="yalign">0</property>
+        <attributes>
+          <attribute name="style" value="italic"/>
+        </attributes>
+      </object>
+      <packing>
+        <property name="left_attach">1</property>
+        <property name="top_attach">2</property>
+      </packing>
+    </child>
+    <child>
+      <placeholder/>
+    </child>
+    <child>
+      <placeholder/>
+    </child>
+  </object>
+</interface>
diff --git a/src/gui_interface.py b/src/gui_interface.py
index 28c95bee7d..ac36a64dc0 100644
--- a/src/gui_interface.py
+++ b/src/gui_interface.py
@@ -562,11 +562,14 @@ class Interface:
 
     def handle_event_last_status_time(self, obj):
         # ('LAST_STATUS_TIME', account, (jid, resource, seconds, status))
-        if obj.seconds < 0:
-            # Ann error occured
-            return
         account = obj.conn.name
         c = gajim.contacts.get_contact(account, obj.jid, obj.resource)
+        tooltip_window = self.roster.tree.get_tooltip_window()
+        if obj.seconds < 0:
+            if tooltip_window:
+                tooltip_window.update_last_time(c, True)
+            return
+
         if c: # c can be none if it's a gc contact
             if obj.status:
                 c.status = obj.status
@@ -576,8 +579,10 @@ class Interface:
                 c.last_status_time = last_time
             else:
                 c.last_activity_time = last_time
-            if self.roster.tooltip.id and self.roster.tooltip.win:
-                self.roster.tooltip.update_last_time(last_time)
+
+            # Set last time on roster tooltip
+            if tooltip_window:
+                tooltip_window.update_last_time(c)
 
     def handle_event_gc_config(self, obj):
         #('GC_CONFIG', account, (jid, form_node))  config is a dict
diff --git a/src/roster_window.py b/src/roster_window.py
index 10d3baa7b5..aa8cbfe419 100644
--- a/src/roster_window.py
+++ b/src/roster_window.py
@@ -2063,25 +2063,6 @@ class RosterWindow:
             vb.hide()
             vb.set_no_show_all(True)
 
-    def show_tooltip(self, contact):
-        self.tooltip.timeout = 0
-        device = self.tree.get_window().get_display().get_device_manager().\
-            get_client_pointer()
-        pointer = self.tree.get_window().get_device_position(device)
-        props = self.tree.get_path_at_pos(pointer[1], pointer[2])
-        # check if the current pointer is at the same path
-        # as it was before setting the timeout
-        if props and self.tooltip.id == props[0]:
-            # bounding rectangle of coordinates for the cell within the treeview
-            rect = self.tree.get_cell_area(props[0], props[1])
-
-            # position of the treeview on the screen
-            position = self.tree.get_window().get_origin()
-            self.tooltip.show_tooltip(contact, rect.height, position[2] + \
-                rect.y)
-        else:
-            self.tooltip.hide_tooltip()
-
     def authorize(self, widget, jid, account):
         """
         Authorize a contact (by re-sending auth menuitem)
@@ -2443,7 +2424,6 @@ class RosterWindow:
         if not gajim.config.get('quit_on_roster_x_button') and (
         (gajim.interface.systray_enabled and gajim.config.get('trayicon') != \
         'on_event') or gajim.config.get('allow_hide_roster')):
-            self.tooltip.hide_tooltip()
             if gajim.config.get('save-roster-position'):
                 x, y = self.window.get_position()
                 gajim.config.set('roster_x-position', x)
@@ -2894,119 +2874,6 @@ class RosterWindow:
                 return
             info[contact.jid] = vcard.ZeroconfVcardWindow(contact, account)
 
-    def on_roster_treeview_leave_notify_event(self, widget, event):
-        props = widget.get_path_at_pos(int(event.x), int(event.y))
-        if self.tooltip.timeout > 0 or self.tooltip.shown:
-            if not props or self.tooltip.id == props[0]:
-                self.tooltip.hide_tooltip()
-
-    def on_roster_treeview_motion_notify_event(self, widget, event):
-        model = widget.get_model()
-        props = widget.get_path_at_pos(int(event.x), int(event.y))
-        if self.tooltip.timeout > 0 or self.tooltip.shown:
-            if not props or self.tooltip.id != props[0]:
-                self.tooltip.hide_tooltip()
-        if props:
-            row = props[0]
-            titer = None
-            try:
-                titer = model.get_iter(row)
-            except Exception:
-                self.tooltip.hide_tooltip()
-                return
-            if model[titer][C_TYPE] in ('contact', 'self_contact'):
-                # we're on a contact entry in the roster
-                if (not self.tooltip.shown and self.tooltip.timeout == 0) or \
-                self.tooltip.id != props[0]:
-                    account = model[titer][C_ACCOUNT]
-                    jid = model[titer][C_JID]
-                    self.tooltip.id = row
-                    contacts = gajim.contacts.get_contacts(account, jid)
-                    connected_contacts = []
-                    for c in contacts:
-                        if c.show not in ('offline', 'error'):
-                            connected_contacts.append(c)
-                    if not connected_contacts:
-                        # no connected contacts, show the ofline one
-                        connected_contacts = contacts
-                    self.tooltip.account = account
-                    self.tooltip.timeout = GLib.timeout_add(500,
-                        self.show_tooltip, connected_contacts)
-            elif model[titer][C_TYPE] == 'groupchat':
-                if (not self.tooltip.shown and self.tooltip.timeout == 0) or \
-                self.tooltip.id != props[0]:
-                    account = model[titer][C_ACCOUNT]
-                    jid = model[titer][C_JID]
-                    self.tooltip.id = row
-                    contact = gajim.contacts.get_contacts(account, jid)
-                    self.tooltip.account = account
-                    self.tooltip.timeout = GLib.timeout_add(500,
-                        self.show_tooltip, contact)
-            elif model[titer][C_TYPE] == 'account':
-                # we're on an account entry in the roster
-                if (not self.tooltip.shown and self.tooltip.timeout == 0) or \
-                self.tooltip.id != props[0]:
-                    account = model[titer][C_ACCOUNT]
-                    if account == 'all':
-                        self.tooltip.id = row
-                        self.tooltip.account = None
-                        self.tooltip.timeout = GLib.timeout_add(500,
-                            self.show_tooltip, [])
-                        return
-                    jid = gajim.get_jid_from_account(account)
-                    contacts = []
-                    connection = gajim.connections[account]
-                    # get our current contact info
-
-                    nbr_on, nbr_total = gajim.\
-                        contacts.get_nb_online_total_contacts(
-                        accounts=[account])
-                    account_name = account
-                    if gajim.account_is_connected(account):
-                        account_name += ' (%s/%s)' % (repr(nbr_on),
-                            repr(nbr_total))
-                    contact = gajim.contacts.create_self_contact(jid=jid,
-                        account=account, name=account_name,
-                        show=connection.get_status(), status=connection.status,
-                        resource=connection.server_resource,
-                        priority=connection.priority)
-                    if gajim.connections[account].gpg:
-                        contact.keyID = gajim.config.get_per('accounts',
-                            connection.name, 'keyid')
-                    contacts.append(contact)
-                    # if we're online ...
-                    if connection.connection:
-                        roster = connection.connection.getRoster()
-                        # in threadless connection when no roster stanza is sent
-                        # 'roster' is None
-                        if roster and roster.getItem(jid):
-                            resources = roster.getResources(jid)
-                            # ...get the contact info for our other online
-                            # resources
-                            for resource in resources:
-                                # Check if we already have this resource
-                                found = False
-                                for contact_ in contacts:
-                                    if contact_.resource == resource:
-                                        found = True
-                                        break
-                                if found:
-                                    continue
-                                show = roster.getShow(jid+'/'+resource)
-                                if not show:
-                                    show = 'online'
-                                contact = gajim.contacts.create_self_contact(
-                                    jid=jid, account=account, show=show,
-                                    status=roster.getStatus(
-                                    jid + '/' + resource),
-                                    priority=roster.getPriority(
-                                    jid + '/' + resource), resource=resource)
-                                contacts.append(contact)
-                    self.tooltip.id = row
-                    self.tooltip.account = None
-                    self.tooltip.timeout = GLib.timeout_add(500,
-                        self.show_tooltip, contacts)
-
     def on_agent_logging(self, widget, jid, state, account):
         """
         When an agent is requested to log in or off
@@ -3449,15 +3316,10 @@ class RosterWindow:
     def on_add_to_roster(self, widget, contact, account):
         dialogs.AddNewContactWindow(account, contact.jid, contact.name)
 
-
-    def on_roster_treeview_scroll_event(self, widget, event):
-        self.tooltip.hide_tooltip()
-
     def on_roster_treeview_key_press_event(self, widget, event):
         """
         When a key is pressed in the treeviews
         """
-        self.tooltip.hide_tooltip()
         if event.keyval == Gdk.KEY_Escape:
             if self.rfilter_enabled:
                 self.disable_rfilter()
@@ -3577,8 +3439,6 @@ class RosterWindow:
                 self.enable_rfilter('')
 
     def on_roster_treeview_button_press_event(self, widget, event):
-        # hide tooltip, no matter the button is pressed
-        self.tooltip.hide_tooltip()
         try:
             pos = self.tree.get_path_at_pos(int(event.x), int(event.y))
             path, x = pos[0], pos[2]
@@ -4079,7 +3939,6 @@ class RosterWindow:
             'quit_on_roster_x_button') and ((gajim.interface.systray_enabled and\
             gajim.config.get('trayicon') == 'always') or gajim.config.get(
             'allow_hide_roster')):
-                self.tooltip.hide_tooltip()
                 self.window.hide()
         elif event.get_state() & Gdk.ModifierType.CONTROL_MASK and event.keyval == \
         Gdk.KEY_i:
@@ -6268,6 +6127,54 @@ class RosterWindow:
             renderer.set_property(self.renderers_propertys[renderer][0],
                 self.renderers_propertys[renderer][1])
 
+    def query_tooltip(self, widget, x_pos, y_pos, keyboard_mode, tooltip):
+        try:
+            row = widget.get_path_at_pos(x_pos, y_pos)[0]
+        except TypeError:
+            return False
+        if not row:
+            return False
+
+        iter_ = None
+        try:
+            model = widget.get_model()
+            iter_ = model.get_iter(row)
+        except Exception:
+            return False
+
+        typ = model[iter_][C_TYPE]
+        account = model[iter_][C_ACCOUNT]
+        jid = model[iter_][C_JID]
+        connected_contacts = []
+
+        if typ in ('contact', 'self_contact'):
+            contacts = gajim.contacts.get_contacts(account, jid)
+            
+            for c in contacts:
+                if c.show not in ('offline', 'error'):
+                    connected_contacts.append(c)
+            if not connected_contacts:
+                # no connected contacts, show the offline one
+                connected_contacts = contacts
+        elif typ == 'groupchat':
+            connected_contacts = gajim.contacts.get_contacts(account, jid)
+        elif typ != 'account':
+            return False
+
+        if self.current_tooltip != row:
+            # If the row changes we hide the current tooltip
+            self.current_tooltip = row
+            return False
+
+        tooltip = widget.get_tooltip_window()
+
+        if tooltip.row == row:
+            # We already populated the window with the row data
+            return True
+        tooltip.row = row
+        tooltip.populate(connected_contacts, account, typ)
+        return True
+
 ################################################################################
 ###
 ################################################################################
@@ -6515,7 +6422,10 @@ class RosterWindow:
         self.combobox_callback_active = True
 
         self.collapsed_rows = gajim.config.get('collapsed_rows').split('\t')
-        self.tooltip = tooltips.RosterTooltip()
+        self.tree.set_has_tooltip(True)
+        self.tree.set_tooltip_window(tooltips.RosterTooltip(self.window))
+        self.current_tooltip = None
+        self.tree.connect('query-tooltip', self.query_tooltip)
         # Workaroung: For strange reasons signal is behaving like row-changed
         self._toggeling_row = False
         self.setup_and_draw_roster()
diff --git a/src/tooltips.py b/src/tooltips.py
index 2391557cfb..7dd612e190 100644
--- a/src/tooltips.py
+++ b/src/tooltips.py
@@ -195,28 +195,28 @@ class StatusTable:
     """
 
     def __init__(self):
-        self.current_row = 1
+        self.current_row = 0
         self.table = None
         self.text_label = None
         self.spacer_label = '   '
 
     def create_table(self):
         self.table = Gtk.Grid()
-        self.table.insert_row(0)
-        self.table.insert_row(0)
         self.table.insert_column(0)
         self.table.set_property('column-spacing', 2)
 
-    def add_text_row(self, text, col_inc = 0):
-        self.current_row += 1
+    def add_text_row(self, text, col_inc=0):
+        self.table.insert_row(self.current_row)
         self.text_label = Gtk.Label()
         self.text_label.set_line_wrap(True)
+        self.text_label.set_max_width_chars(35)
         self.text_label.set_halign(Gtk.Align.START)
         self.text_label.set_valign(Gtk.Align.START)
         self.text_label.set_selectable(False)
         self.text_label.set_markup(text)
         self.table.attach(self.text_label, 1 + col_inc, self.current_row,
             3 - col_inc, 1)
+        self.current_row += 1
 
     def get_status_info(self, resource, priority, show, status):
         str_status = resource + ' (' + str(priority) + ')'
@@ -235,9 +235,7 @@ class StatusTable:
         """
         Append a new row with status icon to the table
         """
-        self.table.insert_row(0)
-        self.table.insert_row(0)
-        self.current_row += 1
+        self.table.insert_row(self.current_row)
         state_file = show.replace(' ', '_')
         files = []
         files.append(os.path.join(file_path, state_file + '.png'))
@@ -265,6 +263,7 @@ class StatusTable:
             lock_image.set_from_stock(Gtk.STOCK_DIALOG_AUTHENTICATION,
                 Gtk.IconSize.MENU)
             self.table.attach(lock_image, 4, self.current_row, 1, 1)
+        self.current_row += 1
 
 class NotificationAreaTooltip(BaseTooltip, StatusTable):
     """
@@ -414,83 +413,181 @@ class GCTooltip(Gtk.Window):
             affiliation = formatted % (color, affiliation)
         return affiliation
 
-class RosterTooltip(NotificationAreaTooltip):
-    """
-    Tooltip that is shown in the roster treeview
-    """
+class RosterTooltip(Gtk.Window, StatusTable):
+    # pylint: disable=E1101
+    def __init__(self, parent):
+        Gtk.Window.__init__(self, type=Gtk.WindowType.POPUP, transient_for=parent)
+        StatusTable.__init__(self)
+        self.create_table()
+        self.row = None
+        self.check_last_time = {}
+        self.contact_jid = None
+        self.last_widget = None
+        self.num_resources = 0
+        self.set_title('tooltip')
+        self.set_border_width(3)
+        self.set_resizable(False)
+        self.set_name('gtk-tooltips')
+        self.set_type_hint(Gdk.WindowTypeHint.TOOLTIP)
 
-    def __init__(self):
-        self.account = None
-        self.avatar_image = Gtk.Image()
-        NotificationAreaTooltip.__init__(self)
+        self.xml = gtkgui_helpers.get_gtk_builder('tooltip_roster_contact.ui')
+        for name in ('name', 'status', 'jid', 'user_show', 'fillelement',
+            'resource', 'avatar', 'resource_label', 'pgp', 'pgp_label',
+                'jid_label', 'tooltip_grid', 'idle_since', 'idle_for',
+                'idle_since_label', 'idle_for_label', 'mood', 'tune',
+                'activity', 'location', 'tune_label', 'location_label',
+                'activity_label', 'mood_label', 'sub_label', 'sub',
+                'status_label'):
+            setattr(self, name, self.xml.get_object(name))
 
-    def populate(self, contacts):
-        self.create_window()
+        self.add(self.tooltip_grid)
+        self.tooltip_grid.show()
 
-        self.create_table()
-        if not contacts or len(contacts) == 0:
+    def clear_tooltip(self):
+        """
+        Hide all Elements of the Tooltip Grid
+        """
+        for child in self.tooltip_grid.get_children():
+            child.hide()
+        status_table = self.tooltip_grid.get_child_at(0, 3)
+        if status_table:
+            status_table.destroy()
+            self.create_table()
+
+    def fill_table_with_accounts(self, accounts):
+        iconset = gajim.config.get('iconset')
+        if not iconset:
+            iconset = 'dcraven'
+        file_path = os.path.join(helpers.get_iconset_path(iconset), '16x16')
+        for acct in accounts:
+            message = acct['message']
+            message = helpers.reduce_chars_newlines(message, 100, 1)
+            message = GLib.markup_escape_text(message)
+            if acct['name'] in gajim.con_types and \
+                    gajim.con_types[acct['name']] in ('tls', 'ssl'):
+                show_lock = True
+            else:
+                show_lock = False
+            if message:
+                self.add_status_row(file_path, acct['show'],
+                    GLib.markup_escape_text(acct['name']) + ' - ' + message,
+                    show_lock=show_lock, indent=False)
+            else:
+                self.add_status_row(file_path, acct['show'],
+                    GLib.markup_escape_text(acct['name']), show_lock=show_lock,
+                    indent=False)
+            for line in acct['event_lines']:
+                self.add_text_row('  ' + line, 1)
+
+    def populate(self, contacts, account, typ):
+        """
+        Populate the Tooltip Grid with data of from the contact
+        """
+        self.current_row = 0
+        self.account = account
+        if self.last_widget:
+            self.last_widget.set_vexpand(False)
+
+        self.clear_tooltip()
+
+        if account == 'all':
             # Tooltip for merged accounts row
             accounts = helpers.get_notification_icon_tooltip_dict()
             self.spacer_label = ''
             self.fill_table_with_accounts(accounts)
-            self.win.add(self.table)
+            self.tooltip_grid.attach(self.table, 0, 3, 2, 1)
+            self.table.show_all()
             return
 
-        # primary contact
-        prim_contact = gajim.contacts.get_highest_prio_contact_from_contacts(
+        if typ == 'account':
+            jid = gajim.get_jid_from_account(account)
+            contacts = []
+            connection = gajim.connections[account]
+            # get our current contact info
+
+            nbr_on, nbr_total = gajim.\
+                contacts.get_nb_online_total_contacts(
+                accounts=[account])
+            account_name = account
+            if gajim.account_is_connected(account):
+                account_name += ' (%s/%s)' % (repr(nbr_on),
+                    repr(nbr_total))
+            contact = gajim.contacts.create_self_contact(jid=jid,
+                account=account, name=account_name,
+                show=connection.get_status(), status=connection.status,
+                resource=connection.server_resource,
+                priority=connection.priority)
+            if gajim.connections[account].gpg:
+                contact.keyID = gajim.config.get_per('accounts',
+                    connection.name, 'keyid')
+            contacts.append(contact)
+            # if we're online ...
+            if connection.connection:
+                roster = connection.connection.getRoster()
+                # in threadless connection when no roster stanza is sent
+                # 'roster' is None
+                if roster and roster.getItem(jid):
+                    resources = roster.getResources(jid)
+                    # ...get the contact info for our other online
+                    # resources
+                    for resource in resources:
+                        # Check if we already have this resource
+                        found = False
+                        for contact_ in contacts:
+                            if contact_.resource == resource:
+                                found = True
+                                break
+                        if found:
+                            continue
+                        show = roster.getShow(jid + '/' + resource)
+                        if not show:
+                            show = 'online'
+                        contact = gajim.contacts.create_self_contact(
+                            jid=jid, account=account, show=show,
+                            status=roster.getStatus(
+                            jid + '/' + resource),
+                            priority=roster.getPriority(
+                            jid + '/' + resource), resource=resource)
+                        contacts.append(contact)
+
+        # Username/Account/Groupchat
+        self.prim_contact = gajim.contacts.get_highest_prio_contact_from_contacts(
             contacts)
-
-        puny_jid = helpers.sanitize_filename(prim_contact.jid)
-        table_size = 3
-
-        file_ = helpers.get_avatar_path(os.path.join(gajim.AVATAR_PATH,
-            puny_jid))
-        if file_:
-            with open(file_, 'rb') as file_data:
-                pix = gtkgui_helpers.get_pixbuf_from_data(file_data.read())
-            pix = gtkgui_helpers.get_scaled_pixbuf(pix, 'tooltip')
-            self.avatar_image.set_from_pixbuf(pix)
-            table_size = 4
-        else:
-            self.avatar_image.set_from_pixbuf(None)
-        vcard_table = Gtk.Grid()
-        vcard_table.insert_row(0)
-        for i in range(0, table_size):
-            vcard_table.insert_column(0)
-        vcard_table.set_property('column-spacing', 2)
-        vcard_current_row = 1
-        properties = []
-
-        name_markup = '<span weight="bold">' + GLib.markup_escape_text(
-            prim_contact.get_shown_name()) + '</span>'
+        self.contact_jid = self.prim_contact.jid
+        name = GLib.markup_escape_text(self.prim_contact.get_shown_name())
+        name_markup = '<b>{}</b>'.format(name)
         if gajim.config.get('mergeaccounts'):
-            name_markup += " <span foreground='%s'>(%s)</span>" % (
-                gajim.config.get('tooltip_account_name_color'),
-                GLib.markup_escape_text(prim_contact.account.name))
+            color = gajim.config.get('tooltip_account_name_color')
+            account_name = GLib.markup_escape_text(self.prim_contact.account.name)
+            name_markup += " <span foreground='{}'>({})</span>".format(
+                color, account_name)
 
-        if self.account and helpers.jid_is_blocked(self.account,
-        prim_contact.jid):
+        if account and helpers.jid_is_blocked(account, self.prim_contact.jid):
             name_markup += _(' [blocked]')
-        if self.account and \
-        self.account in gajim.interface.minimized_controls and \
-        prim_contact.jid in gajim.interface.minimized_controls[self.account]:
-            name_markup += _(' [minimized]')
-        properties.append((name_markup, None))
 
-        num_resources = 0
+        try:
+            if self.prim_contact.jid in gajim.interface.minimized_controls[account]:
+                name_markup += _(' [minimized]')
+        except KeyError:
+            pass
+
+        self.name.set_markup(name_markup)
+        self.name.show()
+
+        self.num_resources = 0
         # put contacts in dict, where key is priority
         contacts_dict = {}
         for contact in contacts:
             if contact.resource:
-                num_resources += 1
-                if contact.priority in contacts_dict:
-                    contacts_dict[int(contact.priority)].append(contact)
+                self.num_resources += 1
+                priority = int(contact.priority)
+                if priority in contacts_dict:
+                    contacts_dict[priority].append(contact)
                 else:
-                    contacts_dict[int(contact.priority)] = [contact]
-
-        if num_resources > 1:
-            properties.append((_('Status: '),       ' '))
-            transport = gajim.get_transport_name_from_jid(prim_contact.jid)
+                    contacts_dict[priority] = [contact]
+        if self.num_resources > 1:
+            self.status_label.show()
+            transport = gajim.get_transport_name_from_jid(self.prim_contact.jid)
             if transport:
                 file_path = os.path.join(helpers.get_transport_path(transport),
                     '16x16')
@@ -505,98 +602,140 @@ class RosterTooltip(NotificationAreaTooltip):
             contact_keys.reverse()
             for priority in contact_keys:
                 for acontact in contacts_dict[priority]:
-                    status_line = self.get_status_info(acontact.resource,
-                        acontact.priority, acontact.show, acontact.status)
-
                     icon_name = self._get_icon_name_for_tooltip(acontact)
+                    if acontact.status and len(acontact.status) > 25:
+                        status = ''
+                        add_text = True
+                    else:
+                        status = acontact.status
+                        add_text = False
+
+                    status_line = self.get_status_info(acontact.resource,
+                    acontact.priority, acontact.show, status)
                     self.add_status_row(file_path, icon_name, status_line,
                         acontact.last_status_time)
-            properties.append((self.table,  None))
+                    if add_text:
+                        self.add_text_row(acontact.status, 2)
+
+            self.tooltip_grid.attach(self.table, 0, 3, 2, 1)
+            self.table.show_all()
 
-        else: # only one resource
+        else:  # only one resource
             if contact.show:
-                show = helpers.get_uf_show(contact.show)
-                if not self.check_last_time and self.account:
+                request_time = False
+                try:
+                    last_time = self.check_last_time[contact]
+                    if isinstance(last_time, float) and last_time < time.time() - 60:
+                        request_time = True
+                except KeyError:
+                    request_time = True
+
+                if request_time:
                     if contact.show == 'offline':
-                        if not contact.last_status_time:
-                            gajim.connections[self.account].\
-                                request_last_status_time(contact.jid, '')
-                        else:
-                            self.check_last_time = contact.last_status_time
+                        gajim.connections[account].\
+                            request_last_status_time(contact.jid, '')
                     elif contact.resource:
-                        gajim.connections[self.account].\
+                        gajim.connections[account].\
                             request_last_status_time(
                             contact.jid, contact.resource)
-                        if contact.last_activity_time:
-                            self.check_last_time = contact.last_activity_time
-                else:
-                    self.check_last_time = None
-                if contact.last_status_time:
-                    vcard_current_row += 1
-                    if contact.show == 'offline':
-                        text = ' - ' + _('Last status: %s')
-                    else:
-                        text = _(' since %s')
-
-                    if time.strftime('%j', time.localtime()) == \
-                        time.strftime('%j', contact.last_status_time):
-                        # it's today, show only the locale hour representation
-                        local_time = time.strftime('%X',
-                            contact.last_status_time)
-                    else:
-                        # time.strftime returns locale encoded string
-                        local_time = time.strftime('%c',
-                            contact.last_status_time)
-
-                    text = text % local_time
-                    show += text
-                if self.account and \
-                prim_contact.jid in gajim.gc_connected[self.account]:
-                    if gajim.gc_connected[self.account][prim_contact.jid]:
-                        show = _('Connected')
-                    else:
-                        show = _('Disconnected')
-                show = colorize_status(show)
+                    self.check_last_time[contact] = time.time()
 
                 if contact.status:
                     status = contact.status.strip()
                     if status:
-                        # reduce long status
-                        # (no more than 300 chars on line and no more than
-                        # 5 lines)
-                        # status is wrapped
-                        status = helpers.reduce_chars_newlines(status, 300, 5)
-                        # escape markup entities.
-                        status = GLib.markup_escape_text(status)
-                        properties.append(('<i>%s</i>' % status, None))
-                properties.append((show, None))
+                        self.status.set_text(status)
+                        self.status.show()
+                        self.status_label.show()
 
-        self._append_pep_info(contact, properties)
+        # PEP Info
+        self._append_pep_info(contact)
 
-        properties.append((_('Jabber ID: '), '\u200E' + "<b>%s</b>" % \
-            prim_contact.jid))
+        # JID
+        self.jid.set_text(self.prim_contact.jid)
+        self.jid.show()
+        self.jid_label.show()
 
         # contact has only one ressource
-        if num_resources == 1 and contact.resource:
-            properties.append((_('Resource: '), GLib.markup_escape_text(
-                contact.resource) + ' (' + str(contact.priority) + ')'))
+        if self.num_resources == 1 and contact.resource:
+            res = GLib.markup_escape_text(contact.resource)
+            prio = str(contact.priority)
+            self.resource.set_text("{} ({})".format(res, prio))
+            self.resource.show()
+            self.resource_label.show()
 
-        if self.account and prim_contact.sub and prim_contact.sub != 'both' and\
-        prim_contact.jid not in gajim.gc_connected[self.account]:
-            # ('both' is the normal sub so we don't show it)
-            properties.append(( _('Subscription: '), GLib.markup_escape_text(
-                helpers.get_uf_sub(prim_contact.sub))))
+        if self.prim_contact.jid not in gajim.gc_connected[account]:
+            if (account and
+                self.prim_contact.sub and
+                    self.prim_contact.sub != 'both'):
+                # ('both' is the normal sub so we don't show it)
+                self.sub.set_text(helpers.get_uf_sub(self.prim_contact.sub))
+                self.sub.show()
+                self.sub_label.show()
 
-        if prim_contact.keyID:
+        if self.prim_contact.keyID:
             keyID = None
-            if len(prim_contact.keyID) == 8:
-                keyID = prim_contact.keyID
-            elif len(prim_contact.keyID) == 16:
-                keyID = prim_contact.keyID[8:]
+            if len(self.prim_contact.keyID) == 8:
+                keyID = self.prim_contact.keyID
+            elif len(self.prim_contact.keyID) == 16:
+                keyID = self.prim_contact.keyID[8:]
             if keyID:
-                properties.append((_('OpenPGP: '), GLib.markup_escape_text(
-                    keyID)))
+                self.pgp.set_text(keyID)
+                self.pgp.show()
+                self.pgp_label.show()
+
+        self._set_idle_time(contact)
+
+        # Avatar
+        puny_jid = helpers.sanitize_filename(self.prim_contact.jid)
+        file_ = helpers.get_avatar_path(os.path.join(gajim.AVATAR_PATH,
+            puny_jid))
+        if file_:
+            with open(file_, 'rb') as file_data:
+                pix = gtkgui_helpers.get_pixbuf_from_data(file_data.read())
+            pix = gtkgui_helpers.get_scaled_pixbuf(pix, 'tooltip')
+            self.avatar.set_from_pixbuf(pix)
+            self.avatar.show()
+
+            # Sets the Widget that is at the bottom to expand.
+            # This is needed in case the Picture takes more Space then the Labels
+            i = 1
+            while i < 15:
+                if self.tooltip_grid.get_child_at(0, i):
+                    if self.tooltip_grid.get_child_at(0, i).get_visible():
+                        self.last_widget = self.tooltip_grid.get_child_at(0, i)
+                i += 1
+            self.last_widget.set_vexpand(True)
+
+    def _append_pep_info(self, contact):
+        """
+        Append Tune, Mood, Activity, Location information of the specified contact
+        to the given property list.
+        """
+        if 'mood' in contact.pep:
+            mood = contact.pep['mood'].asMarkupText()
+            self.mood.set_markup(mood)
+            self.mood.show()
+            self.mood_label.show()
+
+        if 'activity' in contact.pep:
+            activity = contact.pep['activity'].asMarkupText()
+            self.activity.set_markup(activity)
+            self.activity.show()
+            self.activity_label.show()
+
+        if 'tune' in contact.pep:
+            tune = contact.pep['tune'].asMarkupText()
+            self.tune.set_markup(tune)
+            self.tune.show()
+            self.tune_label.show()
+
+        if 'location' in contact.pep:
+            location = contact.pep['location'].asMarkupText()
+            self.location.set_markup(location)
+            self.location.show()
+            self.location_label.show()
 
+    def _set_idle_time(self, contact):
         if contact.last_activity_time:
             last_active = datetime(*contact.last_activity_time[:6])
             current = datetime.now()
@@ -613,78 +752,67 @@ class RosterTooltip(NotificationAreaTooltip):
             # is no meaningful difference between last activity time and
             # current time.
             if diff.days > 0 or diff.seconds > 0:
-                cs = "<span foreground='%s'>" % gajim.config.get(
-                    'tooltip_idle_color')
-                cs += '%s</span>'
-                properties.append((str(), None))
-                idle_since = cs % _("Idle since %s")
-                properties.append((idle_since % formatted, None))
-                idle_for = cs % _("Idle for %s")
-                properties.append((idle_for % str(diff), None))
+                idle_color = gajim.config.get('tooltip_idle_color')
+                idle_markup = "<span foreground='{}'>{}</span>".format(idle_color, formatted)
+                self.idle_since.set_markup(idle_markup)
+                self.idle_since.show()
+                self.idle_since_label.show()
+                idle_markup = "<span foreground='{}'>{}</span>".format(idle_color, str(diff))
+                self.idle_for.set_markup(idle_markup)
+                self.idle_for_label.show()
+                self.idle_for.show()
+
+        if contact.show and self.num_resources < 2:
+            show = helpers.get_uf_show(contact.show)
+            if contact.last_status_time:
+                if contact.show == 'offline':
+                    text = ' - ' + _('Last status: %s')
+                else:
+                    text = _(' since %s')
 
-        while properties:
-            property_ = properties.pop(0)
-            vcard_current_row += 1
-            label = Gtk.Label()
-            if not properties and table_size == 4:
-                label.set_vexpand(True)
-            label.set_halign(Gtk.Align.START)
-            label.set_valign(Gtk.Align.START)
-            if property_[1]:
-                label.set_markup(property_[0])
-                vcard_table.attach(label, 1, vcard_current_row, 1, 1)
-                label = Gtk.Label()
-                if not properties and table_size == 4:
-                    label.set_vexpand(True)
-                label.set_halign(Gtk.Align.START)
-                label.set_valign(Gtk.Align.START)
-                label.set_markup(property_[1])
-                label.set_line_wrap(True)
-                vcard_table.attach(label, 2, vcard_current_row, 1, 1)
-            else:
-                if isinstance(property_[0], str):
-                    label.set_markup(property_[0])
-                    label.set_line_wrap(True)
+                if time.strftime('%j', time.localtime()) == \
+                        time.strftime('%j', contact.last_status_time):
+                    # it's today, show only the locale hour representation
+                    local_time = time.strftime('%X', contact.last_status_time)
                 else:
-                    label = property_[0]
-                vcard_table.attach(label, 1, vcard_current_row, 2, 1)
-        self.avatar_image.set_halign(Gtk.Align.START)
-        self.avatar_image.set_valign(Gtk.Align.START)
-        if table_size == 4:
-            vcard_table.attach(self.avatar_image, 3, 2, 1, vcard_current_row - 1)
-
-        gajim.plugin_manager.gui_extension_point('roster_tooltip_populate',
-            self, contacts, vcard_table)
-        self.win.add(vcard_table)
-
-    def update_last_time(self, last_time):
-        if not self.check_last_time or time.strftime('%x %I:%M %p', last_time) !=\
-        time.strftime('%x %I:%M %p', self.check_last_time):
-            self.win.destroy()
-            self.win = None
-            self.populate(self.cur_data)
-            self.win.show_all()
+                    # time.strftime returns locale encoded string
+                    local_time = time.strftime('%c', contact.last_status_time)
 
-    def _append_pep_info(self, contact, properties):
-        """
-        Append Tune, Mood, Activity, Location information of the specified contact
-        to the given property list.
-        """
-        if 'mood' in contact.pep:
-            mood = contact.pep['mood'].asMarkupText()
-            properties.append((_('Mood: '), "%s" % mood, None))
+                text = text % local_time
+                show += text
 
-        if 'activity' in contact.pep:
-            activity = contact.pep['activity'].asMarkupText()
-            properties.append((_('Activity: '), "%s" % activity, None))
+            # Contact is Groupchat
+            if (self.account and
+                    self.prim_contact.jid in gajim.gc_connected[self.account]):
+                if gajim.gc_connected[self.account][self.prim_contact.jid]:
+                    show = _('Connected')
+                else:
+                    show = _('Disconnected')
 
-        if 'tune' in contact.pep:
-            tune = contact.pep['tune'].asMarkupText()
-            properties.append((_('Tune: '), "%s" % tune, None))
+            self.user_show.set_markup(colorize_status(show))
+            self.user_show.show()
 
-        if 'location' in contact.pep:
-            location = contact.pep['location'].asMarkupText()
-            properties.append((_('Location: '), "%s" % location, None))
+    def _get_icon_name_for_tooltip(self, contact):
+        """
+        Helper function used for tooltip contacts/acounts
+
+        Tooltip on account has fake contact with sub == '', in this case we show
+        real status of the account
+        """
+        if contact.ask == 'subscribe':
+            return 'requested'
+        elif contact.sub in ('both', 'to', ''):
+            return contact.show
+        return 'not in roster'
+
+    def update_last_time(self, contact, error=False):
+        if not contact:
+            return
+        if error:
+            self.check_last_time[contact] = 'error'
+            return
+        if contact.jid == self.contact_jid:
+            self._set_idle_time(contact)
 
 
 class FileTransfersTooltip(BaseTooltip):
-- 
GitLab