1 | /* |
2 | * Copyright © 2021 Benjamin Otte |
3 | * |
4 | * This library is free software; you can redistribute it and/or |
5 | * modify it under the terms of the GNU Lesser General Public |
6 | * License as published by the Free Software Foundation; either |
7 | * version 2.1 of the License, or (at your option) any later version. |
8 | * |
9 | * This library is distributed in the hope that it will be useful, |
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
12 | * Lesser General Public License for more details. |
13 | * |
14 | * You should have received a copy of the GNU Lesser General Public |
15 | * License along with this library. If not, see <http://www.gnu.org/licenses/>. |
16 | * |
17 | * Authors: Benjamin Otte <otte@gnome.org> |
18 | */ |
19 | |
20 | #include "config.h" |
21 | |
22 | #include "gtkdataviewer.h" |
23 | |
24 | #include "gtkbinlayout.h" |
25 | #include "gtklabel.h" |
26 | #include "gtkpicture.h" |
27 | #include "gtkcolorswatchprivate.h" |
28 | #include "gtkbox.h" |
29 | |
30 | |
31 | struct _GtkDataViewer |
32 | { |
33 | GtkWidget parent_instance; |
34 | |
35 | GtkWidget *contents; |
36 | GCancellable *cancellable; |
37 | GError *error; |
38 | |
39 | enum { |
40 | NOT_LOADED = 0, |
41 | LOADING_DONE, |
42 | LOADING_EXTERNALLY, |
43 | LOADING_INTERNALLY, |
44 | LOADING_FAILED |
45 | } loading; |
46 | }; |
47 | |
48 | enum |
49 | { |
50 | PROP_0, |
51 | PROP_LOADING, |
52 | |
53 | N_PROPS |
54 | }; |
55 | |
56 | enum { |
57 | LOAD, |
58 | LAST_SIGNAL |
59 | }; |
60 | |
61 | G_DEFINE_TYPE (GtkDataViewer, gtk_data_viewer, GTK_TYPE_WIDGET) |
62 | |
63 | static GParamSpec *properties[N_PROPS] = { NULL, }; |
64 | static guint signals[LAST_SIGNAL]; |
65 | |
66 | static void |
67 | gtk_data_viewer_ensure_loaded (GtkDataViewer *self) |
68 | { |
69 | gboolean started_loading; |
70 | |
71 | if (self->loading != NOT_LOADED) |
72 | return; |
73 | |
74 | self->loading = LOADING_EXTERNALLY; |
75 | self->cancellable = g_cancellable_new (); |
76 | g_signal_emit (instance: self, signal_id: signals[LOAD], detail: 0, self->cancellable, &started_loading); |
77 | |
78 | if (!started_loading) |
79 | { |
80 | self->loading = LOADING_FAILED; /* avoid notify::is_loading */ |
81 | gtk_data_viewer_load_error (self, error: g_error_new (G_IO_ERROR, code: G_IO_ERROR_FAILED, format: "Nothing to load" )); |
82 | } |
83 | |
84 | g_assert (self->loading != NOT_LOADED); |
85 | |
86 | if (gtk_data_viewer_is_loading (self)) |
87 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_LOADING]); |
88 | } |
89 | |
90 | static void |
91 | gtk_data_viewer_realize (GtkWidget *widget) |
92 | { |
93 | GtkDataViewer *self = GTK_DATA_VIEWER (ptr: widget); |
94 | |
95 | GTK_WIDGET_CLASS (gtk_data_viewer_parent_class)->realize (widget); |
96 | |
97 | gtk_data_viewer_ensure_loaded (self); |
98 | } |
99 | |
100 | static void |
101 | gtk_data_viewer_unrealize (GtkWidget *widget) |
102 | { |
103 | GtkDataViewer *self = GTK_DATA_VIEWER (ptr: widget); |
104 | |
105 | GTK_WIDGET_CLASS (gtk_data_viewer_parent_class)->unrealize (widget); |
106 | |
107 | gtk_data_viewer_reset (self); |
108 | } |
109 | |
110 | static void |
111 | gtk_data_viewer_dispose (GObject *object) |
112 | { |
113 | //GtkDataViewer *self = GTK_DATA_VIEWER (object); |
114 | |
115 | G_OBJECT_CLASS (gtk_data_viewer_parent_class)->dispose (object); |
116 | } |
117 | |
118 | static void |
119 | gtk_data_viewer_get_property (GObject *object, |
120 | guint property_id, |
121 | GValue *value, |
122 | GParamSpec *pspec) |
123 | { |
124 | GtkDataViewer *self = GTK_DATA_VIEWER (ptr: object); |
125 | |
126 | switch (property_id) |
127 | { |
128 | case PROP_LOADING: |
129 | g_value_set_boolean (value, v_boolean: gtk_data_viewer_is_loading (self)); |
130 | break; |
131 | |
132 | default: |
133 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); |
134 | break; |
135 | } |
136 | } |
137 | |
138 | static void |
139 | gtk_data_viewer_set_property (GObject *object, |
140 | guint property_id, |
141 | const GValue *value, |
142 | GParamSpec *pspec) |
143 | { |
144 | //GtkDataViewer *self = GTK_DATA_VIEWER (object); |
145 | |
146 | switch (property_id) |
147 | { |
148 | default: |
149 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); |
150 | break; |
151 | } |
152 | } |
153 | |
154 | static void |
155 | gtk_data_viewer_class_init (GtkDataViewerClass *klass) |
156 | { |
157 | GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); |
158 | GObjectClass *gobject_class = G_OBJECT_CLASS (klass); |
159 | |
160 | widget_class->realize = gtk_data_viewer_realize; |
161 | widget_class->unrealize = gtk_data_viewer_unrealize; |
162 | |
163 | gobject_class->dispose = gtk_data_viewer_dispose; |
164 | gobject_class->get_property = gtk_data_viewer_get_property; |
165 | gobject_class->set_property = gtk_data_viewer_set_property; |
166 | |
167 | properties[PROP_LOADING] = |
168 | g_param_spec_boolean (name: "loading" , |
169 | nick: "Loading" , |
170 | blurb: "If the widget is currently loading the data to display" , |
171 | FALSE, |
172 | flags: G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); |
173 | |
174 | g_object_class_install_properties (oclass: gobject_class, n_pspecs: N_PROPS, pspecs: properties); |
175 | |
176 | signals[LOAD] = |
177 | g_signal_new (signal_name: "load" , |
178 | G_TYPE_FROM_CLASS (klass), |
179 | signal_flags: G_SIGNAL_RUN_LAST, |
180 | class_offset: 0, |
181 | accumulator: g_signal_accumulator_first_wins, NULL, |
182 | NULL, |
183 | G_TYPE_BOOLEAN, n_params: 1, |
184 | G_TYPE_CANCELLABLE); |
185 | |
186 | gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BIN_LAYOUT); |
187 | gtk_widget_class_set_css_name (widget_class, name: "frame" ); |
188 | } |
189 | |
190 | static void |
191 | gtk_data_viewer_init (GtkDataViewer *self) |
192 | { |
193 | } |
194 | |
195 | GtkWidget * |
196 | gtk_data_viewer_new (void) |
197 | { |
198 | return g_object_new (GTK_TYPE_DATA_VIEWER, NULL); |
199 | } |
200 | |
201 | gboolean |
202 | gtk_data_viewer_is_loading (GtkDataViewer *self) |
203 | { |
204 | g_return_val_if_fail (GTK_IS_DATA_VIEWER (self), FALSE); |
205 | |
206 | return self->loading == LOADING_EXTERNALLY || |
207 | self->loading == LOADING_INTERNALLY; |
208 | } |
209 | |
210 | void |
211 | gtk_data_viewer_reset (GtkDataViewer *self) |
212 | { |
213 | gboolean was_loading; |
214 | |
215 | g_return_if_fail (GTK_IS_DATA_VIEWER (self)); |
216 | |
217 | g_object_freeze_notify (G_OBJECT (self)); |
218 | |
219 | was_loading = gtk_data_viewer_is_loading (self); |
220 | |
221 | g_clear_pointer (&self->contents, gtk_widget_unparent); |
222 | g_clear_error (err: &self->error); |
223 | g_cancellable_cancel (cancellable: self->cancellable); |
224 | g_clear_object (&self->cancellable); |
225 | |
226 | self->loading = NOT_LOADED; |
227 | |
228 | if (gtk_widget_get_realized (GTK_WIDGET (self))) |
229 | gtk_data_viewer_ensure_loaded (self); |
230 | |
231 | if (was_loading != gtk_data_viewer_is_loading (self)) |
232 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_LOADING]); |
233 | |
234 | g_object_thaw_notify (G_OBJECT (self)); |
235 | } |
236 | |
237 | void |
238 | gtk_data_viewer_load_value (GtkDataViewer *self, |
239 | const GValue *value) |
240 | { |
241 | gboolean was_loading; |
242 | |
243 | g_return_if_fail (GTK_IS_DATA_VIEWER (self)); |
244 | |
245 | was_loading = gtk_data_viewer_is_loading (self); |
246 | self->loading = LOADING_DONE; |
247 | |
248 | g_clear_pointer (&self->contents, gtk_widget_unparent); |
249 | g_cancellable_cancel (cancellable: self->cancellable); |
250 | g_clear_object (&self->cancellable); |
251 | |
252 | if (g_type_is_a (G_VALUE_TYPE (value), G_TYPE_STRING)) |
253 | { |
254 | self->contents = gtk_label_new (str: g_value_get_string (value)); |
255 | gtk_label_set_wrap (GTK_LABEL (self->contents), TRUE); |
256 | gtk_widget_set_parent (widget: self->contents, GTK_WIDGET (self)); |
257 | } |
258 | else if (g_type_is_a (G_VALUE_TYPE (value), GDK_TYPE_PAINTABLE)) |
259 | { |
260 | self->contents = gtk_picture_new_for_paintable (paintable: g_value_get_object (value)); |
261 | gtk_widget_set_size_request (widget: self->contents, width: 256, height: 256); |
262 | gtk_widget_set_parent (widget: self->contents, GTK_WIDGET (self)); |
263 | } |
264 | else if (g_type_is_a (G_VALUE_TYPE (value), GDK_TYPE_PIXBUF)) |
265 | { |
266 | self->contents = gtk_picture_new_for_pixbuf (pixbuf: g_value_get_object (value)); |
267 | gtk_widget_set_size_request (widget: self->contents, width: 256, height: 256); |
268 | gtk_widget_set_parent (widget: self->contents, GTK_WIDGET (self)); |
269 | } |
270 | else if (g_type_is_a (G_VALUE_TYPE (value), GDK_TYPE_RGBA)) |
271 | { |
272 | const GdkRGBA *color = g_value_get_boxed (value); |
273 | |
274 | self->contents = gtk_color_swatch_new (); |
275 | gtk_color_swatch_set_rgba (GTK_COLOR_SWATCH (self->contents), color); |
276 | gtk_widget_set_size_request (widget: self->contents, width: 48, height: 32); |
277 | gtk_widget_set_halign (widget: self->contents, align: GTK_ALIGN_CENTER); |
278 | gtk_widget_set_parent (widget: self->contents, GTK_WIDGET (self)); |
279 | } |
280 | else if (g_type_is_a (G_VALUE_TYPE (value), G_TYPE_FILE)) |
281 | { |
282 | GFile *file = g_value_get_object (value); |
283 | |
284 | self->contents = gtk_label_new (str: g_file_peek_path (file)); |
285 | gtk_label_set_ellipsize (GTK_LABEL (self->contents), mode: PANGO_ELLIPSIZE_START); |
286 | gtk_widget_set_halign (widget: self->contents, align: GTK_ALIGN_CENTER); |
287 | gtk_widget_set_parent (widget: self->contents, GTK_WIDGET (self)); |
288 | } |
289 | else if (g_type_is_a (G_VALUE_TYPE (value), GDK_TYPE_FILE_LIST)) |
290 | { |
291 | GList *l; |
292 | |
293 | self->contents = gtk_box_new (orientation: GTK_ORIENTATION_VERTICAL, spacing: 10); |
294 | gtk_widget_set_parent (widget: self->contents, GTK_WIDGET (self)); |
295 | |
296 | for (l = g_value_get_boxed (value); l; l = l->next) |
297 | { |
298 | GFile *file = l->data; |
299 | GtkWidget *label = gtk_label_new (str: g_file_peek_path (file)); |
300 | gtk_label_set_ellipsize (GTK_LABEL (label), mode: PANGO_ELLIPSIZE_START); |
301 | gtk_widget_set_halign (widget: label, align: GTK_ALIGN_CENTER); |
302 | gtk_box_append (GTK_BOX (self->contents), child: label); |
303 | } |
304 | } |
305 | else |
306 | { |
307 | gtk_data_viewer_load_error (self, |
308 | error: g_error_new (G_IO_ERROR, |
309 | code: G_IO_ERROR_FAILED, |
310 | format: "Cannot display objects of type \"%s\"" , G_VALUE_TYPE_NAME (value))); |
311 | } |
312 | |
313 | if (was_loading) |
314 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_LOADING]); |
315 | } |
316 | |
317 | static void |
318 | gtk_data_viewer_load_stream_done (GObject *source, |
319 | GAsyncResult *res, |
320 | gpointer data) |
321 | { |
322 | GtkDataViewer *self = data; |
323 | GError *error = NULL; |
324 | GValue value = G_VALUE_INIT; |
325 | |
326 | if (!gdk_content_deserialize_finish (result: res, value: &value, error: &error)) |
327 | { |
328 | if (!g_error_matches (error, G_IO_ERROR, code: G_IO_ERROR_CANCELLED)) |
329 | gtk_data_viewer_load_error (self, error); |
330 | else |
331 | g_clear_error (err: &error); |
332 | |
333 | g_object_unref (object: self); |
334 | return; |
335 | } |
336 | |
337 | gtk_data_viewer_load_value (self, value: &value); |
338 | g_object_unref (object: self); |
339 | g_value_unset (value: &value); |
340 | } |
341 | |
342 | void |
343 | gtk_data_viewer_load_stream (GtkDataViewer *self, |
344 | GInputStream *stream, |
345 | const char *mime_type) |
346 | { |
347 | GdkContentFormats *formats; |
348 | const GType *gtypes; |
349 | gboolean was_loading; |
350 | |
351 | g_return_if_fail (GTK_IS_DATA_VIEWER (self)); |
352 | g_return_if_fail (G_IS_INPUT_STREAM (stream)); |
353 | g_return_if_fail (mime_type != NULL); |
354 | |
355 | was_loading = gtk_data_viewer_is_loading (self); |
356 | self->loading = LOADING_INTERNALLY; |
357 | if (self->cancellable == NULL) |
358 | self->cancellable = g_cancellable_new (); |
359 | |
360 | formats = gdk_content_formats_new (mime_types: &mime_type, n_mime_types: 1); |
361 | formats = gdk_content_formats_union_deserialize_gtypes (formats); |
362 | gtypes = gdk_content_formats_get_gtypes (formats, NULL); |
363 | if (gtypes) |
364 | { |
365 | gdk_content_deserialize_async (stream, |
366 | mime_type, |
367 | type: gtypes[0], |
368 | G_PRIORITY_DEFAULT, |
369 | cancellable: self->cancellable, |
370 | callback: gtk_data_viewer_load_stream_done, |
371 | g_object_ref (self)); |
372 | |
373 | if (!was_loading) |
374 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_LOADING]); |
375 | } |
376 | else |
377 | { |
378 | gtk_data_viewer_load_error (self, |
379 | error: g_error_new (G_IO_ERROR, code: G_IO_ERROR_FAILED, |
380 | format: "Cannot display data of type \"%s\"" , mime_type)); |
381 | } |
382 | |
383 | gdk_content_formats_unref (formats); |
384 | } |
385 | |
386 | void |
387 | gtk_data_viewer_load_error (GtkDataViewer *self, |
388 | GError *error) |
389 | { |
390 | gboolean was_loading; |
391 | |
392 | g_return_if_fail (GTK_IS_DATA_VIEWER (self)); |
393 | |
394 | was_loading = gtk_data_viewer_is_loading (self); |
395 | self->loading = LOADING_FAILED; |
396 | |
397 | g_clear_pointer (&self->contents, gtk_widget_unparent); |
398 | g_clear_error (err: &self->error); |
399 | g_cancellable_cancel (cancellable: self->cancellable); |
400 | g_clear_object (&self->cancellable); |
401 | |
402 | self->error = error; |
403 | self->contents = gtk_label_new (str: error->message); |
404 | gtk_widget_add_css_class (widget: self->contents, css_class: "error" ); |
405 | gtk_widget_set_halign (widget: self->contents, align: GTK_ALIGN_CENTER); |
406 | gtk_widget_set_valign (widget: self->contents, align: GTK_ALIGN_CENTER); |
407 | gtk_widget_set_parent (widget: self->contents, GTK_WIDGET (self)); |
408 | |
409 | if (was_loading) |
410 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_LOADING]); |
411 | } |
412 | |
413 | |