diff options
author | tc@google.com <tc@google.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-06-22 18:42:29 +0000 |
---|---|---|
committer | tc@google.com <tc@google.com@0039d316-1c4b-4281-b951-d872f2087c98> | 2009-06-22 18:42:29 +0000 |
commit | 359a5bf69945cfe2cc0fce78e851bf50f48b270b (patch) | |
tree | cfd2aab36c7e63226fff23330b39e94907238755 /chrome | |
parent | c57a65632f3e4cabc45f380b77d7141c7f9b0a27 (diff) | |
download | chromium_src-359a5bf69945cfe2cc0fce78e851bf50f48b270b.zip chromium_src-359a5bf69945cfe2cc0fce78e851bf50f48b270b.tar.gz chromium_src-359a5bf69945cfe2cc0fce78e851bf50f48b270b.tar.bz2 |
Add the ability to resize the window when over the custom frame.
I also switched from 3px borders to 4px borders because that's what they are
on windows. On linux, the borders look bigger because our webcontent area
doesn't drop a shadow.
For this to work for resizing on top, I had to remove the event box from
the browser titlebar and move the mouse event handling to the window.
BUG=14645
Review URL: http://codereview.chromium.org/140026
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@18927 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'chrome')
-rw-r--r-- | chrome/browser/gtk/browser_titlebar.cc | 52 | ||||
-rw-r--r-- | chrome/browser/gtk/browser_titlebar.h | 14 | ||||
-rw-r--r-- | chrome/browser/gtk/browser_toolbar_gtk.h | 5 | ||||
-rw-r--r-- | chrome/browser/gtk/browser_window_gtk.cc | 219 | ||||
-rw-r--r-- | chrome/browser/gtk/browser_window_gtk.h | 21 | ||||
-rwxr-xr-x | chrome/browser/gtk/tabs/tab_renderer_gtk.cc | 7 |
6 files changed, 250 insertions, 68 deletions
diff --git a/chrome/browser/gtk/browser_titlebar.cc b/chrome/browser/gtk/browser_titlebar.cc index 578cef1..ff4e329 100644 --- a/chrome/browser/gtk/browser_titlebar.cc +++ b/chrome/browser/gtk/browser_titlebar.cc @@ -28,8 +28,15 @@ const int kTitlebarHeight = 14; // A linux specific menu item for toggling window decorations. const int kShowWindowDecorationsCommand = 200; +gboolean OnMouseMoveEvent(GtkWidget* widget, GdkEventMotion* event, + BrowserWindowGtk* browser_window) { + // Reset to the default mouse cursor. + browser_window->ResetCustomFrameCursor(); + return TRUE; } +} // namespace + BrowserTitlebar::BrowserTitlebar(BrowserWindowGtk* browser_window, GtkWindow* window) : browser_window_(browser_window), window_(window) { @@ -44,11 +51,9 @@ void BrowserTitlebar::Init() { browser_window_->browser()->profile()->GetThemeProvider(), 0, IDR_THEME_FRAME_INCOGNITO, 0, 0, 0, 0, 0, 0, 0)); - // The widget hierarchy is shown below. In addition to the diagram, there is - // a gtk event box surrounding the titlebar_hbox which catches mouse events - // in the titlebar. + // The widget hierarchy is shown below. // - // +- HBox (titlebar_hbox) -----------------------------------------------+ + // +- HBox (container_) --------------------------------------------------+ // |+- Alignment (titlebar_alignment_)-++- VBox (titlebar_buttons_box_) -+| // || ||+- HBox -----------------------+|| // || |||+- button -++- button -+ ||| @@ -57,20 +62,16 @@ void BrowserTitlebar::Init() { // ||+--------------------------------+||+------------------------------+|| // |+----------------------------------++--------------------------------+| // +----------------------------------------------------------------------+ - container_ = gtk_event_box_new(); - GtkWidget* titlebar_hbox = gtk_hbox_new(FALSE, 0); - gtk_container_add(GTK_CONTAINER(container_), titlebar_hbox); + container_ = gtk_hbox_new(FALSE, 0); - g_signal_connect(G_OBJECT(container_), "button-press-event", - G_CALLBACK(OnMouseButtonPress), this); - g_signal_connect(G_OBJECT(titlebar_hbox), "expose-event", + g_signal_connect(G_OBJECT(container_), "expose-event", G_CALLBACK(OnExpose), this); g_signal_connect(window_, "window-state-event", G_CALLBACK(OnWindowStateChanged), this); // We use an alignment to control the titlebar height. titlebar_alignment_ = gtk_alignment_new(0.0, 0.0, 1.0, 1.0); - gtk_box_pack_start(GTK_BOX(titlebar_hbox), titlebar_alignment_, TRUE, + gtk_box_pack_start(GTK_BOX(container_), titlebar_alignment_, TRUE, TRUE, 0); // Put the tab strip in the titlebar. @@ -96,7 +97,7 @@ void BrowserTitlebar::Init() { IDR_MINIMIZE_H, buttons_hbox, IDS_XPFRAME_MINIMIZE_TOOLTIP)); - gtk_box_pack_end(GTK_BOX(titlebar_hbox), titlebar_buttons_box_, FALSE, + gtk_box_pack_end(GTK_BOX(container_), titlebar_buttons_box_, FALSE, FALSE, 0); gtk_widget_show_all(container_); @@ -106,8 +107,11 @@ CustomDrawButton* BrowserTitlebar::BuildTitlebarButton(int image, int image_pressed, int image_hot, GtkWidget* box, int tooltip) { CustomDrawButton* button = new CustomDrawButton(image, image_pressed, image_hot, 0); + gtk_widget_add_events(GTK_WIDGET(button->widget()), GDK_POINTER_MOTION_MASK); g_signal_connect(button->widget(), "clicked", G_CALLBACK(OnButtonClicked), this); + g_signal_connect(button->widget(), "motion-notify-event", + G_CALLBACK(OnMouseMoveEvent), browser_window_); std::string localized_tooltip = l10n_util::GetStringUTF8(tooltip); gtk_widget_set_tooltip_text(button->widget(), localized_tooltip.c_str()); @@ -141,30 +145,6 @@ gboolean BrowserTitlebar::OnExpose(GtkWidget* widget, GdkEventExpose* e, return FALSE; // Allow subwidgets to paint. } -gboolean BrowserTitlebar::OnMouseButtonPress(GtkWidget* widget, - GdkEventButton* event, BrowserTitlebar* titlebar) { - if (1 == event->button) { - if (GDK_BUTTON_PRESS == event->type) { - gtk_window_begin_move_drag(GTK_WINDOW(titlebar->window_), - event->button, event->x_root, event->y_root, event->time); - return TRUE; - } else if (GDK_2BUTTON_PRESS == event->type) { - // Maximize/restore on double click. - if (titlebar->browser_window_->IsMaximized()) { - gtk_window_unmaximize(titlebar->window_); - } else { - gtk_window_maximize(titlebar->window_); - } - return TRUE; - } - } else if (3 == event->button) { - titlebar->ShowContextMenu(); - return TRUE; - } - - return FALSE; // Continue to propagate the event. -} - gboolean BrowserTitlebar::OnWindowStateChanged(GtkWindow* window, GdkEventWindowState* event, BrowserTitlebar* titlebar) { // Update the maximize/restore button. diff --git a/chrome/browser/gtk/browser_titlebar.h b/chrome/browser/gtk/browser_titlebar.h index c937f85..b491625 100644 --- a/chrome/browser/gtk/browser_titlebar.h +++ b/chrome/browser/gtk/browser_titlebar.h @@ -34,6 +34,11 @@ class BrowserTitlebar : public MenuGtk::Delegate { // tall titlebar and the min/max/close buttons. void UpdateCustomFrame(bool use_custom_frame); + // On Windows, right clicking in the titlebar background brings up the system + // menu. There's no such thing on linux, so we just show the menu items we + // add to the menu. + void ShowContextMenu(); + private: // Build the titlebar, the space above the tab // strip, and (maybe) the min, max, close buttons. |container| is the gtk @@ -51,11 +56,6 @@ class BrowserTitlebar : public MenuGtk::Delegate { static gboolean OnExpose(GtkWidget* widget, GdkEventExpose* e, BrowserTitlebar* window); - // Callback for when the titlebar (include the background of the tab strip) - // needs to be redrawn. - static gboolean OnMouseButtonPress(GtkWidget* widget, GdkEventButton* e, - BrowserTitlebar* window); - // Callback for changes to window state. This includes // maximizing/restoring/minimizing the window. static gboolean OnWindowStateChanged(GtkWindow* window, @@ -67,10 +67,6 @@ class BrowserTitlebar : public MenuGtk::Delegate { // -- Context Menu ----------------------------------------------------------- - // On Windows, right clicking in the titlebar background brings up the system - // menu. There's no such thing on linux, so we just show the menu items we - // add to the menu. - void ShowContextMenu(); // MenuGtk::Delegate implementation: virtual bool IsCommandEnabled(int command_id) const; virtual bool IsItemChecked(int command_id) const; diff --git a/chrome/browser/gtk/browser_toolbar_gtk.h b/chrome/browser/gtk/browser_toolbar_gtk.h index a636905..3671c14 100644 --- a/chrome/browser/gtk/browser_toolbar_gtk.h +++ b/chrome/browser/gtk/browser_toolbar_gtk.h @@ -47,6 +47,11 @@ class BrowserToolbarGtk : public CommandUpdater::CommandObserver, void Show(); void Hide(); + // Getter for the containing widget. + GtkWidget* widget() { + return toolbar_; + } + virtual LocationBar* GetLocationBar() const; GoButtonGtk* GetGoButton() { return go_.get(); } diff --git a/chrome/browser/gtk/browser_window_gtk.cc b/chrome/browser/gtk/browser_window_gtk.cc index c87377b..4342ff6 100644 --- a/chrome/browser/gtk/browser_window_gtk.cc +++ b/chrome/browser/gtk/browser_window_gtk.cc @@ -60,8 +60,18 @@ const int kLoadingAnimationFrameTimeMs = 30; const char* kBrowserWindowKey = "__BROWSER_WINDOW_GTK__"; -// The width of the custom frame. -const int kCustomFrameWidth = 3; +// The frame border is only visible in restored mode and is hardcoded to 4 px +// on each side regardless of the system window border size. +const int kFrameBorderThickness = 4; +// While resize areas on Windows are normally the same size as the window +// borders, our top area is shrunk by 1 px to make it easier to move the window +// around with our thinner top grabbable strip. (Incidentally, our side and +// bottom resize areas don't match the frame border thickness either -- they +// span the whole nonclient area, so there's no "dead zone" for the mouse.) +const int kTopResizeAdjust = 1; +// In the window corners, the resize areas don't actually expand bigger, but +// the 16 px at the end of each edge triggers diagonal resizing. +const int kResizeAreaCornerSize = 16; gboolean MainWindowConfigured(GtkWindow* window, GdkEventConfigure* event, BrowserWindowGtk* browser_win) { @@ -268,25 +278,35 @@ gboolean OnKeyPress(GtkWindow* window, GdkEventKey* event, Browser* browser) { } } -gboolean OnButtonPressEvent(GtkWidget* widget, GdkEventButton* event, - Browser* browser) { - // TODO(jhawkins): Investigate the possibility of the button numbers being - // different for other mice. - if (event->button == 8) { - browser->GoBack(CURRENT_TAB); - return TRUE; - } else if (event->button == 9) { - browser->GoForward(CURRENT_TAB); - return TRUE; - } - return FALSE; -} - gboolean OnFocusIn(GtkWidget* widget, GdkEventFocus* event, Browser* browser) { BrowserList::SetLastActive(browser); return FALSE; } +GdkCursorType GdkWindowEdgeToGdkCursorType(GdkWindowEdge edge) { + switch (edge) { + case GDK_WINDOW_EDGE_NORTH_WEST: + return GDK_TOP_LEFT_CORNER; + case GDK_WINDOW_EDGE_NORTH: + return GDK_TOP_SIDE; + case GDK_WINDOW_EDGE_NORTH_EAST: + return GDK_TOP_RIGHT_CORNER; + case GDK_WINDOW_EDGE_WEST: + return GDK_LEFT_SIDE; + case GDK_WINDOW_EDGE_EAST: + return GDK_RIGHT_SIDE; + case GDK_WINDOW_EDGE_SOUTH_WEST: + return GDK_BOTTOM_LEFT_CORNER; + case GDK_WINDOW_EDGE_SOUTH: + return GDK_BOTTOM_SIDE; + case GDK_WINDOW_EDGE_SOUTH_EAST: + return GDK_BOTTOM_RIGHT_CORNER; + default: + NOTREACHED(); + } + return GDK_LAST_CURSOR; +} + } // namespace std::map<XID, GtkWindow*> BrowserWindowGtk::xid_map_; @@ -294,11 +314,14 @@ std::map<XID, GtkWindow*> BrowserWindowGtk::xid_map_; BrowserWindowGtk::BrowserWindowGtk(Browser* browser) : browser_(browser), full_screen_(false), - drag_active_(false) { + drag_active_(false), + frame_cursor_(NULL) { use_custom_frame_.Init(prefs::kUseCustomChromeFrame, browser_->profile()->GetPrefs(), this); window_ = GTK_WINDOW(gtk_window_new(GTK_WINDOW_TOPLEVEL)); g_object_set_data(G_OBJECT(window_), kBrowserWindowKey, this); + gtk_widget_add_events(GTK_WIDGET(window_), GDK_BUTTON_PRESS_MASK | + GDK_POINTER_MOTION_MASK); SetWindowIcon(); SetBackgroundColor(); @@ -316,6 +339,11 @@ BrowserWindowGtk::BrowserWindowGtk(Browser* browser) BrowserWindowGtk::~BrowserWindowGtk() { browser_->tabstrip_model()->RemoveObserver(this); + + if (frame_cursor_) { + gdk_cursor_unref(frame_cursor_); + frame_cursor_ = NULL; + } } void BrowserWindowGtk::HandleAccelerator(guint keyval, @@ -775,6 +803,15 @@ void BrowserWindowGtk::AddFindBar(FindBarGtk* findbar) { gtk_box_reorder_child(GTK_BOX(render_area_vbox_), findbar->widget(), 0); } +void BrowserWindowGtk::ResetCustomFrameCursor() { + if (!frame_cursor_) + return; + + gdk_cursor_unref(frame_cursor_); + frame_cursor_ = NULL; + gdk_window_set_cursor(GTK_WIDGET(window_)->window, NULL); +} + // static BrowserWindowGtk* BrowserWindowGtk::GetBrowserWindowForNativeWindow( gfx::NativeWindow window) { @@ -834,8 +871,10 @@ void BrowserWindowGtk::ConnectHandlersToSignals() { G_CALLBACK(MainWindowUnMapped), this); g_signal_connect(window_, "key-press-event", G_CALLBACK(OnKeyPress), browser_.get()); + g_signal_connect(window_, "motion-notify-event", + G_CALLBACK(OnMouseMoveEvent), this); g_signal_connect(window_, "button-press-event", - G_CALLBACK(OnButtonPressEvent), browser_.get()); + G_CALLBACK(OnButtonPressEvent), this); g_signal_connect(window_, "focus-in-event", G_CALLBACK(OnFocusIn), browser_.get()); } @@ -978,7 +1017,7 @@ void BrowserWindowGtk::UpdateCustomFrame() { UpdateWindowShape(bounds_.width(), bounds_.height()); if (enable) { gtk_alignment_set_padding(GTK_ALIGNMENT(window_container_), 1, - kCustomFrameWidth, kCustomFrameWidth, kCustomFrameWidth); + kFrameBorderThickness, kFrameBorderThickness, kFrameBorderThickness); } else { gtk_alignment_set_padding(GTK_ALIGNMENT(window_container_), 0, 0, 0, 0); } @@ -1022,6 +1061,93 @@ gboolean BrowserWindowGtk::OnGtkAccelerator(GtkAccelGroup* accel_group, } // static +gboolean BrowserWindowGtk::OnMouseMoveEvent(GtkWidget* widget, + GdkEventMotion* event, BrowserWindowGtk* browser) { + // Update the cursor if we're on the custom frame border. + GdkWindowEdge edge; + bool has_hit_edge = browser->GetWindowEdge(static_cast<int>(event->x), + static_cast<int>(event->y), &edge); + GdkCursorType new_cursor = GDK_LAST_CURSOR; + if (has_hit_edge) + new_cursor = GdkWindowEdgeToGdkCursorType(edge); + + GdkCursorType last_cursor = GDK_LAST_CURSOR; + if (browser->frame_cursor_) + last_cursor = browser->frame_cursor_->type; + + if (last_cursor != new_cursor) { + if (browser->frame_cursor_) { + gdk_cursor_unref(browser->frame_cursor_); + browser->frame_cursor_ = NULL; + } + if (has_hit_edge) { + browser->frame_cursor_ = gdk_cursor_new(new_cursor); + gdk_window_set_cursor(GTK_WIDGET(browser->window_)->window, + browser->frame_cursor_); + } else { + gdk_window_set_cursor(GTK_WIDGET(browser->window_)->window, NULL); + } + } + return FALSE; +} + +// static +gboolean BrowserWindowGtk::OnButtonPressEvent(GtkWidget* widget, + GdkEventButton* event, BrowserWindowGtk* browser) { + // Handle back/forward. + // TODO(jhawkins): Investigate the possibility of the button numbers being + // different for other mice. + if (event->button == 8) { + browser->browser_->GoBack(CURRENT_TAB); + return TRUE; + } else if (event->button == 9) { + browser->browser_->GoForward(CURRENT_TAB); + return TRUE; + } + + // Handle left and right clicks. In particular, we care about clicks in the + // custom frame border and clicks in the titlebar. + GdkWindowEdge edge; + bool has_hit_edge = browser->GetWindowEdge(static_cast<int>(event->x), + static_cast<int>(event->y), &edge); + // Ignore clicks that are in/below the tab strip. + gint tabstrip_y; + gtk_widget_get_pointer(browser->toolbar_->widget(), NULL, &tabstrip_y); + bool has_hit_titlebar = !browser->IsFullscreen() && (tabstrip_y < 0) + && !has_hit_edge; + if (event->button == 1) { + if (GDK_BUTTON_PRESS == event->type) { + if (has_hit_titlebar) { + gtk_window_begin_move_drag(browser->window_, event->button, + event->x_root, event->y_root, event->time); + return TRUE; + } else if (has_hit_edge) { + gtk_window_begin_resize_drag(browser->window_, edge, event->button, + event->x_root, event->y_root, event->time); + return TRUE; + } + } else if (GDK_2BUTTON_PRESS == event->type) { + if (has_hit_titlebar) { + // Maximize/restore on double click. + if (browser->IsMaximized()) { + gtk_window_unmaximize(browser->window_); + } else { + gtk_window_maximize(browser->window_); + } + return TRUE; + } + } + } else if (event->button == 3) { + if (has_hit_titlebar) { + browser->titlebar_->ShowContextMenu(); + return TRUE; + } + } + + return FALSE; // Continue to propagate the event. +} + +// static void BrowserWindowGtk::MainWindowMapped(GtkWidget* widget, BrowserWindowGtk* window) { // Map the X Window ID of the window to our window. @@ -1077,3 +1203,58 @@ bool BrowserWindowGtk::IsToolbarSupported() { bool BrowserWindowGtk::IsBookmarkBarSupported() { return browser_->SupportsWindowFeature(Browser::FEATURE_BOOKMARKBAR); } + +bool BrowserWindowGtk::GetWindowEdge(int x, int y, GdkWindowEdge* edge) { + if (!use_custom_frame_.GetValue()) + return false; + + if (IsMaximized() || IsFullscreen()) + return false; + + if (x < kFrameBorderThickness) { + // Left edge. + if (y < kResizeAreaCornerSize - kTopResizeAdjust) { + *edge = GDK_WINDOW_EDGE_NORTH_WEST; + } else if (y < bounds_.height() - kResizeAreaCornerSize) { + *edge = GDK_WINDOW_EDGE_WEST; + } else { + *edge = GDK_WINDOW_EDGE_SOUTH_WEST; + } + return true; + } else if (x < bounds_.width() - kFrameBorderThickness) { + if (y < kFrameBorderThickness - kTopResizeAdjust) { + // Top edge. + if (x < kResizeAreaCornerSize) { + *edge = GDK_WINDOW_EDGE_NORTH_WEST; + } else if (x < x < bounds_.width() - kResizeAreaCornerSize) { + *edge = GDK_WINDOW_EDGE_NORTH; + } else { + *edge = GDK_WINDOW_EDGE_NORTH_EAST; + } + } else if (y < bounds_.height() - kFrameBorderThickness) { + // Ignore the middle content area. + return false; + } else { + // Bottom edge. + if (x < kResizeAreaCornerSize) { + *edge = GDK_WINDOW_EDGE_SOUTH_WEST; + } else if (x < bounds_.width() - kResizeAreaCornerSize) { + *edge = GDK_WINDOW_EDGE_SOUTH; + } else { + *edge = GDK_WINDOW_EDGE_SOUTH_EAST; + } + } + return true; + } else { + // Right edge. + if (y < kResizeAreaCornerSize - kTopResizeAdjust) { + *edge = GDK_WINDOW_EDGE_NORTH_EAST; + } else if (y < bounds_.height() - kResizeAreaCornerSize) { + *edge = GDK_WINDOW_EDGE_EAST; + } else { + *edge = GDK_WINDOW_EDGE_SOUTH_EAST; + } + return true; + } + NOTREACHED(); +} diff --git a/chrome/browser/gtk/browser_window_gtk.h b/chrome/browser/gtk/browser_window_gtk.h index 74b8dd6..b347288 100644 --- a/chrome/browser/gtk/browser_window_gtk.h +++ b/chrome/browser/gtk/browser_window_gtk.h @@ -127,6 +127,10 @@ class BrowserWindowGtk : public BrowserWindow, // close. void set_drag_active(bool drag_active) { drag_active_ = drag_active; } + // Reset the mouse cursor to the default cursor if it was set to something + // else for the custom frame. + void ResetCustomFrameCursor(); + // Returns the BrowserWindowGtk registered with |window|. static BrowserWindowGtk* GetBrowserWindowForNativeWindow( gfx::NativeWindow window); @@ -205,6 +209,14 @@ class BrowserWindowGtk : public BrowserWindow, GdkModifierType modifier, BrowserWindowGtk* browser_window); + // Mouse move and mouse button press callbacks. + static gboolean OnMouseMoveEvent(GtkWidget* widget, + GdkEventMotion* event, + BrowserWindowGtk* browser); + static gboolean OnButtonPressEvent(GtkWidget* widget, + GdkEventButton* event, + BrowserWindowGtk* browser); + // Maps and Unmaps the xid of |widget| to |window|. static void MainWindowMapped(GtkWidget* widget, BrowserWindowGtk* window); static void MainWindowUnMapped(GtkWidget* widget, BrowserWindowGtk* window); @@ -227,6 +239,11 @@ class BrowserWindowGtk : public BrowserWindow, bool IsToolbarSupported(); bool IsBookmarkBarSupported(); + // Checks to see if the mouse pointer at |x|, |y| is over the border of the + // custom frame (a spot that should trigger a window resize). Returns true if + // it should and sets |edge|. + bool GetWindowEdge(int x, int y, GdkWindowEdge* edge); + NotificationRegistrar registrar_; gfx::Rect bounds_; @@ -272,6 +289,10 @@ class BrowserWindowGtk : public BrowserWindow, // A map which translates an X Window ID into its respective GtkWindow. static std::map<XID, GtkWindow*> xid_map_; + // The current window cursor. We set it to a resize cursor when over the + // custom frame border. We set it to NULL if we want the default cursor. + GdkCursor* frame_cursor_; + DISALLOW_COPY_AND_ASSIGN(BrowserWindowGtk); }; diff --git a/chrome/browser/gtk/tabs/tab_renderer_gtk.cc b/chrome/browser/gtk/tabs/tab_renderer_gtk.cc index 2408d55..b4456de 100755 --- a/chrome/browser/gtk/tabs/tab_renderer_gtk.cc +++ b/chrome/browser/gtk/tabs/tab_renderer_gtk.cc @@ -515,10 +515,9 @@ void TabRendererGtk::PaintTab(GdkEventExpose* event) { return; // The tab is rendered into a windowless widget whose offset is at the - // coordinate [x(), y()]. Additionally, the parent widget is windowless, and - // it has an offset of event->area. Translate by these offsets so we can - // render at (0,0) to match windows rendering metrics. - canvas.TranslateInt(x(), y() + event->area.y); + // coordinate event->area. Translate by these offsets so we can render at + // (0,0) to match Windows' rendering metrics. + canvas.TranslateInt(event->area.x, event->area.y); Paint(&canvas); } |