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