1 /* ide-hover-popover.c
2  *
3  * Copyright 2018-2019 Christian Hergert <chergert@redhat.com>
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
17  *
18  * SPDX-License-Identifier: GPL-3.0-or-later
19  */
20 
21 #define G_LOG_DOMAIN "ide-hover-popover"
22 
23 #include "config.h"
24 
25 #include <dazzle.h>
26 #include <libide-code.h>
27 #include <libide-gui.h>
28 #include <string.h>
29 
30 #include "ide-hover-context-private.h"
31 #include "ide-hover-popover-private.h"
32 
33 struct _IdeHoverPopover
34 {
35   GtkPopover parent_instance;
36 
37   /*
38    * A vertical box containing all of our marked content/widgets that
39    * were provided by the context.
40    */
41   GtkBox *box;
42 
43   /*
44    * Our context to be observed. As items are added to the context,
45    * we add them to the popver (creating or re-using the widget) based
46    * on the kind of content.
47    */
48   IdeHoverContext *context;
49 
50   /*
51    * This is our cancellable to cancel any in-flight requests to the
52    * hover providers when the popover is withdrawn. That could happen
53    * before we've even really been displayed to the user.
54    */
55   GCancellable *cancellable;
56 
57   /*
58    * The position where the hover operation began, in buffer coordinates.
59    */
60   GdkRectangle hovered_at;
61 
62   /*
63    * If we've had any providers added, so that we can short-circuit
64    * in that case without having to display the popover.
65    */
66   guint has_providers : 1;
67 };
68 
69 enum {
70   PROP_0,
71   PROP_CONTEXT,
72   PROP_HOVERED_AT,
73   N_PROPS
74 };
75 
G_DEFINE_FINAL_TYPE(IdeHoverPopover,ide_hover_popover,GTK_TYPE_POPOVER)76 G_DEFINE_FINAL_TYPE (IdeHoverPopover, ide_hover_popover, GTK_TYPE_POPOVER)
77 
78 static GParamSpec *properties [N_PROPS];
79 
80 static void
81 ide_hover_popover_add_content (const gchar      *title,
82                                IdeMarkedContent *content,
83                                GtkWidget        *widget,
84                                gpointer          user_data)
85 {
86   IdeHoverPopover *self = user_data;
87   GtkBox *box;
88 
89   g_assert (content != NULL || widget != NULL);
90   g_assert (!widget || GTK_IS_WIDGET (widget));
91 
92   box = g_object_new (GTK_TYPE_BOX,
93                       "orientation", GTK_ORIENTATION_VERTICAL,
94                       "visible", TRUE,
95                       NULL);
96   gtk_container_add (GTK_CONTAINER (self->box), GTK_WIDGET (box));
97   dzl_gtk_widget_add_style_class (GTK_WIDGET (box), "hoverer-box");
98 
99   if (!dzl_str_empty0 (title))
100     {
101       GtkWidget *label;
102 
103       label = g_object_new (GTK_TYPE_LABEL,
104                             "xalign", 0.0f,
105                             "label", title,
106                             "use-markup", FALSE,
107                             "visible", TRUE,
108                             NULL);
109       dzl_gtk_widget_add_style_class (label, "title");
110       gtk_container_add (GTK_CONTAINER (box), label);
111     }
112 
113   if (content != NULL)
114     {
115       GtkWidget *view = ide_marked_view_new (content);
116 
117       if (view != NULL)
118         {
119           gtk_container_add (GTK_CONTAINER (box), view);
120           gtk_widget_show (view);
121         }
122     }
123 
124   if (widget != NULL)
125     {
126       gtk_container_add (GTK_CONTAINER (box), widget);
127       gtk_widget_show (widget);
128     }
129 }
130 
131 static void
ide_hover_popover_query_cb(GObject * object,GAsyncResult * result,gpointer user_data)132 ide_hover_popover_query_cb (GObject      *object,
133                             GAsyncResult *result,
134                             gpointer      user_data)
135 {
136   IdeHoverContext *context = (IdeHoverContext *)object;
137   g_autoptr(IdeHoverPopover) self = user_data;
138   g_autoptr(GError) error = NULL;
139 
140   g_assert (IDE_IS_HOVER_CONTEXT (context));
141   g_assert (G_IS_ASYNC_RESULT (result));
142   g_assert (IDE_IS_HOVER_POPOVER (self));
143 
144   if (!_ide_hover_context_query_finish (context, result, &error) ||
145       !ide_hover_context_has_content (context))
146     {
147       gtk_widget_destroy (GTK_WIDGET (self));
148       return;
149     }
150 
151   _ide_hover_context_foreach (context,
152                               ide_hover_popover_add_content,
153                               self);
154 
155   gtk_widget_show (GTK_WIDGET (self));
156 }
157 
158 static void
ide_hover_popover_get_preferred_height(GtkWidget * widget,gint * min_height,gint * nat_height)159 ide_hover_popover_get_preferred_height (GtkWidget *widget,
160                                         gint      *min_height,
161                                         gint      *nat_height)
162 {
163   g_assert (IDE_IS_HOVER_POPOVER (widget));
164   g_assert (min_height != NULL);
165   g_assert (nat_height != NULL);
166 
167   GTK_WIDGET_CLASS (ide_hover_popover_parent_class)->get_preferred_height (widget, min_height, nat_height);
168 
169   /*
170    * If we have embedded webkit views, they can get some bogus size requests
171    * sometimes. So try to detect that and prevent giant popovers.
172    */
173 
174   if (*nat_height > 1024)
175     *nat_height = *min_height;
176 }
177 
178 void
_ide_hover_popover_set_hovered_at(IdeHoverPopover * self,const GdkRectangle * hovered_at)179 _ide_hover_popover_set_hovered_at (IdeHoverPopover    *self,
180                                    const GdkRectangle *hovered_at)
181 {
182   g_assert (IDE_IS_HOVER_POPOVER (self));
183 
184   if (hovered_at != NULL)
185     self->hovered_at = *hovered_at;
186   else
187     memset (&self->hovered_at, 0, sizeof self->hovered_at);
188 }
189 
190 static void
ide_hover_popover_destroy(GtkWidget * widget)191 ide_hover_popover_destroy (GtkWidget *widget)
192 {
193   IdeHoverPopover *self = (IdeHoverPopover *)widget;
194 
195   g_cancellable_cancel (self->cancellable);
196 
197   g_clear_object (&self->context);
198   g_clear_object (&self->cancellable);
199 
200   GTK_WIDGET_CLASS (ide_hover_popover_parent_class)->destroy (widget);
201 }
202 
203 static void
ide_hover_popover_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)204 ide_hover_popover_get_property (GObject    *object,
205                                 guint       prop_id,
206                                 GValue     *value,
207                                 GParamSpec *pspec)
208 {
209   IdeHoverPopover *self = IDE_HOVER_POPOVER (object);
210 
211   switch (prop_id)
212     {
213     case PROP_CONTEXT:
214       g_value_set_object (value, _ide_hover_popover_get_context (self));
215       break;
216 
217     case PROP_HOVERED_AT:
218       g_value_set_boxed (value, &self->hovered_at);
219       break;
220 
221     default:
222       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
223     }
224 }
225 
226 static void
ide_hover_popover_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)227 ide_hover_popover_set_property (GObject      *object,
228                                 guint         prop_id,
229                                 const GValue *value,
230                                 GParamSpec   *pspec)
231 {
232   IdeHoverPopover *self = IDE_HOVER_POPOVER (object);
233 
234   switch (prop_id)
235     {
236     case PROP_HOVERED_AT:
237       _ide_hover_popover_set_hovered_at (self, g_value_get_boxed (value));
238       break;
239 
240     default:
241       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
242     }
243 }
244 
245 static void
ide_hover_popover_class_init(IdeHoverPopoverClass * klass)246 ide_hover_popover_class_init (IdeHoverPopoverClass *klass)
247 {
248   GObjectClass *object_class = G_OBJECT_CLASS (klass);
249   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
250 
251   object_class->get_property = ide_hover_popover_get_property;
252   object_class->set_property = ide_hover_popover_set_property;
253 
254   widget_class->destroy = ide_hover_popover_destroy;
255   widget_class->get_preferred_height = ide_hover_popover_get_preferred_height;
256 
257   properties [PROP_CONTEXT] =
258     g_param_spec_object ("context",
259                          "Context",
260                          "The hover context to display to the user",
261                          IDE_TYPE_HOVER_CONTEXT,
262                          (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
263 
264   properties [PROP_HOVERED_AT] =
265     g_param_spec_boxed ("hovered-at",
266                          "Hovered At",
267                          "The position that the hover originated in buffer coordinates",
268                          GDK_TYPE_RECTANGLE,
269                          (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
270 
271   g_object_class_install_properties (object_class, N_PROPS, properties);
272 }
273 
274 static void
ide_hover_popover_init(IdeHoverPopover * self)275 ide_hover_popover_init (IdeHoverPopover *self)
276 {
277   GtkStyleContext *style_context;
278 
279   self->context = g_object_new (IDE_TYPE_HOVER_CONTEXT, NULL);
280   self->cancellable = g_cancellable_new ();
281 
282   style_context = gtk_widget_get_style_context (GTK_WIDGET (self));
283   gtk_style_context_add_class (style_context, "hoverer");
284 
285   self->box = g_object_new (GTK_TYPE_BOX,
286                             "orientation", GTK_ORIENTATION_VERTICAL,
287                             "visible", TRUE,
288                             NULL);
289   gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (self->box));
290 }
291 
292 IdeHoverContext *
_ide_hover_popover_get_context(IdeHoverPopover * self)293 _ide_hover_popover_get_context (IdeHoverPopover *self)
294 {
295   g_return_val_if_fail (IDE_IS_HOVER_POPOVER (self), NULL);
296 
297   return self->context;
298 }
299 
300 void
_ide_hover_popover_add_provider(IdeHoverPopover * self,IdeHoverProvider * provider)301 _ide_hover_popover_add_provider (IdeHoverPopover  *self,
302                                  IdeHoverProvider *provider)
303 {
304   g_return_if_fail (IDE_IS_HOVER_POPOVER (self));
305   g_return_if_fail (IDE_IS_HOVER_PROVIDER (provider));
306 
307   _ide_hover_context_add_provider (self->context, provider);
308 
309   self->has_providers = TRUE;
310 }
311 
312 void
_ide_hover_popover_show(IdeHoverPopover * self)313 _ide_hover_popover_show (IdeHoverPopover *self)
314 {
315   GtkWidget *view;
316 
317   g_return_if_fail (IDE_IS_HOVER_POPOVER (self));
318   g_return_if_fail (self->context != NULL);
319 
320   if (self->has_providers &&
321       !g_cancellable_is_cancelled (self->cancellable) &&
322       (view = gtk_popover_get_relative_to (GTK_POPOVER (self))) &&
323       GTK_IS_TEXT_VIEW (view))
324     {
325       GtkTextIter iter;
326 
327       /* hovered_at is in buffer coordinates */
328       gtk_text_view_get_iter_at_location (GTK_TEXT_VIEW (view),
329                                           &iter,
330                                           self->hovered_at.x,
331                                           self->hovered_at.y);
332 
333       _ide_hover_context_query_async (self->context,
334                                       &iter,
335                                       self->cancellable,
336                                       ide_hover_popover_query_cb,
337                                       g_object_ref (self));
338 
339       return;
340     }
341 
342   /* Cancel this popover immediately, we have nothing to do */
343   gtk_widget_destroy (GTK_WIDGET (self));
344 }
345 
346 void
_ide_hover_popover_hide(IdeHoverPopover * self)347 _ide_hover_popover_hide (IdeHoverPopover *self)
348 {
349   g_return_if_fail (IDE_IS_HOVER_POPOVER (self));
350 
351   gtk_widget_destroy (GTK_WIDGET (self));
352 }
353