2011-01-03 Martin Robinson <mrobinson@igalia.com>
[WebKit-https.git] / WebCore / platform / gtk / RenderThemeGtk.cpp
1 /*
2  * Copyright (C) 2007 Apple Inc.
3  * Copyright (C) 2007 Alp Toker <alp@atoker.com>
4  * Copyright (C) 2008 Collabora Ltd.
5  * Copyright (C) 2009 Kenneth Rohde Christiansen
6  * Copyright (C) 2010 Igalia S.L.
7  *
8  * This library is free software; you can redistribute it and/or
9  * modify it under the terms of the GNU Library General Public
10  * License as published by the Free Software Foundation; either
11  * version 2 of the License, or (at your option) any later version.
12  *
13  * This library is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
16  * Library General Public License for more details.
17  *
18  * You should have received a copy of the GNU Library General Public License
19  * along with this library; see the file COPYING.LIB.  If not, write to
20  * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
21  * Boston, MA 02110-1301, USA.
22  *
23  */
24
25 #include "config.h"
26 #include "RenderThemeGtk.h"
27
28 #include "CSSValueKeywords.h"
29 #include "GOwnPtr.h"
30 #include "Gradient.h"
31 #include "GraphicsContext.h"
32 #include "GtkVersioning.h"
33 #include "HTMLMediaElement.h"
34 #include "HTMLNames.h"
35 #include "MediaControlElements.h"
36 #include "RenderBox.h"
37 #include "RenderObject.h"
38 #include "TimeRanges.h"
39 #include "UserAgentStyleSheets.h"
40 #include <gdk/gdk.h>
41 #include <gtk/gtk.h>
42
43 namespace WebCore {
44
45 using namespace HTMLNames;
46
47 #if ENABLE(VIDEO)
48 static HTMLMediaElement* getMediaElementFromRenderObject(RenderObject* o)
49 {
50     Node* node = o->node();
51     Node* mediaNode = node ? node->shadowAncestorNode() : 0;
52     if (!mediaNode || (!mediaNode->hasTagName(videoTag) && !mediaNode->hasTagName(audioTag)))
53         return 0;
54
55     return static_cast<HTMLMediaElement*>(mediaNode);
56 }
57
58 static GtkIconSize getMediaButtonIconSize(int mediaIconSize)
59 {
60     GtkIconSize iconSize = gtk_icon_size_from_name("webkit-media-button-size");
61     if (!iconSize)
62         iconSize = gtk_icon_size_register("webkit-media-button-size", mediaIconSize, mediaIconSize);
63     return iconSize;
64 }
65
66 void RenderThemeGtk::initMediaButtons()
67 {
68     static bool iconsInitialized = false;
69
70     if (iconsInitialized)
71         return;
72
73     GRefPtr<GtkIconFactory> iconFactory = adoptGRef(gtk_icon_factory_new());
74     GtkIconSource* iconSource = gtk_icon_source_new();
75     const char* icons[] = { "audio-volume-high", "audio-volume-muted" };
76
77     gtk_icon_factory_add_default(iconFactory.get());
78
79     for (size_t i = 0; i < G_N_ELEMENTS(icons); ++i) {
80         gtk_icon_source_set_icon_name(iconSource, icons[i]);
81         GtkIconSet* iconSet = gtk_icon_set_new();
82         gtk_icon_set_add_source(iconSet, iconSource);
83         gtk_icon_factory_add(iconFactory.get(), icons[i], iconSet);
84         gtk_icon_set_unref(iconSet);
85     }
86
87     gtk_icon_source_free(iconSource);
88
89     iconsInitialized = true;
90 }
91 #endif
92
93 PassRefPtr<RenderTheme> RenderThemeGtk::create()
94 {
95     return adoptRef(new RenderThemeGtk());
96 }
97
98 PassRefPtr<RenderTheme> RenderTheme::themeForPage(Page* page)
99 {
100     static RenderTheme* rt = RenderThemeGtk::create().releaseRef();
101     return rt;
102 }
103
104 static int mozGtkRefCount = 0;
105
106 RenderThemeGtk::RenderThemeGtk()
107     : m_gtkWindow(0)
108     , m_gtkContainer(0)
109     , m_gtkButton(0)
110     , m_gtkEntry(0)
111     , m_gtkTreeView(0)
112     , m_gtkVScale(0)
113     , m_gtkHScale(0)
114     , m_panelColor(Color::white)
115     , m_sliderColor(Color::white)
116     , m_sliderThumbColor(Color::white)
117     , m_mediaIconSize(16)
118     , m_mediaSliderHeight(14)
119     , m_mediaSliderThumbWidth(12)
120     , m_mediaSliderThumbHeight(12)
121 #ifdef GTK_API_VERSION_2
122     , m_themePartsHaveRGBAColormap(true)
123 #endif
124 {
125
126     memset(&m_themeParts, 0, sizeof(GtkThemeParts));
127 #ifdef GTK_API_VERSION_2
128     GdkColormap* colormap = gdk_screen_get_rgba_colormap(gdk_screen_get_default());
129     if (!colormap) {
130         m_themePartsHaveRGBAColormap = false;
131         colormap = gdk_screen_get_default_colormap(gdk_screen_get_default());
132     }
133     m_themeParts.colormap = colormap;
134 #endif
135
136     // Initialize the Mozilla theme drawing code.
137     if (!mozGtkRefCount) {
138         moz_gtk_init();
139         moz_gtk_use_theme_parts(&m_themeParts);
140     }
141     ++mozGtkRefCount;
142
143 #if ENABLE(VIDEO)
144     initMediaColors();
145     initMediaButtons();
146 #endif
147 }
148
149 RenderThemeGtk::~RenderThemeGtk()
150 {
151     --mozGtkRefCount;
152
153     if (!mozGtkRefCount)
154         moz_gtk_shutdown();
155
156     gtk_widget_destroy(m_gtkWindow);
157 }
158
159 void RenderThemeGtk::getIndicatorMetrics(ControlPart part, int& indicatorSize, int& indicatorSpacing) const
160 {
161     ASSERT(part == CheckboxPart || part == RadioPart);
162     if (part == CheckboxPart) {
163         moz_gtk_checkbox_get_metrics(&indicatorSize, &indicatorSpacing);
164         return;
165     }
166
167     // RadioPart
168     moz_gtk_radio_get_metrics(&indicatorSize, &indicatorSpacing);
169 }
170
171 static bool supportsFocus(ControlPart appearance)
172 {
173     switch (appearance) {
174     case PushButtonPart:
175     case ButtonPart:
176     case TextFieldPart:
177     case TextAreaPart:
178     case SearchFieldPart:
179     case MenulistPart:
180     case RadioPart:
181     case CheckboxPart:
182     case SliderHorizontalPart:
183     case SliderVerticalPart:
184         return true;
185     default:
186         return false;
187     }
188 }
189
190 bool RenderThemeGtk::supportsFocusRing(const RenderStyle* style) const
191 {
192     return supportsFocus(style->appearance());
193 }
194
195 bool RenderThemeGtk::controlSupportsTints(const RenderObject* o) const
196 {
197     return isEnabled(o);
198 }
199
200 int RenderThemeGtk::baselinePosition(const RenderObject* o) const
201 {
202     if (!o->isBox())
203         return 0;
204
205     // FIXME: This strategy is possibly incorrect for the GTK+ port.
206     if (o->style()->appearance() == CheckboxPart
207         || o->style()->appearance() == RadioPart) {
208         const RenderBox* box = toRenderBox(o);
209         return box->marginTop() + box->height() - 2;
210     }
211
212     return RenderTheme::baselinePosition(o);
213 }
214
215 // This is used in RenderThemeGtk2 and RenderThemeGtk3. Normally, it would be in
216 // the RenderThemeGtk header (perhaps as a static method), but we want to avoid
217 // having to include GTK+ headers only for the GtkTextDirection enum.
218 GtkTextDirection gtkTextDirection(TextDirection direction)
219 {
220     switch (direction) {
221     case RTL:
222         return GTK_TEXT_DIR_RTL;
223     case LTR:
224         return GTK_TEXT_DIR_LTR;
225     default:
226         return GTK_TEXT_DIR_NONE;
227     }
228 }
229
230 GtkStateType RenderThemeGtk::gtkIconState(RenderObject* renderObject)
231 {
232     if (!isEnabled(renderObject))
233         return GTK_STATE_INSENSITIVE;
234     if (isPressed(renderObject))
235         return GTK_STATE_ACTIVE;
236     if (isHovered(renderObject))
237         return GTK_STATE_PRELIGHT;
238
239     return GTK_STATE_NORMAL;
240 }
241
242 void RenderThemeGtk::adjustButtonStyle(CSSStyleSelector* selector, RenderStyle* style, WebCore::Element* e) const
243 {
244     // Some layout tests check explicitly that buttons ignore line-height.
245     if (style->appearance() == PushButtonPart)
246         style->setLineHeight(RenderStyle::initialLineHeight());
247 }
248
249 void RenderThemeGtk::adjustMenuListStyle(CSSStyleSelector* selector, RenderStyle* style, Element* e) const
250 {
251     // The tests check explicitly that select menu buttons ignore line height.
252     style->setLineHeight(RenderStyle::initialLineHeight());
253
254     // We cannot give a proper rendering when border radius is active, unfortunately.
255     style->resetBorderRadius();
256 }
257
258 void RenderThemeGtk::adjustMenuListButtonStyle(CSSStyleSelector* selector, RenderStyle* style, Element* e) const
259 {
260     adjustMenuListStyle(selector, style, e);
261 }
262
263 bool RenderThemeGtk::paintMenuListButton(RenderObject* object, const PaintInfo& info, const IntRect& rect)
264 {
265     return paintMenuList(object, info, rect);
266 }
267
268 void RenderThemeGtk::adjustTextFieldStyle(CSSStyleSelector* selector, RenderStyle* style, Element* e) const
269 {
270     RenderThemeGtk::setTextInputBorders(style);
271 }
272
273 void RenderThemeGtk::adjustTextAreaStyle(CSSStyleSelector* selector, RenderStyle* style, Element* e) const
274 {
275     RenderThemeGtk::setTextInputBorders(style);
276 }
277
278 bool RenderThemeGtk::paintTextArea(RenderObject* o, const PaintInfo& i, const IntRect& r)
279 {
280     return paintTextField(o, i, r);
281 }
282
283 static void paintGdkPixbuf(GraphicsContext* context, const GdkPixbuf* icon, const IntPoint& iconPoint)
284 {
285     cairo_t* cr = context->platformContext();
286     cairo_save(cr);
287     gdk_cairo_set_source_pixbuf(cr, icon, iconPoint.x(), iconPoint.y());
288     cairo_paint(cr);
289     cairo_restore(cr);
290 }
291
292 void RenderThemeGtk::adjustSearchFieldResultsButtonStyle(CSSStyleSelector* selector, RenderStyle* style, Element* e) const
293 {
294     adjustSearchFieldCancelButtonStyle(selector, style, e);
295 }
296
297 bool RenderThemeGtk::paintSearchFieldResultsButton(RenderObject* o, const PaintInfo& i, const IntRect& rect)
298 {
299     return paintSearchFieldResultsDecoration(o, i, rect);
300 }
301
302 void RenderThemeGtk::adjustSearchFieldResultsDecorationStyle(CSSStyleSelector* selector, RenderStyle* style, Element* e) const
303 {
304     style->resetBorder();
305     style->resetPadding();
306
307     gint width = 0, height = 0;
308     gtk_icon_size_lookup(GTK_ICON_SIZE_MENU, &width, &height);
309     style->setWidth(Length(width, Fixed));
310     style->setHeight(Length(height, Fixed));
311 }
312
313 static IntPoint centerRectVerticallyInParentInputElement(RenderObject* object, const IntRect& rect)
314 {
315     Node* input = object->node()->shadowAncestorNode(); // Get the renderer of <input> element.
316     if (!input->renderer()->isBox())
317         return rect.topLeft();
318
319     // If possible center the y-coordinate of the rect vertically in the parent input element.
320     // We also add one pixel here to ensure that the y coordinate is rounded up for box heights
321     // that are even, which looks in relation to the box text.
322     IntRect inputContentBox = toRenderBox(input->renderer())->absoluteContentBox();
323
324     return IntPoint(rect.x(), inputContentBox.y() + (inputContentBox.height() - rect.height() + 1) / 2);
325 }
326
327 bool RenderThemeGtk::paintSearchFieldResultsDecoration(RenderObject* renderObject, const PaintInfo& paintInfo, const IntRect& rect)
328 {
329     GRefPtr<GdkPixbuf> icon = getStockIcon(GTK_TYPE_ENTRY, GTK_STOCK_FIND,
330                                            gtkTextDirection(renderObject->style()->direction()),
331                                            gtkIconState(renderObject), GTK_ICON_SIZE_MENU);
332     paintGdkPixbuf(paintInfo.context, icon.get(), centerRectVerticallyInParentInputElement(renderObject, rect));
333     return false;
334 }
335
336 void RenderThemeGtk::adjustSearchFieldCancelButtonStyle(CSSStyleSelector* selector, RenderStyle* style, Element* e) const
337 {
338     style->resetBorder();
339     style->resetPadding();
340
341     gint width = 0, height = 0;
342     gtk_icon_size_lookup(GTK_ICON_SIZE_MENU, &width, &height);
343     style->setWidth(Length(width, Fixed));
344     style->setHeight(Length(height, Fixed));
345 }
346
347 bool RenderThemeGtk::paintSearchFieldCancelButton(RenderObject* renderObject, const PaintInfo& paintInfo, const IntRect& rect)
348 {
349     GRefPtr<GdkPixbuf> icon = getStockIcon(GTK_TYPE_ENTRY, GTK_STOCK_CLEAR,
350                                            gtkTextDirection(renderObject->style()->direction()),
351                                            gtkIconState(renderObject), GTK_ICON_SIZE_MENU);
352     paintGdkPixbuf(paintInfo.context, icon.get(), centerRectVerticallyInParentInputElement(renderObject, rect));
353     return false;
354 }
355
356 void RenderThemeGtk::adjustSearchFieldStyle(CSSStyleSelector* selector, RenderStyle* style, Element* e) const
357 {
358     style->setLineHeight(RenderStyle::initialLineHeight());
359     setTextInputBorders(style);
360 }
361
362 bool RenderThemeGtk::paintSearchField(RenderObject* o, const PaintInfo& i, const IntRect& rect)
363 {
364     return paintTextField(o, i, rect);
365 }
366
367 void RenderThemeGtk::adjustSliderTrackStyle(CSSStyleSelector*, RenderStyle* style, Element*) const
368 {
369     style->setBoxShadow(0);
370 }
371
372 void RenderThemeGtk::adjustSliderThumbStyle(CSSStyleSelector*, RenderStyle* style, Element*) const
373 {
374     style->setBoxShadow(0);
375 }
376
377 double RenderThemeGtk::caretBlinkInterval() const
378 {
379     GtkSettings* settings = gtk_settings_get_default();
380
381     gboolean shouldBlink;
382     gint time;
383
384     g_object_get(settings, "gtk-cursor-blink", &shouldBlink, "gtk-cursor-blink-time", &time, NULL);
385
386     if (!shouldBlink)
387         return 0;
388
389     return time / 2000.;
390 }
391
392 static double getScreenDPI()
393 {
394     // FIXME: Really this should be the widget's screen.
395     GdkScreen* screen = gdk_screen_get_default();
396     if (!screen)
397         return 96; // Default to 96 DPI.
398
399     float dpi = gdk_screen_get_resolution(screen);
400     if (dpi <= 0)
401         return 96;
402     return dpi;
403 }
404
405 void RenderThemeGtk::systemFont(int, FontDescription& fontDescription) const
406 {
407     GtkSettings* settings = gtk_settings_get_default();
408     if (!settings)
409         return;
410
411     // This will be a font selection string like "Sans 10" so we cannot use it as the family name.
412     GOwnPtr<gchar> fontName;
413     g_object_get(settings, "gtk-font-name", &fontName.outPtr(), NULL);
414
415     PangoFontDescription* pangoDescription = pango_font_description_from_string(fontName.get());
416     if (!pangoDescription)
417         return;
418
419     fontDescription.firstFamily().setFamily(pango_font_description_get_family(pangoDescription));
420
421     int size = pango_font_description_get_size(pangoDescription) / PANGO_SCALE;
422     // If the size of the font is in points, we need to convert it to pixels.
423     if (!pango_font_description_get_size_is_absolute(pangoDescription))
424         size = size * (getScreenDPI() / 72.0);
425
426     fontDescription.setSpecifiedSize(size);
427     fontDescription.setIsAbsoluteSize(true);
428     fontDescription.setGenericFamily(FontDescription::NoFamily);
429     fontDescription.setWeight(FontWeightNormal);
430     fontDescription.setItalic(false);
431     pango_font_description_free(pangoDescription);
432 }
433
434 void RenderThemeGtk::platformColorsDidChange()
435 {
436 #if ENABLE(VIDEO)
437     initMediaColors();
438 #endif
439     RenderTheme::platformColorsDidChange();
440 }
441
442 #if ENABLE(VIDEO)
443 String RenderThemeGtk::extraMediaControlsStyleSheet()
444 {
445     return String(mediaControlsGtkUserAgentStyleSheet, sizeof(mediaControlsGtkUserAgentStyleSheet));
446 }
447
448 bool RenderThemeGtk::paintMediaButton(RenderObject* renderObject, GraphicsContext* context, const IntRect& rect, const char* iconName)
449 {
450     GRefPtr<GdkPixbuf> icon = getStockIcon(GTK_TYPE_CONTAINER, iconName,
451                                            gtkTextDirection(renderObject->style()->direction()),
452                                            gtkIconState(renderObject),
453                                            getMediaButtonIconSize(m_mediaIconSize));
454     IntPoint iconPoint(rect.x() + (rect.width() - m_mediaIconSize) / 2,
455                        rect.y() + (rect.height() - m_mediaIconSize) / 2);
456     context->fillRect(FloatRect(rect), m_panelColor, ColorSpaceDeviceRGB);
457     paintGdkPixbuf(context, icon.get(), iconPoint);
458     return false;
459 }
460
461 bool RenderThemeGtk::paintMediaFullscreenButton(RenderObject* renderObject, const PaintInfo& paintInfo, const IntRect& rect)
462 {
463     return paintMediaButton(renderObject, paintInfo.context, rect, GTK_STOCK_FULLSCREEN);
464 }
465
466 bool RenderThemeGtk::paintMediaMuteButton(RenderObject* renderObject, const PaintInfo& paintInfo, const IntRect& rect)
467 {
468     HTMLMediaElement* mediaElement = getMediaElementFromRenderObject(renderObject);
469     if (!mediaElement)
470         return false;
471
472     return paintMediaButton(renderObject, paintInfo.context, rect, mediaElement->muted() ? "audio-volume-muted" : "audio-volume-high");
473 }
474
475 bool RenderThemeGtk::paintMediaPlayButton(RenderObject* renderObject, const PaintInfo& paintInfo, const IntRect& rect)
476 {
477     Node* node = renderObject->node();
478     if (!node)
479         return false;
480
481     MediaControlPlayButtonElement* button = static_cast<MediaControlPlayButtonElement*>(node);
482     return paintMediaButton(renderObject, paintInfo.context, rect, button->displayType() == MediaPlayButton ? GTK_STOCK_MEDIA_PLAY : GTK_STOCK_MEDIA_PAUSE);
483 }
484
485 bool RenderThemeGtk::paintMediaSeekBackButton(RenderObject* renderObject, const PaintInfo& paintInfo, const IntRect& rect)
486 {
487     return paintMediaButton(renderObject, paintInfo.context, rect, GTK_STOCK_MEDIA_REWIND);
488 }
489
490 bool RenderThemeGtk::paintMediaSeekForwardButton(RenderObject* renderObject, const PaintInfo& paintInfo, const IntRect& rect)
491 {
492     return paintMediaButton(renderObject, paintInfo.context, rect, GTK_STOCK_MEDIA_FORWARD);
493 }
494
495 bool RenderThemeGtk::paintMediaSliderTrack(RenderObject* o, const PaintInfo& paintInfo, const IntRect& r)
496 {
497     GraphicsContext* context = paintInfo.context;
498
499     context->fillRect(FloatRect(r), m_panelColor, ColorSpaceDeviceRGB);
500     context->fillRect(FloatRect(IntRect(r.x(), r.y() + (r.height() - m_mediaSliderHeight) / 2,
501                                         r.width(), m_mediaSliderHeight)), m_sliderColor, ColorSpaceDeviceRGB);
502
503     RenderStyle* style = o->style();
504     HTMLMediaElement* mediaElement = toParentMediaElement(o);
505
506     if (!mediaElement)
507         return false;
508
509     // Draw the buffered ranges. This code is highly inspired from
510     // Chrome for the gradient code.
511     float mediaDuration = mediaElement->duration();
512     RefPtr<TimeRanges> timeRanges = mediaElement->buffered();
513     IntRect trackRect = r;
514     int totalWidth = trackRect.width();
515
516     trackRect.inflate(-style->borderLeftWidth());
517     context->save();
518     context->setStrokeStyle(NoStroke);
519
520     for (unsigned index = 0; index < timeRanges->length(); ++index) {
521         ExceptionCode ignoredException;
522         float start = timeRanges->start(index, ignoredException);
523         float end = timeRanges->end(index, ignoredException);
524         int width = ((end - start) * totalWidth) / mediaDuration;
525         IntRect rangeRect;
526         if (!index) {
527             rangeRect = trackRect;
528             rangeRect.setWidth(width);
529         } else {
530             rangeRect.setLocation(IntPoint(trackRect.x() + start / mediaDuration* totalWidth, trackRect.y()));
531             rangeRect.setSize(IntSize(width, trackRect.height()));
532         }
533
534         // Don't bother drawing empty range.
535         if (rangeRect.isEmpty())
536             continue;
537
538         IntPoint sliderTopLeft = rangeRect.location();
539         IntPoint sliderTopRight = sliderTopLeft;
540         sliderTopRight.move(0, rangeRect.height());
541
542         RefPtr<Gradient> gradient = Gradient::create(sliderTopLeft, sliderTopRight);
543         Color startColor = m_panelColor;
544         gradient->addColorStop(0.0, startColor);
545         gradient->addColorStop(1.0, Color(startColor.red() / 2, startColor.green() / 2, startColor.blue() / 2, startColor.alpha()));
546
547         context->setFillGradient(gradient);
548         context->fillRect(rangeRect);
549     }
550
551     context->restore();
552     return false;
553 }
554
555 bool RenderThemeGtk::paintMediaSliderThumb(RenderObject* o, const PaintInfo& paintInfo, const IntRect& r)
556 {
557     // Make the thumb nicer with rounded corners.
558     paintInfo.context->fillRoundedRect(r, IntSize(3, 3), IntSize(3, 3), IntSize(3, 3), IntSize(3, 3), m_sliderThumbColor, ColorSpaceDeviceRGB);
559     return false;
560 }
561
562 String RenderThemeGtk::formatMediaControlsCurrentTime(float currentTime, float duration) const
563 {
564     return formatMediaControlsTime(currentTime) + " / " + formatMediaControlsTime(duration);
565 }
566
567 bool RenderThemeGtk::paintMediaCurrentTime(RenderObject* renderObject, const PaintInfo& paintInfo, const IntRect& rect)
568 {
569     GraphicsContext* context = paintInfo.context;
570
571     context->fillRect(FloatRect(rect), m_panelColor, ColorSpaceDeviceRGB);
572     return false;
573 }
574 #endif
575
576 #if ENABLE(PROGRESS_TAG)
577 double RenderThemeGtk::animationRepeatIntervalForProgressBar(RenderProgress*) const
578 {
579     // FIXME: It doesn't look like there is a good way yet to support animated
580     // progress bars with the Mozilla theme drawing code.
581     return 0;
582 }
583
584 double RenderThemeGtk::animationDurationForProgressBar(RenderProgress*) const
585 {
586     // FIXME: It doesn't look like there is a good way yet to support animated
587     // progress bars with the Mozilla theme drawing code.
588     return 0;
589 }
590
591 void RenderThemeGtk::adjustProgressBarStyle(CSSStyleSelector*, RenderStyle* style, Element*) const
592 {
593     style->setBoxShadow(0);
594 }
595 #endif
596
597 }