// 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 "chrome/browser/ui/gtk/extensions/shell_window_gtk.h" #include "base/utf_string_conversions.h" #include "chrome/browser/profiles/profile.h" #include "chrome/browser/ui/gtk/extensions/extension_keybinding_registry_gtk.h" #include "chrome/browser/ui/gtk/gtk_util.h" #include "chrome/browser/ui/gtk/gtk_window_util.h" #include "chrome/browser/web_applications/web_app.h" #include "chrome/common/extensions/extension.h" #include "content/public/browser/render_view_host.h" #include "content/public/browser/render_widget_host_view.h" #include "content/public/browser/web_contents.h" #include "content/public/browser/web_contents_view.h" #include "ui/base/x/active_window_watcher_x.h" #include "ui/gfx/image/image.h" #include "ui/gfx/rect.h" namespace { // The timeout in milliseconds before we'll get the true window position with // gtk_window_get_position() after the last GTK configure-event signal. const int kDebounceTimeoutMilliseconds = 100; } // namespace ShellWindowGtk::ShellWindowGtk(ShellWindow* shell_window, const ShellWindow::CreateParams& params) : shell_window_(shell_window), window_(NULL), state_(GDK_WINDOW_STATE_WITHDRAWN), is_active_(false), content_thinks_its_fullscreen_(false), frameless_(params.frame == ShellWindow::CreateParams::FRAME_NONE) { window_ = GTK_WINDOW(gtk_window_new(GTK_WINDOW_TOPLEVEL)); gfx::NativeView native_view = web_contents()->GetView()->GetNativeView(); gtk_container_add(GTK_CONTAINER(window_), native_view); if (params.bounds.x() != INT_MIN && params.bounds.y() != INT_MIN) gtk_window_move(window_, params.bounds.x(), params.bounds.y()); // This is done to avoid a WM "feature" where setting the window size to // the monitor size causes the WM to set the EWMH for full screen mode. if (frameless_ && gtk_window_util::BoundsMatchMonitorSize(window_, params.bounds)) { gtk_window_set_default_size( window_, params.bounds.width(), params.bounds.height() - 1); } else { gtk_window_set_default_size( window_, params.bounds.width(), params.bounds.height()); } // make sure bounds_ and restored_bounds_ have correct values until we // get our first configure-event bounds_ = restored_bounds_ = params.bounds; gint x, y; gtk_window_get_position(window_, &x, &y); bounds_.set_origin(gfx::Point(x, y)); // Hide titlebar when {frame: 'none'} specified on ShellWindow. if (frameless_) gtk_window_set_decorated(window_, false); int min_width = params.minimum_size.width(); int min_height = params.minimum_size.height(); int max_width = params.maximum_size.width(); int max_height = params.maximum_size.height(); GdkGeometry hints; int hints_mask = 0; if (min_width || min_height) { hints.min_height = min_height; hints.min_width = min_width; hints_mask |= GDK_HINT_MIN_SIZE; } if (max_width || max_height) { hints.max_height = max_height ? max_height : G_MAXINT; hints.max_width = max_width ? max_width : G_MAXINT; hints_mask |= GDK_HINT_MAX_SIZE; } if (hints_mask) { gtk_window_set_geometry_hints( window_, GTK_WIDGET(window_), &hints, static_cast(hints_mask)); } // In some (older) versions of compiz, raising top-level windows when they // are partially off-screen causes them to get snapped back on screen, not // always even on the current virtual desktop. If we are running under // compiz, suppress such raises, as they are not necessary in compiz anyway. if (ui::GuessWindowManager() == ui::WM_COMPIZ) suppress_window_raise_ = true; gtk_window_set_title(window_, extension()->name().c_str()); gtk_window_util::SetWindowCustomClass(window_, web_app::GetWMClassFromAppName(extension()->name())); g_signal_connect(window_, "delete-event", G_CALLBACK(OnMainWindowDeleteEventThunk), this); g_signal_connect(window_, "configure-event", G_CALLBACK(OnConfigureThunk), this); g_signal_connect(window_, "window-state-event", G_CALLBACK(OnWindowStateThunk), this); if (frameless_) { g_signal_connect(window_, "button-press-event", G_CALLBACK(OnButtonPressThunk), this); } // Add the keybinding registry. extension_keybinding_registry_.reset(new ExtensionKeybindingRegistryGtk( shell_window_->profile(), window_, extensions::ExtensionKeybindingRegistry::PLATFORM_APPS_ONLY, shell_window_)); ui::ActiveWindowWatcherX::AddObserver(this); } ShellWindowGtk::~ShellWindowGtk() { ui::ActiveWindowWatcherX::RemoveObserver(this); } bool ShellWindowGtk::IsActive() const { if (ui::ActiveWindowWatcherX::WMSupportsActivation()) return is_active_; // This still works even though we don't get the activation notification. return gtk_window_is_active(window_); } bool ShellWindowGtk::IsMaximized() const { return (state_ & GDK_WINDOW_STATE_MAXIMIZED); } bool ShellWindowGtk::IsMinimized() const { return (state_ & GDK_WINDOW_STATE_ICONIFIED); } bool ShellWindowGtk::IsFullscreen() const { return false; } gfx::NativeWindow ShellWindowGtk::GetNativeWindow() { return window_; } gfx::Rect ShellWindowGtk::GetRestoredBounds() const { return restored_bounds_; } gfx::Rect ShellWindowGtk::GetBounds() const { return bounds_; } void ShellWindowGtk::Show() { gtk_window_present(window_); } void ShellWindowGtk::ShowInactive() { gtk_window_set_focus_on_map(window_, false); gtk_widget_show(GTK_WIDGET(window_)); } void ShellWindowGtk::Hide() { gtk_widget_hide(GTK_WIDGET(window_)); } void ShellWindowGtk::Close() { shell_window_->OnNativeWindowChanged(); // Cancel any pending callback from the window configure debounce timer. window_configure_debounce_timer_.Stop(); GtkWidget* window = GTK_WIDGET(window_); // To help catch bugs in any event handlers that might get fired during the // destruction, set window_ to NULL before any handlers will run. window_ = NULL; // OnNativeClose does a delete this so no other members should // be accessed after. gtk_widget_destroy is safe (and must // be last). shell_window_->OnNativeClose(); gtk_widget_destroy(window); } void ShellWindowGtk::Activate() { gtk_window_present(window_); } void ShellWindowGtk::Deactivate() { gdk_window_lower(gtk_widget_get_window(GTK_WIDGET(window_))); } void ShellWindowGtk::Maximize() { gtk_window_maximize(window_); } void ShellWindowGtk::Minimize() { gtk_window_iconify(window_); shell_window_->OnNativeWindowChanged(); } void ShellWindowGtk::Restore() { if (IsMaximized()) gtk_window_unmaximize(window_); else if (IsMinimized()) gtk_window_deiconify(window_); shell_window_->OnNativeWindowChanged(); } void ShellWindowGtk::SetBounds(const gfx::Rect& bounds) { gtk_window_move(window_, bounds.x(), bounds.y()); gtk_window_util::SetWindowSize(window_, gfx::Size(bounds.width(), bounds.height())); } void ShellWindowGtk::FlashFrame(bool flash) { gtk_window_set_urgency_hint(window_, flash); } bool ShellWindowGtk::IsAlwaysOnTop() const { return false; } void ShellWindowGtk::ActiveWindowChanged(GdkWindow* active_window) { // Do nothing if we're in the process of closing the browser window. if (!window_) return; is_active_ = gtk_widget_get_window(GTK_WIDGET(window_)) == active_window; } // Callback for the delete event. This event is fired when the user tries to // close the window (e.g., clicking on the X in the window manager title bar). gboolean ShellWindowGtk::OnMainWindowDeleteEvent(GtkWidget* widget, GdkEvent* event) { Close(); // Return true to prevent the GTK window from being destroyed. Close will // destroy it for us. return TRUE; } gboolean ShellWindowGtk::OnConfigure(GtkWidget* widget, GdkEventConfigure* event) { // We update |bounds_| but not |restored_bounds_| here. The latter needs // to be updated conditionally when the window is non-maximized and non- // fullscreen, but whether those state updates have been processed yet is // window-manager specific. We update |restored_bounds_| in the debounced // handler below, after the window state has been updated. bounds_.SetRect(event->x, event->y, event->width, event->height); // The GdkEventConfigure* we get here doesn't have quite the right // coordinates though (they're relative to the drawable window area, rather // than any window manager decorations, if enabled), so we need to call // gtk_window_get_position() to get the right values. (Otherwise session // restore, if enabled, will restore windows to incorrect positions.) That's // a round trip to the X server though, so we set a debounce timer and only // call it (in OnDebouncedBoundsChanged() below) after we haven't seen a // reconfigure event in a short while. // We don't use Reset() because the timer may not yet be running. // (In that case Stop() is a no-op.) window_configure_debounce_timer_.Stop(); window_configure_debounce_timer_.Start(FROM_HERE, base::TimeDelta::FromMilliseconds(kDebounceTimeoutMilliseconds), this, &ShellWindowGtk::OnDebouncedBoundsChanged); return FALSE; } void ShellWindowGtk::OnDebouncedBoundsChanged() { gtk_window_util::UpdateWindowPosition(this, &bounds_, &restored_bounds_); shell_window_->OnNativeWindowChanged(); } gboolean ShellWindowGtk::OnWindowState(GtkWidget* sender, GdkEventWindowState* event) { state_ = event->new_window_state; if (content_thinks_its_fullscreen_ && !(state_ & GDK_WINDOW_STATE_FULLSCREEN)) { content_thinks_its_fullscreen_ = false; content::RenderViewHost* rvh = web_contents()->GetRenderViewHost(); if (rvh) rvh->ExitFullscreen(); } shell_window_->OnNativeWindowChanged(); return FALSE; } gboolean ShellWindowGtk::OnButtonPress(GtkWidget* widget, GdkEventButton* event) { if (draggable_region_ && draggable_region_->contains(event->x, event->y)) { if (event->button == 1) { if (GDK_BUTTON_PRESS == event->type) { if (!suppress_window_raise_) gdk_window_raise(GTK_WIDGET(widget)->window); return gtk_window_util::HandleTitleBarLeftMousePress( GTK_WINDOW(widget), bounds_, event); } else if (GDK_2BUTTON_PRESS == event->type) { bool is_maximized = gdk_window_get_state(GTK_WIDGET(widget)->window) & GDK_WINDOW_STATE_MAXIMIZED; if (is_maximized) { gtk_window_util::UnMaximize(GTK_WINDOW(widget), bounds_, restored_bounds_); } else { gtk_window_maximize(GTK_WINDOW(widget)); } return TRUE; } } else if (event->button == 2) { gdk_window_lower(GTK_WIDGET(widget)->window); return TRUE; } } return FALSE; } void ShellWindowGtk::SetFullscreen(bool fullscreen) { content_thinks_its_fullscreen_ = fullscreen; if (fullscreen) gtk_window_fullscreen(window_); else gtk_window_unfullscreen(window_); } bool ShellWindowGtk::IsFullscreenOrPending() const { return content_thinks_its_fullscreen_; } void ShellWindowGtk::UpdateWindowIcon() { Profile* profile = shell_window_->profile(); gfx::Image app_icon = shell_window_->app_icon(); if (!app_icon.IsEmpty()) gtk_util::SetWindowIcon(window_, profile, app_icon.ToGdkPixbuf()); else gtk_util::SetWindowIcon(window_, profile); } void ShellWindowGtk::UpdateWindowTitle() { string16 title = shell_window_->GetTitle(); gtk_window_set_title(window_, UTF16ToUTF8(title).c_str()); } void ShellWindowGtk::HandleKeyboardEvent( const content::NativeWebKeyboardEvent& event) { // No-op. } void ShellWindowGtk::UpdateDraggableRegions( const std::vector& regions) { // Draggable region is not supported for non-frameless window. if (!frameless_) return; draggable_region_.reset(ShellWindow::RawDraggableRegionsToSkRegion(regions)); } // static NativeShellWindow* NativeShellWindow::Create( ShellWindow* shell_window, const ShellWindow::CreateParams& params) { return new ShellWindowGtk(shell_window, params); }