// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "ui/gtk/gtk_ui.h" #include #include #include #include #include #include #include #include "base/command_line.h" #include "base/containers/flat_map.h" #include "base/debug/leak_annotations.h" #include "base/environment.h" #include "base/logging.h" #include "base/nix/mime_util_xdg.h" #include "base/nix/xdg_util.h" #include "base/stl_util.h" #include "base/strings/string_split.h" #include "base/strings/stringprintf.h" #include "chrome/browser/themes/theme_properties.h" // nogncheck #include "printing/buildflags/buildflags.h" #include "third_party/skia/include/core/SkBitmap.h" #include "third_party/skia/include/core/SkCanvas.h" #include "third_party/skia/include/core/SkColor.h" #include "third_party/skia/include/core/SkShader.h" #include "ui/base/cursor/cursor_theme_manager_observer.h" #include "ui/base/ime/linux/fake_input_method_context.h" #include "ui/base/ime/linux/linux_input_method_context.h" #include "ui/base/ime/linux/linux_input_method_context_factory.h" #include "ui/display/display.h" #include "ui/events/keycodes/dom/dom_code.h" #include "ui/events/keycodes/dom/dom_keyboard_layout_manager.h" #include "ui/events/keycodes/dom/keycode_converter.h" #include "ui/gfx/canvas.h" #include "ui/gfx/font_render_params.h" #include "ui/gfx/geometry/rect.h" #include "ui/gfx/geometry/size.h" #include "ui/gfx/image/image.h" #include "ui/gfx/image/image_skia_source.h" #include "ui/gfx/skbitmap_operations.h" #include "ui/gfx/skia_util.h" #include "ui/gtk/gtk_key_bindings_handler.h" #include "ui/gtk/gtk_ui_delegate.h" #include "ui/gtk/gtk_util.h" #include "ui/gtk/input_method_context_impl_gtk.h" #include "ui/gtk/native_theme_gtk.h" #include "ui/gtk/nav_button_provider_gtk.h" #include "ui/gtk/printing/print_dialog_gtk.h" #include "ui/gtk/printing/printing_gtk_util.h" #include "ui/gtk/select_file_dialog_impl.h" #include "ui/gtk/settings_provider_gtk.h" #include "ui/native_theme/native_theme.h" #include "ui/shell_dialogs/select_file_policy.h" #include "ui/views/controls/button/button.h" #include "ui/views/controls/button/label_button.h" #include "ui/views/controls/button/label_button_border.h" #include "ui/views/linux_ui/device_scale_factor_observer.h" #include "ui/views/linux_ui/nav_button_provider.h" #include "ui/views/linux_ui/window_button_order_observer.h" #if defined(USE_GIO) #include "ui/gtk/settings_provider_gsettings.h" #endif #if defined(USE_OZONE) #include "ui/base/ime/input_method.h" #include "ui/ozone/public/ozone_platform.h" #endif #if BUILDFLAG(ENABLE_PRINTING) #include "printing/printing_context_linux.h" #endif namespace gtk { namespace { // Stores the GtkUi singleton instance const GtkUi* g_gtk_ui = nullptr; const double kDefaultDPI = 96; class GtkButtonImageSource : public gfx::ImageSkiaSource { public: GtkButtonImageSource(bool focus, views::Button::ButtonState button_state, gfx::Size size) : focus_(focus), width_(size.width()), height_(size.height()) { switch (button_state) { case views::Button::ButtonState::STATE_NORMAL: state_ = ui::NativeTheme::kNormal; break; case views::Button::ButtonState::STATE_HOVERED: state_ = ui::NativeTheme::kHovered; break; case views::Button::ButtonState::STATE_PRESSED: state_ = ui::NativeTheme::kPressed; break; case views::Button::ButtonState::STATE_DISABLED: state_ = ui::NativeTheme::kDisabled; break; case views::Button::ButtonState::STATE_COUNT: NOTREACHED(); state_ = ui::NativeTheme::kNormal; break; } } ~GtkButtonImageSource() override = default; gfx::ImageSkiaRep GetImageForScale(float scale) override { int width = width_ * scale; int height = height_ * scale; SkBitmap border; border.allocN32Pixels(width, height); border.eraseColor(0); cairo_surface_t* surface = cairo_image_surface_create_for_data( static_cast(border.getAddr(0, 0)), CAIRO_FORMAT_ARGB32, width, height, width * 4); cairo_t* cr = cairo_create(surface); ScopedStyleContext context = GetStyleContextFromCss("GtkButton#button"); GtkStateFlags state_flags = StateToStateFlags(state_); if (focus_) { state_flags = static_cast(state_flags | GTK_STATE_FLAG_FOCUSED); } gtk_style_context_set_state(context, state_flags); gtk_render_background(context, cr, 0, 0, width, height); gtk_render_frame(context, cr, 0, 0, width, height); if (focus_) { gfx::Rect focus_rect(width, height); #if !GTK_CHECK_VERSION(3, 90, 0) if (!GtkCheckVersion(3, 14)) { gint focus_pad; gtk_style_context_get_style(context, "focus-padding", &focus_pad, nullptr); focus_rect.Inset(focus_pad, focus_pad); if (state_ == ui::NativeTheme::kPressed) { gint child_displacement_x, child_displacement_y; gboolean displace_focus; gtk_style_context_get_style( context, "child-displacement-x", &child_displacement_x, "child-displacement-y", &child_displacement_y, "displace-focus", &displace_focus, nullptr); if (displace_focus) focus_rect.Offset(child_displacement_x, child_displacement_y); } } #endif if (!GtkCheckVersion(3, 20)) { GtkBorder border; #if GTK_CHECK_VERSION(3, 90, 0) gtk_style_context_get_border(context, &border); #else gtk_style_context_get_border(context, state_flags, &border); #endif focus_rect.Inset(border.left, border.top, border.right, border.bottom); } gtk_render_focus(context, cr, focus_rect.x(), focus_rect.y(), focus_rect.width(), focus_rect.height()); } cairo_destroy(cr); cairo_surface_destroy(surface); return gfx::ImageSkiaRep(border, scale); } private: bool focus_; ui::NativeTheme::State state_; int width_; int height_; DISALLOW_COPY_AND_ASSIGN(GtkButtonImageSource); }; class GtkButtonPainter : public views::Painter { public: GtkButtonPainter(bool focus, views::Button::ButtonState button_state) : focus_(focus), button_state_(button_state) {} ~GtkButtonPainter() override = default; gfx::Size GetMinimumSize() const override { return gfx::Size(); } void Paint(gfx::Canvas* canvas, const gfx::Size& size) override { gfx::ImageSkia image( std::make_unique(focus_, button_state_, size), 1); canvas->DrawImageInt(image, 0, 0); } private: const bool focus_; const views::Button::ButtonState button_state_; DISALLOW_COPY_AND_ASSIGN(GtkButtonPainter); }; struct GObjectDeleter { void operator()(void* ptr) { g_object_unref(ptr); } }; struct GtkIconInfoDeleter { void operator()(GtkIconInfo* ptr) { #if GTK_CHECK_VERSION(3, 90, 0) g_object_unref(ptr); #else G_GNUC_BEGIN_IGNORE_DEPRECATIONS gtk_icon_info_free(ptr); G_GNUC_END_IGNORE_DEPRECATIONS #endif } }; typedef std::unique_ptr ScopedGIcon; typedef std::unique_ptr ScopedGtkIconInfo; typedef std::unique_ptr ScopedGdkPixbuf; // Number of app indicators used (used as part of app-indicator id). int indicators_count; // The unknown content type. const char kUnknownContentType[] = "application/octet-stream"; std::unique_ptr CreateSettingsProvider(GtkUi* gtk_ui) { if (GtkCheckVersion(3, 14)) return std::make_unique(gtk_ui); #if defined(USE_GIO) return std::make_unique(gtk_ui); #else return nullptr; #endif } // Returns a gfx::FontRenderParams corresponding to GTK's configuration. gfx::FontRenderParams GetGtkFontRenderParams() { GtkSettings* gtk_settings = gtk_settings_get_default(); CHECK(gtk_settings); gint antialias = 0; gint hinting = 0; gchar* hint_style = nullptr; gchar* rgba = nullptr; g_object_get(gtk_settings, "gtk-xft-antialias", &antialias, "gtk-xft-hinting", &hinting, "gtk-xft-hintstyle", &hint_style, "gtk-xft-rgba", &rgba, nullptr); gfx::FontRenderParams params; params.antialiasing = antialias != 0; if (hinting == 0 || !hint_style || strcmp(hint_style, "hintnone") == 0) { params.hinting = gfx::FontRenderParams::HINTING_NONE; } else if (strcmp(hint_style, "hintslight") == 0) { params.hinting = gfx::FontRenderParams::HINTING_SLIGHT; } else if (strcmp(hint_style, "hintmedium") == 0) { params.hinting = gfx::FontRenderParams::HINTING_MEDIUM; } else if (strcmp(hint_style, "hintfull") == 0) { params.hinting = gfx::FontRenderParams::HINTING_FULL; } else { LOG(WARNING) << "Unexpected gtk-xft-hintstyle \"" << hint_style << "\""; params.hinting = gfx::FontRenderParams::HINTING_NONE; } if (!rgba || strcmp(rgba, "none") == 0) { params.subpixel_rendering = gfx::FontRenderParams::SUBPIXEL_RENDERING_NONE; } else if (strcmp(rgba, "rgb") == 0) { params.subpixel_rendering = gfx::FontRenderParams::SUBPIXEL_RENDERING_RGB; } else if (strcmp(rgba, "bgr") == 0) { params.subpixel_rendering = gfx::FontRenderParams::SUBPIXEL_RENDERING_BGR; } else if (strcmp(rgba, "vrgb") == 0) { params.subpixel_rendering = gfx::FontRenderParams::SUBPIXEL_RENDERING_VRGB; } else if (strcmp(rgba, "vbgr") == 0) { params.subpixel_rendering = gfx::FontRenderParams::SUBPIXEL_RENDERING_VBGR; } else { LOG(WARNING) << "Unexpected gtk-xft-rgba \"" << rgba << "\""; params.subpixel_rendering = gfx::FontRenderParams::SUBPIXEL_RENDERING_NONE; } g_free(hint_style); g_free(rgba); return params; } views::LinuxUI::WindowFrameAction GetDefaultMiddleClickAction() { if (GtkCheckVersion(3, 14)) return views::LinuxUI::WindowFrameAction::kNone; std::unique_ptr env(base::Environment::Create()); switch (base::nix::GetDesktopEnvironment(env.get())) { case base::nix::DESKTOP_ENVIRONMENT_KDE4: case base::nix::DESKTOP_ENVIRONMENT_KDE5: // Starting with KDE 4.4, windows' titlebars can be dragged with the // middle mouse button to create tab groups. We don't support that in // Chrome, but at least avoid lowering windows in response to middle // clicks to avoid surprising users who expect the KDE behavior. return views::LinuxUI::WindowFrameAction::kNone; default: return views::LinuxUI::WindowFrameAction::kLower; } } const SkBitmap GdkPixbufToSkBitmap(GdkPixbuf* pixbuf) { // TODO(erg): What do we do in the case where the pixbuf fails these dchecks? // I would prefer to use our gtk based canvas, but that would require // recompiling half of our skia extensions with gtk support, which we can't // do in this build. DCHECK_EQ(GDK_COLORSPACE_RGB, gdk_pixbuf_get_colorspace(pixbuf)); int n_channels = gdk_pixbuf_get_n_channels(pixbuf); int w = gdk_pixbuf_get_width(pixbuf); int h = gdk_pixbuf_get_height(pixbuf); SkBitmap ret; ret.allocN32Pixels(w, h); ret.eraseColor(0); uint32_t* skia_data = static_cast(ret.getAddr(0, 0)); if (n_channels == 4) { int total_length = w * h; guchar* gdk_pixels = gdk_pixbuf_get_pixels(pixbuf); // Now here's the trick: we need to convert the gdk data (which is RGBA and // isn't premultiplied) to skia (which can be anything and premultiplied). for (int i = 0; i < total_length; ++i, gdk_pixels += 4) { const unsigned char& red = gdk_pixels[0]; const unsigned char& green = gdk_pixels[1]; const unsigned char& blue = gdk_pixels[2]; const unsigned char& alpha = gdk_pixels[3]; skia_data[i] = SkPreMultiplyARGB(alpha, red, green, blue); } } else if (n_channels == 3) { // Because GDK makes rowstrides word aligned, we need to do something a bit // more complex when a pixel isn't perfectly a word of memory. int rowstride = gdk_pixbuf_get_rowstride(pixbuf); guchar* gdk_pixels = gdk_pixbuf_get_pixels(pixbuf); for (int y = 0; y < h; ++y) { int row = y * rowstride; for (int x = 0; x < w; ++x) { guchar* pixel = gdk_pixels + row + (x * 3); const unsigned char& red = pixel[0]; const unsigned char& green = pixel[1]; const unsigned char& blue = pixel[2]; skia_data[y * w + x] = SkPreMultiplyARGB(255, red, green, blue); } } } else { NOTREACHED(); } return ret; } } // namespace GtkUi::GtkUi(ui::GtkUiDelegate* delegate) : delegate_(delegate) { using Action = views::LinuxUI::WindowFrameAction; using ActionSource = views::LinuxUI::WindowFrameActionSource; DCHECK(delegate_); DCHECK(!g_gtk_ui); g_gtk_ui = this; window_frame_actions_ = { {ActionSource::kDoubleClick, Action::kToggleMaximize}, {ActionSource::kMiddleClick, GetDefaultMiddleClickAction()}, {ActionSource::kRightClick, Action::kMenu}}; // Avoid GTK initializing atk-bridge, and let AuraLinux implementation // do it once it is ready. std::unique_ptr env(base::Environment::Create()); env->SetVar("NO_AT_BRIDGE", "1"); GtkInitFromCommandLine(*base::CommandLine::ForCurrentProcess()); native_theme_ = NativeThemeGtk::instance(); fake_window_ = gtk_window_new(GTK_WINDOW_TOPLEVEL); gtk_widget_realize(fake_window_); } GtkUi::~GtkUi() { gtk_widget_destroy(fake_window_); g_gtk_ui = nullptr; } ui::GtkUiDelegate* GtkUi::GetDelegate() { DCHECK(g_gtk_ui) << "GtkUi instance is not set."; return g_gtk_ui->delegate_; } void GtkUi::Initialize() { #if defined(USE_OZONE) // Linux ozone platforms may want to set LinuxInputMethodContextFactory // instance instead of using GtkUi context factory. This step is made upon // CreateInputMethod call. If the factory is not set, use the GtkUi context // factory. if (!ui::OzonePlatform::GetInstance()->CreateInputMethod( nullptr, gfx::kNullAcceleratedWidget)) { DCHECK(!ui::LinuxInputMethodContextFactory::instance()); ui::LinuxInputMethodContextFactory::SetInstance(this); } #endif GtkSettings* settings = gtk_settings_get_default(); g_signal_connect_after(settings, "notify::gtk-theme-name", G_CALLBACK(OnThemeChangedThunk), this); g_signal_connect_after(settings, "notify::gtk-icon-theme-name", G_CALLBACK(OnThemeChangedThunk), this); g_signal_connect_after(settings, "notify::gtk-application-prefer-dark-theme", G_CALLBACK(OnThemeChangedThunk), this); g_signal_connect_after(settings, "notify::gtk-cursor-theme-name", G_CALLBACK(OnCursorThemeNameChangedThunk), this); g_signal_connect_after(settings, "notify::gtk-cursor-theme-size", G_CALLBACK(OnCursorThemeSizeChangedThunk), this); GdkScreen* screen = gdk_screen_get_default(); // Listen for DPI changes. g_signal_connect_after(screen, "notify::resolution", G_CALLBACK(OnDeviceScaleFactorMaybeChangedThunk), this); // Listen for scale factor changes. We would prefer to listen on // |screen|, but there is no scale-factor property, so use an // unmapped window instead. g_signal_connect(fake_window_, "notify::scale-factor", G_CALLBACK(OnDeviceScaleFactorMaybeChangedThunk), this); LoadGtkValues(); #if BUILDFLAG(ENABLE_PRINTING) printing::PrintingContextLinux::SetCreatePrintDialogFunction( &PrintDialogGtk::CreatePrintDialog); printing::PrintingContextLinux::SetPdfPaperSizeFunction( &GetPdfPaperSizeDeviceUnitsGtk); #endif // We must build this after GTK gets initialized. settings_provider_ = CreateSettingsProvider(this); indicators_count = 0; GetDelegate()->OnInitialized(); } bool GtkUi::GetTint(int id, color_utils::HSL* tint) const { switch (id) { // Tints for which the cross-platform default is fine. Before adding new // values here, specifically verify they work well on Linux. case ThemeProperties::TINT_BACKGROUND_TAB: // TODO(estade): Return something useful for TINT_BUTTONS so that chrome:// // page icons are colored appropriately. case ThemeProperties::TINT_BUTTONS: break; default: // Assume any tints not specifically verified on Linux aren't usable. // TODO(pkasting): Try to remove values from |colors_| that could just be // added to the group above instead. NOTREACHED(); } return false; } bool GtkUi::GetColor(int id, SkColor* color, bool use_custom_frame) const { for (const ColorMap& color_map : {colors_, use_custom_frame ? custom_frame_colors_ : native_frame_colors_}) { auto it = color_map.find(id); if (it != color_map.end()) { *color = it->second; return true; } } return false; } bool GtkUi::GetDisplayProperty(int id, int* result) const { if (id == ThemeProperties::SHOULD_FILL_BACKGROUND_TAB_COLOR) { *result = 0; return true; } return false; } SkColor GtkUi::GetFocusRingColor() const { return focus_ring_color_; } SkColor GtkUi::GetActiveSelectionBgColor() const { return active_selection_bg_color_; } SkColor GtkUi::GetActiveSelectionFgColor() const { return active_selection_fg_color_; } SkColor GtkUi::GetInactiveSelectionBgColor() const { return inactive_selection_bg_color_; } SkColor GtkUi::GetInactiveSelectionFgColor() const { return inactive_selection_fg_color_; } base::TimeDelta GtkUi::GetCursorBlinkInterval() const { // From http://library.gnome.org/devel/gtk/unstable/GtkSettings.html, this is // the default value for gtk-cursor-blink-time. static const gint kGtkDefaultCursorBlinkTime = 1200; // Dividing GTK's cursor blink cycle time (in milliseconds) by this value // yields an appropriate value for // blink::mojom::RendererPreferences::caret_blink_interval. static const double kGtkCursorBlinkCycleFactor = 2000.0; gint cursor_blink_time = kGtkDefaultCursorBlinkTime; gboolean cursor_blink = TRUE; g_object_get(gtk_settings_get_default(), "gtk-cursor-blink-time", &cursor_blink_time, "gtk-cursor-blink", &cursor_blink, nullptr); return cursor_blink ? base::TimeDelta::FromSecondsD( cursor_blink_time / kGtkCursorBlinkCycleFactor) : base::TimeDelta(); } ui::NativeTheme* GtkUi::GetNativeTheme(aura::Window* window) const { return (use_system_theme_callback_.is_null() || use_system_theme_callback_.Run(window)) ? native_theme_ : ui::NativeTheme::GetInstanceForNativeUi(); } void GtkUi::SetUseSystemThemeCallback(UseSystemThemeCallback callback) { use_system_theme_callback_ = std::move(callback); } bool GtkUi::GetDefaultUsesSystemTheme() const { std::unique_ptr env(base::Environment::Create()); switch (base::nix::GetDesktopEnvironment(env.get())) { case base::nix::DESKTOP_ENVIRONMENT_CINNAMON: case base::nix::DESKTOP_ENVIRONMENT_GNOME: case base::nix::DESKTOP_ENVIRONMENT_PANTHEON: case base::nix::DESKTOP_ENVIRONMENT_UNITY: case base::nix::DESKTOP_ENVIRONMENT_XFCE: return true; case base::nix::DESKTOP_ENVIRONMENT_KDE3: case base::nix::DESKTOP_ENVIRONMENT_KDE4: case base::nix::DESKTOP_ENVIRONMENT_KDE5: case base::nix::DESKTOP_ENVIRONMENT_OTHER: return false; } // Unless GetDesktopEnvironment() badly misbehaves, this should never happen. NOTREACHED(); return false; } gfx::Image GtkUi::GetIconForContentType(const std::string& content_type, int size) const { // This call doesn't take a reference. GtkIconTheme* theme = gtk_icon_theme_get_default(); std::string content_types[] = {content_type, kUnknownContentType}; for (size_t i = 0; i < base::size(content_types); ++i) { ScopedGIcon icon(g_content_type_get_icon(content_types[i].c_str())); ScopedGtkIconInfo icon_info(gtk_icon_theme_lookup_by_gicon( theme, icon.get(), size, static_cast(GTK_ICON_LOOKUP_FORCE_SIZE))); if (!icon_info) continue; ScopedGdkPixbuf pixbuf(gtk_icon_info_load_icon(icon_info.get(), nullptr)); if (!pixbuf) continue; SkBitmap bitmap = GdkPixbufToSkBitmap(pixbuf.get()); DCHECK_EQ(size, bitmap.width()); DCHECK_EQ(size, bitmap.height()); gfx::ImageSkia image_skia = gfx::ImageSkia::CreateFrom1xBitmap(bitmap); image_skia.MakeThreadSafe(); return gfx::Image(image_skia); } return gfx::Image(); } std::unique_ptr GtkUi::CreateNativeBorder( views::LabelButton* owning_button, std::unique_ptr border) { if (owning_button->GetNativeTheme() != native_theme_) return std::move(border); auto gtk_border = std::make_unique(); gtk_border->set_insets(border->GetInsets()); constexpr bool kFocus = true; static struct { bool focus; views::Button::ButtonState state; } const paintstate[] = { {!kFocus, views::Button::STATE_NORMAL}, {!kFocus, views::Button::STATE_HOVERED}, {!kFocus, views::Button::STATE_PRESSED}, {!kFocus, views::Button::STATE_DISABLED}, {kFocus, views::Button::STATE_NORMAL}, {kFocus, views::Button::STATE_HOVERED}, {kFocus, views::Button::STATE_PRESSED}, {kFocus, views::Button::STATE_DISABLED}, }; for (unsigned i = 0; i < base::size(paintstate); i++) { gtk_border->SetPainter( paintstate[i].focus, paintstate[i].state, border->PaintsButtonState(paintstate[i].focus, paintstate[i].state) ? std::make_unique(paintstate[i].focus, paintstate[i].state) : nullptr); } return std::move(gtk_border); } void GtkUi::AddWindowButtonOrderObserver( views::WindowButtonOrderObserver* observer) { if (nav_buttons_set_) observer->OnWindowButtonOrderingChange(leading_buttons_, trailing_buttons_); window_button_order_observer_list_.AddObserver(observer); } void GtkUi::RemoveWindowButtonOrderObserver( views::WindowButtonOrderObserver* observer) { window_button_order_observer_list_.RemoveObserver(observer); } void GtkUi::SetWindowButtonOrdering( const std::vector& leading_buttons, const std::vector& trailing_buttons) { leading_buttons_ = leading_buttons; trailing_buttons_ = trailing_buttons; nav_buttons_set_ = true; for (views::WindowButtonOrderObserver& observer : window_button_order_observer_list_) { observer.OnWindowButtonOrderingChange(leading_buttons_, trailing_buttons_); } } void GtkUi::SetWindowFrameAction(WindowFrameActionSource source, WindowFrameAction action) { window_frame_actions_[source] = action; } std::unique_ptr GtkUi::CreateInputMethodContext( ui::LinuxInputMethodContextDelegate* delegate, bool is_simple) const { return std::make_unique(delegate, is_simple); } gfx::FontRenderParams GtkUi::GetDefaultFontRenderParams() const { static gfx::FontRenderParams params = GetGtkFontRenderParams(); return params; } void GtkUi::GetDefaultFontDescription(std::string* family_out, int* size_pixels_out, int* style_out, gfx::Font::Weight* weight_out, gfx::FontRenderParams* params_out) const { *family_out = default_font_family_; *size_pixels_out = default_font_size_pixels_; *style_out = default_font_style_; *weight_out = default_font_weight_; *params_out = default_font_render_params_; } ui::SelectFileDialog* GtkUi::CreateSelectFileDialog( ui::SelectFileDialog::Listener* listener, std::unique_ptr policy) const { return SelectFileDialogImpl::Create(listener, std::move(policy)); } views::LinuxUI::WindowFrameAction GtkUi::GetWindowFrameAction( WindowFrameActionSource source) { return window_frame_actions_[source]; } void GtkUi::NotifyWindowManagerStartupComplete() { // TODO(port) Implement this using _NET_STARTUP_INFO_BEGIN/_NET_STARTUP_INFO // from http://standards.freedesktop.org/startup-notification-spec/ instead. gdk_notify_startup_complete(); } void GtkUi::AddDeviceScaleFactorObserver( views::DeviceScaleFactorObserver* observer) { device_scale_factor_observer_list_.AddObserver(observer); } void GtkUi::RemoveDeviceScaleFactorObserver( views::DeviceScaleFactorObserver* observer) { device_scale_factor_observer_list_.RemoveObserver(observer); } bool GtkUi::PreferDarkTheme() const { gboolean dark = false; g_object_get(gtk_settings_get_default(), "gtk-application-prefer-dark-theme", &dark, nullptr); return dark; } bool GtkUi::AnimationsEnabled() const { gboolean animations_enabled = false; g_object_get(gtk_settings_get_default(), "gtk-enable-animations", &animations_enabled, nullptr); return animations_enabled; } std::unique_ptr GtkUi::CreateNavButtonProvider() { if (GtkCheckVersion(3, 14)) return std::make_unique(); return nullptr; } // Mapping from GDK dead keys to corresponding printable character. static struct { guint gdk_key; guint16 unicode; } kDeadKeyMapping[] = { {GDK_KEY_dead_grave, 0x0060}, {GDK_KEY_dead_acute, 0x0027}, {GDK_KEY_dead_circumflex, 0x005e}, {GDK_KEY_dead_tilde, 0x007e}, {GDK_KEY_dead_diaeresis, 0x00a8}, }; base::flat_map GtkUi::GetKeyboardLayoutMap() { GdkDisplay* display = gdk_display_get_default(); GdkKeymap* keymap = gdk_keymap_get_for_display(display); if (!keymap) return {}; ui::DomKeyboardLayoutManager* layouts = new ui::DomKeyboardLayoutManager(); auto map = base::flat_map(); for (unsigned int i_domcode = 0; i_domcode < ui::kWritingSystemKeyDomCodeEntries; ++i_domcode) { ui::DomCode domcode = ui::writing_system_key_domcodes[i_domcode]; guint16 keycode = ui::KeycodeConverter::DomCodeToNativeKeycode(domcode); GdkKeymapKey* keys = nullptr; guint* keyvals = nullptr; gint n_entries = 0; // The order of the layouts is based on the system default ordering in // Keyboard Settings. The currently active layout does not affect this // order. if (gdk_keymap_get_entries_for_keycode(keymap, keycode, &keys, &keyvals, &n_entries)) { for (gint i = 0; i < n_entries; ++i) { // There are 4 entries per layout group, one each for shift level 0..3. // We only care about the unshifted values (level = 0). if (keys[i].level == 0) { uint16_t unicode = gdk_keyval_to_unicode(keyvals[i]); if (unicode == 0) { for (unsigned int i_dead = 0; i_dead < base::size(kDeadKeyMapping); ++i_dead) { if (keyvals[i] == kDeadKeyMapping[i_dead].gdk_key) unicode = kDeadKeyMapping[i_dead].unicode; } } if (unicode != 0) layouts->GetLayout(keys[i].group)->AddKeyMapping(domcode, unicode); } } } g_free(keys); keys = nullptr; g_free(keyvals); keyvals = nullptr; } return layouts->GetFirstAsciiCapableLayout()->GetMap(); } std::string GtkUi::GetCursorThemeName() { gchar* theme = nullptr; g_object_get(gtk_settings_get_default(), "gtk-cursor-theme-name", &theme, nullptr); std::string theme_string; if (theme) { theme_string = theme; g_free(theme); } return theme_string; } int GtkUi::GetCursorThemeSize() { gint size = 0; g_object_get(gtk_settings_get_default(), "gtk-cursor-theme-size", &size, nullptr); return size; } bool GtkUi::MatchEvent(const ui::Event& event, std::vector* commands) { // TODO(crbug.com/963419): Use delegate's |GetGdkKeymap| here to // determine if GtkUi's key binding handling implementation is used or not. // Ozone/Wayland was unintentionally using GtkUi for keybinding handling, so // early out here, for now, until a proper solution for ozone is implemented. if (!GetDelegate()->GetGdkKeymap()) return false; // Ensure that we have a keyboard handler. if (!key_bindings_handler_) key_bindings_handler_ = std::make_unique(); return key_bindings_handler_->MatchEvent(event, commands); } void GtkUi::OnThemeChanged(GtkSettings* settings, GtkParamSpec* param) { colors_.clear(); custom_frame_colors_.clear(); native_frame_colors_.clear(); native_theme_->OnThemeChanged(settings, param); LoadGtkValues(); native_theme_->NotifyObservers(); } void GtkUi::OnCursorThemeNameChanged(GtkSettings* settings, GtkParamSpec* param) { std::string cursor_theme_name = GetCursorThemeName(); if (cursor_theme_name.empty()) return; for (auto& observer : cursor_theme_observers()) observer.OnCursorThemeNameChanged(cursor_theme_name); } void GtkUi::OnCursorThemeSizeChanged(GtkSettings* settings, GtkParamSpec* param) { int cursor_theme_size = GetCursorThemeSize(); if (!cursor_theme_size) return; for (auto& observer : cursor_theme_observers()) observer.OnCursorThemeSizeChanged(cursor_theme_size); } void GtkUi::OnDeviceScaleFactorMaybeChanged(void*, GParamSpec*) { UpdateDeviceScaleFactor(); } void GtkUi::LoadGtkValues() { // TODO(thomasanderson): GtkThemeService had a comment here about having to // muck with the raw Prefs object to remove prefs::kCurrentThemeImages or else // we'd regress startup time. Figure out how to do that when we can't access // the prefs system from here. UpdateDeviceScaleFactor(); UpdateColors(); } void GtkUi::UpdateColors() { SkColor location_bar_border = GetBorderColor("GtkEntry#entry"); if (SkColorGetA(location_bar_border)) colors_[ThemeProperties::COLOR_LOCATION_BAR_BORDER] = location_bar_border; inactive_selection_bg_color_ = GetSelectionBgColor( GtkCheckVersion(3, 20) ? "GtkTextView#textview.view:backdrop " "#text:backdrop #selection:backdrop" : "GtkTextView.view:selected:backdrop"); inactive_selection_fg_color_ = GetFgColor(GtkCheckVersion(3, 20) ? "GtkTextView#textview.view:backdrop " "#text:backdrop #selection:backdrop" : "GtkTextView.view:selected:backdrop"); SkColor tab_border = GetBorderColor("GtkButton#button"); // Separates the toolbar from the bookmark bar or butter bars. colors_[ThemeProperties::COLOR_TOOLBAR_CONTENT_AREA_SEPARATOR] = tab_border; // Separates entries in the downloads bar. colors_[ThemeProperties::COLOR_TOOLBAR_VERTICAL_SEPARATOR] = tab_border; colors_[ThemeProperties::COLOR_NTP_BACKGROUND] = native_theme_->GetSystemColor( ui::NativeTheme::kColorId_TextfieldDefaultBackground); colors_[ThemeProperties::COLOR_NTP_TEXT] = native_theme_->GetSystemColor( ui::NativeTheme::kColorId_TextfieldDefaultColor); colors_[ThemeProperties::COLOR_NTP_HEADER] = GetBorderColor("GtkButton#button"); SkColor tab_text_color = GetFgColor("GtkLabel"); colors_[ThemeProperties::COLOR_TOOLBAR_BUTTON_ICON] = tab_text_color; colors_[ThemeProperties::COLOR_TOOLBAR_BUTTON_ICON_HOVERED] = tab_text_color; colors_[ThemeProperties::COLOR_TOOLBAR_BUTTON_ICON_PRESSED] = tab_text_color; colors_[ThemeProperties::COLOR_TAB_FOREGROUND_ACTIVE_FRAME_ACTIVE] = tab_text_color; colors_[ThemeProperties::COLOR_TAB_FOREGROUND_ACTIVE_FRAME_INACTIVE] = tab_text_color; colors_[ThemeProperties::COLOR_BOOKMARK_TEXT] = tab_text_color; colors_[ThemeProperties::COLOR_NTP_LINK] = native_theme_->GetSystemColor( ui::NativeTheme::kColorId_TextfieldSelectionBackgroundFocused); // Generate the colors that we pass to Blink. focus_ring_color_ = native_theme_->GetSystemColor( ui::NativeTheme::kColorId_FocusedBorderColor); // Some GTK themes only define the text selection colors on the GtkEntry // class, so we need to use that for getting selection colors. active_selection_bg_color_ = native_theme_->GetSystemColor( ui::NativeTheme::kColorId_TextfieldSelectionBackgroundFocused); active_selection_fg_color_ = native_theme_->GetSystemColor( ui::NativeTheme::kColorId_TextfieldSelectionColor); colors_[ThemeProperties::COLOR_TAB_THROBBER_SPINNING] = native_theme_->GetSystemColor( ui::NativeTheme::kColorId_ThrobberSpinningColor); colors_[ThemeProperties::COLOR_TAB_THROBBER_WAITING] = native_theme_->GetSystemColor( ui::NativeTheme::kColorId_ThrobberWaitingColor); // Generate colors that depend on whether or not a custom window frame is // used. These colors belong in |color_map| below, not |colors_|. for (bool custom_frame : {false, true}) { ColorMap& color_map = custom_frame ? custom_frame_colors_ : native_frame_colors_; const std::string header_selector = custom_frame ? "#headerbar.header-bar.titlebar" : "GtkMenuBar#menubar"; const std::string header_selector_inactive = header_selector + ":backdrop"; const SkColor frame_color = SkColorSetA(GetBgColor(header_selector), SK_AlphaOPAQUE); const SkColor frame_color_incognito = color_utils::HSLShift(frame_color, kDefaultTintFrameIncognito); const SkColor frame_color_inactive = SkColorSetA(GetBgColor(header_selector_inactive), SK_AlphaOPAQUE); const SkColor frame_color_incognito_inactive = color_utils::HSLShift(frame_color_inactive, kDefaultTintFrameIncognito); color_map[ThemeProperties::COLOR_FRAME_ACTIVE] = frame_color; color_map[ThemeProperties::COLOR_FRAME_INACTIVE] = frame_color_inactive; color_map[ThemeProperties::COLOR_FRAME_ACTIVE_INCOGNITO] = frame_color_incognito; color_map[ThemeProperties::COLOR_FRAME_INACTIVE_INCOGNITO] = frame_color_incognito_inactive; // Compose the window color on the frame color to ensure the resulting tab // color is opaque. SkColor tab_color = color_utils::GetResultingPaintColor(GetBgColor(""), frame_color); color_map[ThemeProperties::COLOR_TOOLBAR] = tab_color; color_map[ThemeProperties::COLOR_DOWNLOAD_SHELF] = tab_color; color_map[ThemeProperties::COLOR_INFOBAR] = tab_color; color_map[ThemeProperties::COLOR_STATUS_BUBBLE] = tab_color; color_map[ThemeProperties::COLOR_TAB_BACKGROUND_ACTIVE_FRAME_ACTIVE] = tab_color; color_map[ThemeProperties::COLOR_TAB_BACKGROUND_ACTIVE_FRAME_INACTIVE] = tab_color; const SkColor background_tab_text_color = GetFgColor(header_selector + " GtkLabel.title"); const SkColor background_tab_text_color_inactive = GetFgColor(header_selector_inactive + " GtkLabel.title"); color_map[ThemeProperties::COLOR_TAB_FOREGROUND_INACTIVE_FRAME_ACTIVE] = background_tab_text_color; color_map[ThemeProperties:: COLOR_TAB_FOREGROUND_INACTIVE_FRAME_ACTIVE_INCOGNITO] = color_utils::BlendForMinContrast( color_utils::HSLShift(background_tab_text_color, kDefaultTintFrameIncognito), frame_color_incognito) .color; color_map[ThemeProperties::COLOR_TAB_FOREGROUND_INACTIVE_FRAME_INACTIVE] = background_tab_text_color_inactive; color_map[ThemeProperties:: COLOR_TAB_FOREGROUND_INACTIVE_FRAME_INACTIVE_INCOGNITO] = color_utils::BlendForMinContrast( color_utils::HSLShift(background_tab_text_color_inactive, kDefaultTintFrameIncognito), frame_color_incognito_inactive) .color; color_map[ThemeProperties::COLOR_OMNIBOX_TEXT] = native_theme_->GetSystemColor( ui::NativeTheme::kColorId_TextfieldDefaultColor); color_map[ThemeProperties::COLOR_OMNIBOX_BACKGROUND] = native_theme_->GetSystemColor( ui::NativeTheme::kColorId_TextfieldDefaultBackground); // These colors represent the border drawn around tabs and between // the tabstrip and toolbar. SkColor toolbar_top_separator = GetBorderColor( header_selector + " GtkSeparator#separator.vertical.titlebutton"); SkColor toolbar_top_separator_inactive = GetBorderColor(header_selector + ":backdrop GtkSeparator#separator.vertical.titlebutton"); auto toolbar_top_separator_has_good_contrast = [&]() { // This constant is copied from chrome/browser/themes/theme_service.cc. const float kMinContrastRatio = 2.f; SkColor active = color_utils::GetResultingPaintColor( toolbar_top_separator, frame_color); SkColor inactive = color_utils::GetResultingPaintColor( toolbar_top_separator_inactive, frame_color_inactive); return color_utils::GetContrastRatio(frame_color, active) >= kMinContrastRatio && color_utils::GetContrastRatio(frame_color_inactive, inactive) >= kMinContrastRatio; }; if (!toolbar_top_separator_has_good_contrast()) { toolbar_top_separator = GetBorderColor(header_selector + " GtkButton#button"); toolbar_top_separator_inactive = GetBorderColor(header_selector + ":backdrop GtkButton#button"); } // If we can't get a contrasting stroke from the theme, have ThemeService // provide a stroke color for us. if (toolbar_top_separator_has_good_contrast()) { color_map[ThemeProperties::COLOR_TOOLBAR_TOP_SEPARATOR] = toolbar_top_separator; color_map[ThemeProperties::COLOR_TOOLBAR_TOP_SEPARATOR_INACTIVE] = toolbar_top_separator_inactive; } } } void GtkUi::UpdateDefaultFont() { gfx::SetFontRenderParamsDeviceScaleFactor(device_scale_factor_); GtkWidget* fake_label = gtk_label_new(nullptr); g_object_ref_sink(fake_label); // Remove the floating reference. PangoContext* pc = gtk_widget_get_pango_context(fake_label); const PangoFontDescription* desc = pango_context_get_font_description(pc); // Use gfx::FontRenderParams to select a family and determine the rendering // settings. gfx::FontRenderParamsQuery query; query.families = base::SplitString(pango_font_description_get_family(desc), ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL); if (pango_font_description_get_size_is_absolute(desc)) { // If the size is absolute, it's specified in Pango units. There are // PANGO_SCALE Pango units in a device unit (pixel). const int size_pixels = pango_font_description_get_size(desc) / PANGO_SCALE; default_font_size_pixels_ = size_pixels; query.pixel_size = size_pixels; } else { // Non-absolute sizes are in points (again scaled by PANGO_SIZE). // Round the value when converting to pixels to match GTK's logic. const double size_points = pango_font_description_get_size(desc) / static_cast(PANGO_SCALE); default_font_size_pixels_ = static_cast(kDefaultDPI / 72.0 * size_points + 0.5); query.point_size = static_cast(size_points); } query.style = gfx::Font::NORMAL; query.weight = static_cast(pango_font_description_get_weight(desc)); // TODO(davemoore): What about PANGO_STYLE_OBLIQUE? if (pango_font_description_get_style(desc) == PANGO_STYLE_ITALIC) query.style |= gfx::Font::ITALIC; default_font_render_params_ = gfx::GetFontRenderParams(query, &default_font_family_); default_font_style_ = query.style; gtk_widget_destroy(fake_label); g_object_unref(fake_label); } float GtkUi::GetRawDeviceScaleFactor() { if (display::Display::HasForceDeviceScaleFactor()) return display::Display::GetForcedDeviceScaleFactor(); GdkScreen* screen = gdk_screen_get_default(); float scale = gtk_widget_get_scale_factor(fake_window_); DCHECK_GT(scale, 0.0); gdouble resolution = gdk_screen_get_resolution(screen); // TODO(https://crbug.com/1033552): Remove this hack once the Trusty bots are // fixed to have a resolution of 96, or when the Trusty bots are removed // altogether. if (std::abs(resolution - 95.8486) < 0.001) resolution = 96; if (resolution > 0) scale *= resolution / kDefaultDPI; return scale; } void GtkUi::UpdateDeviceScaleFactor() { float old_device_scale_factor = device_scale_factor_; device_scale_factor_ = GetRawDeviceScaleFactor(); if (device_scale_factor_ != old_device_scale_factor) { for (views::DeviceScaleFactorObserver& observer : device_scale_factor_observer_list_) { observer.OnDeviceScaleFactorChanged(); } } UpdateDefaultFont(); } float GtkUi::GetDeviceScaleFactor() const { return device_scale_factor_; } } // namespace gtk views::LinuxUI* BuildGtkUi(ui::GtkUiDelegate* delegate) { return new gtk::GtkUi(delegate); }