1 /*
2  * util.c
3  * Copyright 2010-2012 John Lindgren and Michał Lipski
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions are met:
7  *
8  * 1. Redistributions of source code must retain the above copyright notice,
9  *    this list of conditions, and the following disclaimer.
10  *
11  * 2. Redistributions in binary form must reproduce the above copyright notice,
12  *    this list of conditions, and the following disclaimer in the documentation
13  *    provided with the distribution.
14  *
15  * This software is provided "as is" and without any warranty, express or
16  * implied. In no event shall the authors be liable for any damages arising from
17  * the use of this software.
18  */
19 
20 #include <math.h>
21 #include <string.h>
22 
23 #ifdef _WIN32
24 #include <windows.h>
25 #endif
26 
27 #include <gdk/gdkkeysyms.h>
28 #include <gtk/gtk.h>
29 
30 #define AUD_GLIB_INTEGRATION
31 #include <libaudcore/audstrings.h>
32 #include <libaudcore/hook.h>
33 #include <libaudcore/i18n.h>
34 #include <libaudcore/runtime.h>
35 
36 #include "internal.h"
37 #include "libaudgui.h"
38 #include "libaudgui-gtk.h"
39 
40 #define PORTABLE_DPI 96
41 
audgui_get_dpi()42 EXPORT int audgui_get_dpi ()
43 {
44     static int dpi = 0;
45 
46     if (! dpi)
47     {
48 #ifdef _WIN32
49         HDC screen = GetDC (nullptr);
50         dpi = (GetDeviceCaps (screen, LOGPIXELSX) + GetDeviceCaps (screen, LOGPIXELSY)) / 2;
51         ReleaseDC (nullptr, screen);
52 #else
53         GdkScreen * screen = gdk_screen_get_default ();
54 
55         /* force GTK settings to be loaded for the GDK screen */
56         (void) gtk_settings_get_for_screen (screen);
57 
58         dpi = round (gdk_screen_get_resolution (screen));
59 #endif
60 
61         dpi = aud::max (PORTABLE_DPI, dpi);
62     }
63 
64     return dpi;
65 }
66 
audgui_to_native_dpi(int size)67 EXPORT int audgui_to_native_dpi (int size)
68 {
69     return aud::rescale (size, PORTABLE_DPI, audgui_get_dpi ());
70 }
71 
audgui_to_portable_dpi(int size)72 EXPORT int audgui_to_portable_dpi (int size)
73 {
74     return aud::rescale (size, audgui_get_dpi (), PORTABLE_DPI);
75 }
76 
audgui_get_digit_width(GtkWidget * widget)77 EXPORT int audgui_get_digit_width (GtkWidget * widget)
78 {
79     int width;
80     PangoLayout * layout = gtk_widget_create_pango_layout (widget, "0123456789");
81     PangoFontDescription * desc = pango_font_description_new ();
82     pango_font_description_set_weight (desc, PANGO_WEIGHT_BOLD);
83     pango_layout_set_font_description (layout, desc);
84     pango_layout_get_pixel_size (layout, & width, nullptr);
85     pango_font_description_free (desc);
86     g_object_unref (layout);
87     return (width + 9) / 10;
88 }
89 
audgui_get_mouse_coords(GtkWidget * widget,int * x,int * y)90 EXPORT void audgui_get_mouse_coords (GtkWidget * widget, int * x, int * y)
91 {
92     gtk_widget_get_pointer (widget, x, y);
93 }
94 
audgui_get_mouse_coords(GdkScreen * screen,int * x,int * y)95 EXPORT void audgui_get_mouse_coords (GdkScreen * screen, int * x, int * y)
96 {
97     gdk_display_get_pointer (gdk_screen_get_display (screen), nullptr, x, y, nullptr);
98 }
99 
audgui_get_monitor_geometry(GdkScreen * screen,int x,int y,GdkRectangle * geom)100 EXPORT void audgui_get_monitor_geometry (GdkScreen * screen, int x, int y, GdkRectangle * geom)
101 {
102     int monitors = gdk_screen_get_n_monitors (screen);
103 
104     for (int i = 0; i < monitors; i ++)
105     {
106         gdk_screen_get_monitor_geometry (screen, i, geom);
107         if (x >= geom->x && x < geom->x + geom->width && y >= geom->y && y < geom->y + geom->height)
108             return;
109     }
110 
111     /* fall back to entire screen */
112     geom->x = 0;
113     geom->y = 0;
114     geom->width = gdk_screen_get_width (screen);
115     geom->height = gdk_screen_get_height (screen);
116 }
117 
escape_destroy_cb(GtkWidget * widget,GdkEventKey * event)118 static gboolean escape_destroy_cb (GtkWidget * widget, GdkEventKey * event)
119 {
120     if (event->keyval == GDK_KEY_Escape)
121     {
122         gtk_widget_destroy (widget);
123         return true;
124     }
125 
126     return false;
127 }
128 
audgui_destroy_on_escape(GtkWidget * widget)129 EXPORT void audgui_destroy_on_escape (GtkWidget * widget)
130 {
131     g_signal_connect (widget, "key-press-event", (GCallback) escape_destroy_cb, nullptr);
132 }
133 
audgui_button_new(const char * text,const char * icon,AudguiCallback callback,void * data)134 EXPORT GtkWidget * audgui_button_new (const char * text, const char * icon,
135  AudguiCallback callback, void * data)
136 {
137     GtkWidget * button = gtk_button_new_with_mnemonic (text);
138 
139     if (icon)
140     {
141         GtkWidget * image = gtk_image_new_from_icon_name (icon, GTK_ICON_SIZE_MENU);
142         gtk_button_set_image ((GtkButton *) button, image);
143     }
144 
145     if (callback)
146         g_signal_connect_swapped (button, "clicked", (GCallback) callback, data);
147 
148     return button;
149 }
150 
151 struct FileEntryData {
152     GtkFileChooserAction action;
153     String title;
154 };
155 
entry_response_cb(GtkWidget * dialog,int response,GtkWidget * entry)156 static void entry_response_cb (GtkWidget * dialog, int response, GtkWidget * entry)
157 {
158     if (response == GTK_RESPONSE_ACCEPT)
159     {
160         CharPtr uri (gtk_file_chooser_get_uri ((GtkFileChooser *) dialog));
161         if (uri)
162             audgui_file_entry_set_uri (entry, uri);
163     }
164 
165     gtk_widget_destroy (dialog);
166 }
167 
entry_browse_cb(GtkWidget * entry,GtkEntryIconPosition pos,GdkEvent * event,const FileEntryData * data)168 static void entry_browse_cb (GtkWidget * entry, GtkEntryIconPosition pos,
169  GdkEvent * event, const FileEntryData * data)
170 {
171     GtkWidget * dialog = gtk_file_chooser_dialog_new (data->title, nullptr,
172      data->action, _("Open"), GTK_RESPONSE_ACCEPT, _("Cancel"),
173      GTK_RESPONSE_REJECT, nullptr);
174 
175     gtk_file_chooser_set_local_only ((GtkFileChooser *) dialog, false);
176 
177     String uri = audgui_file_entry_get_uri (entry);
178     if (uri)
179         gtk_file_chooser_set_uri ((GtkFileChooser *) dialog, uri);
180 
181     g_signal_connect (dialog, "response", (GCallback) entry_response_cb, entry);
182     g_signal_connect_object (entry, "destroy", (GCallback) gtk_widget_destroy,
183      dialog, G_CONNECT_SWAPPED);
184 
185     gtk_widget_show (dialog);
186 }
187 
audgui_file_entry_new(GtkFileChooserAction action,const char * title)188 EXPORT GtkWidget * audgui_file_entry_new (GtkFileChooserAction action, const char * title)
189 {
190     GtkWidget * entry = gtk_entry_new ();
191 
192     auto data = new FileEntryData {action, String (title)};
193     g_object_set_data_full ((GObject *) entry, "file-entry-data", data,
194      aud::delete_obj<FileEntryData>);
195 
196     gtk_entry_set_icon_from_icon_name ((GtkEntry *) entry,
197      GTK_ENTRY_ICON_SECONDARY, "document-open");
198     g_signal_connect (entry, "icon-press", (GCallback) entry_browse_cb, data);
199 
200     return entry;
201 }
202 
audgui_file_entry_get_uri(GtkWidget * entry)203 EXPORT String audgui_file_entry_get_uri (GtkWidget * entry)
204 {
205     const char * text = gtk_entry_get_text ((GtkEntry *) entry);
206 
207     if (! text[0])
208         return String ();
209     else if (strstr (text, "://"))
210         return String (text);
211     else
212         return String (filename_to_uri (filename_normalize (filename_expand (str_copy (text)))));
213 }
214 
audgui_file_entry_set_uri(GtkWidget * entry,const char * uri)215 EXPORT void audgui_file_entry_set_uri (GtkWidget * entry, const char * uri)
216 {
217     if (! uri || ! uri[0])
218     {
219         gtk_entry_set_text ((GtkEntry *) entry, "");
220         return;
221     }
222 
223     StringBuf path = uri_to_filename (uri, false);
224     gtk_entry_set_text ((GtkEntry *) entry, path ? filename_contract (std::move (path)) : uri);
225     gtk_editable_set_position ((GtkEditable *) entry, -1);
226 }
227 
set_label_wrap(GtkWidget * label,void *)228 static void set_label_wrap (GtkWidget * label, void *)
229 {
230     if (GTK_IS_LABEL (label))
231         gtk_label_set_line_wrap_mode ((GtkLabel *) label, PANGO_WRAP_WORD_CHAR);
232 }
233 
audgui_dialog_new(GtkMessageType type,const char * title,const char * text,GtkWidget * button1,GtkWidget * button2)234 EXPORT GtkWidget * audgui_dialog_new (GtkMessageType type, const char * title,
235  const char * text, GtkWidget * button1, GtkWidget * button2)
236 {
237     GtkWidget * dialog = gtk_message_dialog_new (nullptr, (GtkDialogFlags) 0, type,
238      GTK_BUTTONS_NONE, "%s", text);
239     gtk_window_set_title ((GtkWindow *) dialog, title);
240 
241     GtkWidget * box = gtk_message_dialog_get_message_area ((GtkMessageDialog *) dialog);
242     gtk_container_foreach ((GtkContainer *) box, set_label_wrap, nullptr);
243 
244     if (button2)
245     {
246         gtk_dialog_add_action_widget ((GtkDialog *) dialog, button2, GTK_RESPONSE_NONE);
247         g_signal_connect_swapped (button2, "clicked", (GCallback) gtk_widget_destroy, dialog);
248     }
249 
250     gtk_dialog_add_action_widget ((GtkDialog *) dialog, button1, GTK_RESPONSE_NONE);
251     g_signal_connect_swapped (button1, "clicked", (GCallback) gtk_widget_destroy, dialog);
252 
253     gtk_widget_set_can_default (button1, true);
254     gtk_widget_grab_default (button1);
255 
256     return dialog;
257 }
258 
audgui_dialog_add_widget(GtkWidget * dialog,GtkWidget * widget)259 EXPORT void audgui_dialog_add_widget (GtkWidget * dialog, GtkWidget * widget)
260 {
261     GtkWidget * box = gtk_message_dialog_get_message_area ((GtkMessageDialog *) dialog);
262     gtk_box_pack_start ((GtkBox *) box, widget, false, false, 0);
263 }
264 
audgui_simple_message(GtkWidget ** widget,GtkMessageType type,const char * title,const char * text)265 EXPORT void audgui_simple_message (GtkWidget * * widget, GtkMessageType type,
266  const char * title, const char * text)
267 {
268     if (type == GTK_MESSAGE_ERROR)
269         AUDERR ("%s\n", text);
270     else if (type == GTK_MESSAGE_WARNING)
271         AUDWARN ("%s\n", text);
272     else if (type == GTK_MESSAGE_INFO)
273         AUDINFO ("%s\n", text);
274 
275     if (* widget)
276     {
277         char * old = nullptr;
278         g_object_get ((GObject *) * widget, "text", & old, nullptr);
279         g_return_if_fail (old);
280 
281         int messages = GPOINTER_TO_INT (g_object_get_data ((GObject *) * widget, "messages"));
282         if (messages > 10)
283             text = _("\n(Further messages have been hidden.)");
284 
285         if (! strstr (old, text))
286         {
287             StringBuf both = str_concat ({old, "\n", text});
288             g_object_set ((GObject *) * widget, "text", (const char *) both, nullptr);
289             g_object_set_data ((GObject *) * widget, "messages", GINT_TO_POINTER (messages + 1));
290         }
291 
292         g_free (old);
293         gtk_window_present ((GtkWindow *) * widget);
294     }
295     else
296     {
297         GtkWidget * button = audgui_button_new (_("_Close"), "window-close", nullptr, nullptr);
298         * widget = audgui_dialog_new (type, title, text, button, nullptr);
299 
300         g_object_set_data ((GObject *) * widget, "messages", GINT_TO_POINTER (1));
301         g_signal_connect (* widget, "destroy", (GCallback) gtk_widget_destroyed, widget);
302 
303         gtk_widget_show_all (* widget);
304     }
305 }
306 
audgui_dark_bg_gradient(const GdkColor & base,int height)307 EXPORT cairo_pattern_t * audgui_dark_bg_gradient (const GdkColor & base, int height)
308 {
309     float r = 1, g = 1, b = 1;
310 
311     /* in a dark theme, try to match the tone of the base color */
312     int v = aud::max (aud::max (base.red, base.green), base.blue);
313 
314     if (v >= 10*256 && v < 80*256)
315     {
316         r = (float) base.red / v;
317         g = (float) base.green / v;
318         b = (float) base.blue / v;
319     }
320 
321     cairo_pattern_t * gradient = cairo_pattern_create_linear (0, 0, 0, height);
322     cairo_pattern_add_color_stop_rgb (gradient, 0, 0.16 * r, 0.16 * g, 0.16 * b);
323     cairo_pattern_add_color_stop_rgb (gradient, 0.45, 0.11 * r, 0.11 * g, 0.11 * b);
324     cairo_pattern_add_color_stop_rgb (gradient, 0.55, 0.06 * r, 0.06 * g, 0.06 * b);
325     cairo_pattern_add_color_stop_rgb (gradient, 1, 0.09 * r, 0.09 * g, 0.09 * b);
326     return gradient;
327 }
328 
rgb_to_hsv(float r,float g,float b,float * h,float * s,float * v)329 static void rgb_to_hsv (float r, float g, float b, float * h, float * s, float * v)
330 {
331     float max = aud::max (aud::max (r, g), b);
332     float min = aud::min (aud::min (r, g), b);
333 
334     * v = max;
335 
336     if (max == min)
337     {
338         * h = 0;
339         * s = 0;
340         return;
341     }
342 
343     if (r == max)
344         * h = 1 + (g - b) / (max - min);
345     else if (g == max)
346         * h = 3 + (b - r) / (max - min);
347     else
348         * h = 5 + (r - g) / (max - min);
349 
350     * s = (max - min) / max;
351 }
352 
hsv_to_rgb(float h,float s,float v,float * r,float * g,float * b)353 static void hsv_to_rgb (float h, float s, float v, float * r, float * g, float * b)
354 {
355     for (; h >= 2; h -= 2)
356     {
357         float * p = r;
358         r = g;
359         g = b;
360         b = p;
361     }
362 
363     if (h < 1)
364     {
365         * r = 1;
366         * g = 0;
367         * b = 1 - h;
368     }
369     else
370     {
371         * r = 1;
372         * g = h - 1;
373         * b = 0;
374     }
375 
376     * r = v * (1 - s * (1 - * r));
377     * g = v * (1 - s * (1 - * g));
378     * b = v * (1 - s * (1 - * b));
379 }
380 
audgui_vis_bar_color(const GdkColor & hue,int bar,int n_bars,float & r,float & g,float & b)381 EXPORT void audgui_vis_bar_color (const GdkColor & hue, int bar, int n_bars,
382                                   float & r, float & g, float & b)
383 {
384     float h, s, v;
385     rgb_to_hsv (hue.red / 65535.0, hue.green / 65535.0, hue.blue / 65535.0, & h, & s, & v);
386 
387     if (s < 0.1) /* monochrome theme? use blue instead */
388         h = 4.6;
389 
390     s = 1 - 0.9 * bar / (n_bars - 1);
391     v = 0.75 + 0.25 * bar / (n_bars - 1);
392 
393     hsv_to_rgb (h, s, v, & r, & g, & b);
394 }
395