mirror of
https://gitlab.gnome.org/GNOME/gtk.git
synced 2024-11-07 01:00:28 +00:00
7e778aa033
In the Quartz backend, there are two methods by which windows are resized. The first method is fully handled by Quartz and does not appear in the event stream the application resizes. The second method is when we resize windows by ourselves. In OS X this happens when a GTK+ resize grip is used. This resize grip is larger than the Quartz resize grip. When the resize is started outside the "Quartz area", we have to handle it by ourselves. This patch fixes this manual window resizing by ignoring events while we are in the process of resizing (such that the events actually arrive at the sendEvent handler of GdkQuartzWindow where this resize is handled). When the resize has finished we break all grabs such that GDK is not stuck thinking the cursor is still in the resize window.
612 lines
16 KiB
C
612 lines
16 KiB
C
/* GdkQuartzWindow.m
|
|
*
|
|
* Copyright (C) 2005-2007 Imendio AB
|
|
*
|
|
* This library is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Lesser General Public
|
|
* License as published by the Free Software Foundation; either
|
|
* version 2 of the License, or (at your option) any later version.
|
|
*
|
|
* This library is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public
|
|
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#import "GdkQuartzNSWindow.h"
|
|
#include "gdkquartzwindow.h"
|
|
#include "gdkdnd-quartz.h"
|
|
#include "gdkprivate-quartz.h"
|
|
|
|
@implementation GdkQuartzNSWindow
|
|
|
|
-(BOOL)windowShouldClose:(id)sender
|
|
{
|
|
GdkWindow *window = [[self contentView] gdkWindow];
|
|
GdkEvent *event;
|
|
|
|
event = gdk_event_new (GDK_DELETE);
|
|
|
|
event->any.window = g_object_ref (window);
|
|
event->any.send_event = FALSE;
|
|
|
|
_gdk_event_queue_append (gdk_display_get_default (), event);
|
|
|
|
return NO;
|
|
}
|
|
|
|
-(void)windowWillMiniaturize:(NSNotification *)aNotification
|
|
{
|
|
GdkWindow *window = [[self contentView] gdkWindow];
|
|
|
|
_gdk_quartz_window_detach_from_parent (window);
|
|
}
|
|
|
|
-(void)windowDidMiniaturize:(NSNotification *)aNotification
|
|
{
|
|
GdkWindow *window = [[self contentView] gdkWindow];
|
|
|
|
gdk_synthesize_window_state (window, 0,
|
|
GDK_WINDOW_STATE_ICONIFIED);
|
|
}
|
|
|
|
-(void)windowDidDeminiaturize:(NSNotification *)aNotification
|
|
{
|
|
GdkWindow *window = [[self contentView] gdkWindow];
|
|
|
|
_gdk_quartz_window_attach_to_parent (window);
|
|
|
|
gdk_synthesize_window_state (window, GDK_WINDOW_STATE_ICONIFIED, 0);
|
|
}
|
|
|
|
-(void)windowDidBecomeKey:(NSNotification *)aNotification
|
|
{
|
|
GdkWindow *window = [[self contentView] gdkWindow];
|
|
|
|
_gdk_quartz_events_update_focus_window (window, TRUE);
|
|
}
|
|
|
|
-(void)windowDidResignKey:(NSNotification *)aNotification
|
|
{
|
|
GdkWindow *window = [[self contentView] gdkWindow];
|
|
|
|
_gdk_quartz_events_update_focus_window (window, FALSE);
|
|
}
|
|
|
|
-(void)windowDidBecomeMain:(NSNotification *)aNotification
|
|
{
|
|
GdkWindow *window = [[self contentView] gdkWindow];
|
|
|
|
if (![self isVisible])
|
|
{
|
|
/* Note: This is a hack needed because for unknown reasons, hidden
|
|
* windows get shown when clicking the dock icon when the application
|
|
* is not already active.
|
|
*/
|
|
[self orderOut:nil];
|
|
return;
|
|
}
|
|
|
|
_gdk_quartz_window_did_become_main (window);
|
|
}
|
|
|
|
-(void)windowDidResignMain:(NSNotification *)aNotification
|
|
{
|
|
GdkWindow *window;
|
|
|
|
window = [[self contentView] gdkWindow];
|
|
_gdk_quartz_window_did_resign_main (window);
|
|
}
|
|
|
|
/* Used in combination with NSLeftMouseUp in sendEvent to keep track
|
|
* of when the window is being moved with the mouse.
|
|
*/
|
|
-(void)windowWillMove:(NSNotification *)aNotification
|
|
{
|
|
inMove = YES;
|
|
}
|
|
|
|
-(void)sendEvent:(NSEvent *)event
|
|
{
|
|
switch ([event type])
|
|
{
|
|
case NSLeftMouseUp:
|
|
{
|
|
double time = ((double)[event timestamp]) * 1000.0;
|
|
|
|
_gdk_quartz_events_break_all_grabs (time);
|
|
inManualMove = NO;
|
|
inManualResize = NO;
|
|
inMove = NO;
|
|
break;
|
|
}
|
|
|
|
case NSLeftMouseDragged:
|
|
if ([self trackManualMove] || [self trackManualResize])
|
|
return;
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
[super sendEvent:event];
|
|
}
|
|
|
|
-(BOOL)isInMove
|
|
{
|
|
return inMove;
|
|
}
|
|
|
|
-(void)windowDidMove:(NSNotification *)aNotification
|
|
{
|
|
GdkWindow *window = [[self contentView] gdkWindow];
|
|
GdkEvent *event;
|
|
|
|
_gdk_quartz_window_update_position (window);
|
|
|
|
/* Synthesize a configure event */
|
|
event = gdk_event_new (GDK_CONFIGURE);
|
|
event->configure.window = g_object_ref (window);
|
|
event->configure.x = window->x;
|
|
event->configure.y = window->y;
|
|
event->configure.width = window->width;
|
|
event->configure.height = window->height;
|
|
|
|
_gdk_event_queue_append (gdk_display_get_default (), event);
|
|
}
|
|
|
|
-(void)windowDidResize:(NSNotification *)aNotification
|
|
{
|
|
NSRect content_rect = [self contentRectForFrameRect:[self frame]];
|
|
GdkWindow *window = [[self contentView] gdkWindow];
|
|
GdkEvent *event;
|
|
|
|
window->width = content_rect.size.width;
|
|
window->height = content_rect.size.height;
|
|
|
|
/* Certain resize operations (e.g. going fullscreen), also move the
|
|
* origin of the window.
|
|
*/
|
|
_gdk_quartz_window_update_position (window);
|
|
|
|
[[self contentView] setFrame:NSMakeRect (0, 0, window->width, window->height)];
|
|
|
|
_gdk_window_update_size (window);
|
|
|
|
/* Synthesize a configure event */
|
|
event = gdk_event_new (GDK_CONFIGURE);
|
|
event->configure.window = g_object_ref (window);
|
|
event->configure.x = window->x;
|
|
event->configure.y = window->y;
|
|
event->configure.width = window->width;
|
|
event->configure.height = window->height;
|
|
|
|
_gdk_event_queue_append (gdk_display_get_default (), event);
|
|
}
|
|
|
|
-(id)initWithContentRect:(NSRect)contentRect styleMask:(NSUInteger)styleMask backing:(NSBackingStoreType)backingType defer:(BOOL)flag screen:(NSScreen *)screen
|
|
{
|
|
self = [super initWithContentRect:contentRect
|
|
styleMask:styleMask
|
|
backing:backingType
|
|
defer:flag
|
|
screen:screen];
|
|
|
|
[self setAcceptsMouseMovedEvents:YES];
|
|
[self setDelegate:self];
|
|
[self setReleasedWhenClosed:YES];
|
|
|
|
return self;
|
|
}
|
|
|
|
-(BOOL)canBecomeMainWindow
|
|
{
|
|
GdkWindow *window = [[self contentView] gdkWindow];
|
|
GdkWindowImplQuartz *impl = GDK_WINDOW_IMPL_QUARTZ (window->impl);
|
|
|
|
switch (impl->type_hint)
|
|
{
|
|
case GDK_WINDOW_TYPE_HINT_NORMAL:
|
|
case GDK_WINDOW_TYPE_HINT_DIALOG:
|
|
return YES;
|
|
|
|
case GDK_WINDOW_TYPE_HINT_MENU:
|
|
case GDK_WINDOW_TYPE_HINT_TOOLBAR:
|
|
case GDK_WINDOW_TYPE_HINT_SPLASHSCREEN:
|
|
case GDK_WINDOW_TYPE_HINT_UTILITY:
|
|
case GDK_WINDOW_TYPE_HINT_DOCK:
|
|
case GDK_WINDOW_TYPE_HINT_DESKTOP:
|
|
case GDK_WINDOW_TYPE_HINT_DROPDOWN_MENU:
|
|
case GDK_WINDOW_TYPE_HINT_POPUP_MENU:
|
|
case GDK_WINDOW_TYPE_HINT_TOOLTIP:
|
|
case GDK_WINDOW_TYPE_HINT_NOTIFICATION:
|
|
case GDK_WINDOW_TYPE_HINT_COMBO:
|
|
case GDK_WINDOW_TYPE_HINT_DND:
|
|
return NO;
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
-(BOOL)canBecomeKeyWindow
|
|
{
|
|
GdkWindow *window = [[self contentView] gdkWindow];
|
|
GdkWindowImplQuartz *impl = GDK_WINDOW_IMPL_QUARTZ (window->impl);
|
|
|
|
if (!window->accept_focus)
|
|
return NO;
|
|
|
|
/* Popup windows should not be able to get focused in the window
|
|
* manager sense, it's only handled through grabs.
|
|
*/
|
|
if (window->window_type == GDK_WINDOW_TEMP)
|
|
return NO;
|
|
|
|
switch (impl->type_hint)
|
|
{
|
|
case GDK_WINDOW_TYPE_HINT_NORMAL:
|
|
case GDK_WINDOW_TYPE_HINT_DIALOG:
|
|
case GDK_WINDOW_TYPE_HINT_MENU:
|
|
case GDK_WINDOW_TYPE_HINT_TOOLBAR:
|
|
case GDK_WINDOW_TYPE_HINT_UTILITY:
|
|
case GDK_WINDOW_TYPE_HINT_DOCK:
|
|
case GDK_WINDOW_TYPE_HINT_DESKTOP:
|
|
case GDK_WINDOW_TYPE_HINT_DROPDOWN_MENU:
|
|
case GDK_WINDOW_TYPE_HINT_POPUP_MENU:
|
|
case GDK_WINDOW_TYPE_HINT_COMBO:
|
|
return YES;
|
|
|
|
case GDK_WINDOW_TYPE_HINT_SPLASHSCREEN:
|
|
case GDK_WINDOW_TYPE_HINT_TOOLTIP:
|
|
case GDK_WINDOW_TYPE_HINT_NOTIFICATION:
|
|
case GDK_WINDOW_TYPE_HINT_DND:
|
|
return NO;
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
- (void)showAndMakeKey:(BOOL)makeKey
|
|
{
|
|
GdkWindow *window = [[self contentView] gdkWindow];
|
|
GdkWindowImplQuartz *impl = GDK_WINDOW_IMPL_QUARTZ (window->impl);
|
|
|
|
inShowOrHide = YES;
|
|
|
|
if (makeKey)
|
|
[impl->toplevel makeKeyAndOrderFront:impl->toplevel];
|
|
else
|
|
[impl->toplevel orderFront:nil];
|
|
|
|
inShowOrHide = NO;
|
|
}
|
|
|
|
- (void)hide
|
|
{
|
|
GdkWindow *window = [[self contentView] gdkWindow];
|
|
GdkWindowImplQuartz *impl = GDK_WINDOW_IMPL_QUARTZ (window->impl);
|
|
|
|
inShowOrHide = YES;
|
|
[impl->toplevel orderOut:nil];
|
|
inShowOrHide = NO;
|
|
}
|
|
|
|
- (BOOL)trackManualMove
|
|
{
|
|
NSPoint currentLocation;
|
|
NSPoint newOrigin;
|
|
NSRect screenFrame = [[NSScreen mainScreen] visibleFrame];
|
|
NSRect windowFrame = [self frame];
|
|
|
|
if (!inManualMove)
|
|
return NO;
|
|
|
|
currentLocation = [self convertBaseToScreen:[self mouseLocationOutsideOfEventStream]];
|
|
newOrigin.x = currentLocation.x - initialMoveLocation.x;
|
|
newOrigin.y = currentLocation.y - initialMoveLocation.y;
|
|
|
|
/* Clamp vertical position to below the menu bar. */
|
|
if (newOrigin.y + windowFrame.size.height > screenFrame.origin.y + screenFrame.size.height)
|
|
newOrigin.y = screenFrame.origin.y + screenFrame.size.height - windowFrame.size.height;
|
|
|
|
[self setFrameOrigin:newOrigin];
|
|
|
|
return YES;
|
|
}
|
|
|
|
-(BOOL)isInManualResize
|
|
{
|
|
return inManualResize;
|
|
}
|
|
|
|
-(void)beginManualMove
|
|
{
|
|
NSRect frame = [self frame];
|
|
|
|
if (inMove || inManualMove || inManualResize)
|
|
return;
|
|
|
|
inManualMove = YES;
|
|
|
|
initialMoveLocation = [self convertBaseToScreen:[self mouseLocationOutsideOfEventStream]];
|
|
initialMoveLocation.x -= frame.origin.x;
|
|
initialMoveLocation.y -= frame.origin.y;
|
|
}
|
|
|
|
- (BOOL)trackManualResize
|
|
{
|
|
NSPoint currentLocation;
|
|
NSRect newFrame;
|
|
float dx, dy;
|
|
NSSize min_size;
|
|
|
|
if (!inManualResize || inTrackManualResize)
|
|
return NO;
|
|
|
|
inTrackManualResize = YES;
|
|
|
|
currentLocation = [self convertBaseToScreen:[self mouseLocationOutsideOfEventStream]];
|
|
currentLocation.x -= initialResizeFrame.origin.x;
|
|
currentLocation.y -= initialResizeFrame.origin.y;
|
|
|
|
dx = currentLocation.x - initialResizeLocation.x;
|
|
dy = -(currentLocation.y - initialResizeLocation.y);
|
|
|
|
newFrame = initialResizeFrame;
|
|
newFrame.size.width = initialResizeFrame.size.width + dx;
|
|
newFrame.size.height = initialResizeFrame.size.height + dy;
|
|
|
|
min_size = [self contentMinSize];
|
|
if (newFrame.size.width < min_size.width)
|
|
newFrame.size.width = min_size.width;
|
|
if (newFrame.size.height < min_size.height)
|
|
newFrame.size.height = min_size.height;
|
|
|
|
/* We could also apply aspect ratio:
|
|
newFrame.size.height = newFrame.size.width / [self aspectRatio].width * [self aspectRatio].height;
|
|
*/
|
|
|
|
dy = newFrame.size.height - initialResizeFrame.size.height;
|
|
|
|
newFrame.origin.x = initialResizeFrame.origin.x;
|
|
newFrame.origin.y = initialResizeFrame.origin.y - dy;
|
|
|
|
[self setFrame:newFrame display:YES];
|
|
|
|
/* Let the resizing be handled by GTK+. */
|
|
if (g_main_context_pending (NULL))
|
|
g_main_context_iteration (NULL, FALSE);
|
|
|
|
inTrackManualResize = NO;
|
|
|
|
return YES;
|
|
}
|
|
|
|
-(void)beginManualResize
|
|
{
|
|
if (inMove || inManualMove || inManualResize)
|
|
return;
|
|
|
|
inManualResize = YES;
|
|
|
|
initialResizeFrame = [self frame];
|
|
initialResizeLocation = [self convertBaseToScreen:[self mouseLocationOutsideOfEventStream]];
|
|
initialResizeLocation.x -= initialResizeFrame.origin.x;
|
|
initialResizeLocation.y -= initialResizeFrame.origin.y;
|
|
}
|
|
|
|
|
|
|
|
static GdkDragContext *current_context = NULL;
|
|
|
|
static GdkDragAction
|
|
drag_operation_to_drag_action (NSDragOperation operation)
|
|
{
|
|
GdkDragAction result = 0;
|
|
|
|
/* GDK and Quartz drag operations do not map 1:1.
|
|
* This mapping represents about the best that we
|
|
* can come up.
|
|
*
|
|
* Note that NSDragOperationPrivate and GDK_ACTION_PRIVATE
|
|
* have almost opposite meanings: the GDK one means that the
|
|
* destination is solely responsible for the action; the Quartz
|
|
* one means that the source and destination will agree
|
|
* privately on the action. NSOperationGeneric is close in meaning
|
|
* to GDK_ACTION_PRIVATE but there is a problem: it will be
|
|
* sent for any ordinary drag, and likely not understood
|
|
* by any intra-widget drag (since the source & dest are the
|
|
* same).
|
|
*/
|
|
|
|
if (operation & NSDragOperationGeneric)
|
|
result |= GDK_ACTION_MOVE;
|
|
if (operation & NSDragOperationCopy)
|
|
result |= GDK_ACTION_COPY;
|
|
if (operation & NSDragOperationMove)
|
|
result |= GDK_ACTION_MOVE;
|
|
if (operation & NSDragOperationLink)
|
|
result |= GDK_ACTION_LINK;
|
|
|
|
return result;
|
|
}
|
|
|
|
static NSDragOperation
|
|
drag_action_to_drag_operation (GdkDragAction action)
|
|
{
|
|
NSDragOperation result = 0;
|
|
|
|
if (action & GDK_ACTION_COPY)
|
|
result |= NSDragOperationCopy;
|
|
if (action & GDK_ACTION_LINK)
|
|
result |= NSDragOperationLink;
|
|
if (action & GDK_ACTION_MOVE)
|
|
result |= NSDragOperationMove;
|
|
|
|
return result;
|
|
}
|
|
|
|
static void
|
|
update_context_from_dragging_info (id <NSDraggingInfo> sender)
|
|
{
|
|
g_assert (current_context != NULL);
|
|
|
|
GDK_QUARTZ_DRAG_CONTEXT (current_context)->dragging_info = sender;
|
|
current_context->suggested_action = drag_operation_to_drag_action ([sender draggingSourceOperationMask]);
|
|
current_context->actions = current_context->suggested_action;
|
|
}
|
|
|
|
- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender
|
|
{
|
|
GdkDeviceManager *device_manager;
|
|
GdkEvent *event;
|
|
GdkWindow *window;
|
|
|
|
if (current_context)
|
|
g_object_unref (current_context);
|
|
|
|
current_context = g_object_new (GDK_TYPE_QUARTZ_DRAG_CONTEXT, NULL);
|
|
update_context_from_dragging_info (sender);
|
|
|
|
window = [[self contentView] gdkWindow];
|
|
|
|
device_manager = gdk_display_get_device_manager (gdk_display_get_default ());
|
|
gdk_drag_context_set_device (current_context,
|
|
gdk_device_manager_get_client_pointer (device_manager));
|
|
|
|
event = gdk_event_new (GDK_DRAG_ENTER);
|
|
event->dnd.window = g_object_ref (window);
|
|
event->dnd.send_event = FALSE;
|
|
event->dnd.context = g_object_ref (current_context);
|
|
event->dnd.time = GDK_CURRENT_TIME;
|
|
|
|
gdk_event_set_device (event, gdk_drag_context_get_device (current_context));
|
|
|
|
_gdk_event_emit (event);
|
|
|
|
gdk_event_free (event);
|
|
|
|
return NSDragOperationNone;
|
|
}
|
|
|
|
- (void)draggingEnded:(id <NSDraggingInfo>)sender
|
|
{
|
|
/* leave a note for the source about what action was taken */
|
|
if (_gdk_quartz_drag_source_context && current_context)
|
|
_gdk_quartz_drag_source_context->action = current_context->action;
|
|
|
|
if (current_context)
|
|
g_object_unref (current_context);
|
|
current_context = NULL;
|
|
}
|
|
|
|
- (void)draggingExited:(id <NSDraggingInfo>)sender
|
|
{
|
|
GdkEvent *event;
|
|
|
|
event = gdk_event_new (GDK_DRAG_LEAVE);
|
|
event->dnd.window = g_object_ref ([[self contentView] gdkWindow]);
|
|
event->dnd.send_event = FALSE;
|
|
event->dnd.context = g_object_ref (current_context);
|
|
event->dnd.time = GDK_CURRENT_TIME;
|
|
|
|
gdk_event_set_device (event, gdk_drag_context_get_device (current_context));
|
|
|
|
_gdk_event_emit (event);
|
|
|
|
gdk_event_free (event);
|
|
|
|
g_object_unref (current_context);
|
|
current_context = NULL;
|
|
}
|
|
|
|
- (NSDragOperation)draggingUpdated:(id <NSDraggingInfo>)sender
|
|
{
|
|
NSPoint point = [sender draggingLocation];
|
|
NSPoint screen_point = [self convertBaseToScreen:point];
|
|
GdkEvent *event;
|
|
int gx, gy;
|
|
|
|
update_context_from_dragging_info (sender);
|
|
_gdk_quartz_window_nspoint_to_gdk_xy (screen_point, &gx, &gy);
|
|
|
|
event = gdk_event_new (GDK_DRAG_MOTION);
|
|
event->dnd.window = g_object_ref ([[self contentView] gdkWindow]);
|
|
event->dnd.send_event = FALSE;
|
|
event->dnd.context = g_object_ref (current_context);
|
|
event->dnd.time = GDK_CURRENT_TIME;
|
|
event->dnd.x_root = gx;
|
|
event->dnd.y_root = gy;
|
|
|
|
gdk_event_set_device (event, gdk_drag_context_get_device (current_context));
|
|
|
|
_gdk_event_emit (event);
|
|
|
|
gdk_event_free (event);
|
|
|
|
return drag_action_to_drag_operation (current_context->action);
|
|
}
|
|
|
|
- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender
|
|
{
|
|
NSPoint point = [sender draggingLocation];
|
|
NSPoint screen_point = [self convertBaseToScreen:point];
|
|
GdkEvent *event;
|
|
int gy, gx;
|
|
|
|
update_context_from_dragging_info (sender);
|
|
_gdk_quartz_window_nspoint_to_gdk_xy (screen_point, &gx, &gy);
|
|
|
|
event = gdk_event_new (GDK_DROP_START);
|
|
event->dnd.window = g_object_ref ([[self contentView] gdkWindow]);
|
|
event->dnd.send_event = FALSE;
|
|
event->dnd.context = g_object_ref (current_context);
|
|
event->dnd.time = GDK_CURRENT_TIME;
|
|
event->dnd.x_root = gx;
|
|
event->dnd.y_root = gy;
|
|
|
|
gdk_event_set_device (event, gdk_drag_context_get_device (current_context));
|
|
|
|
_gdk_event_emit (event);
|
|
|
|
gdk_event_free (event);
|
|
|
|
g_object_unref (current_context);
|
|
current_context = NULL;
|
|
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)wantsPeriodicDraggingUpdates
|
|
{
|
|
return NO;
|
|
}
|
|
|
|
- (void)draggedImage:(NSImage *)anImage endedAt:(NSPoint)aPoint operation:(NSDragOperation)operation
|
|
{
|
|
GdkEvent *event;
|
|
|
|
g_assert (_gdk_quartz_drag_source_context != NULL);
|
|
|
|
event = gdk_event_new (GDK_DROP_FINISHED);
|
|
event->dnd.window = g_object_ref ([[self contentView] gdkWindow]);
|
|
event->dnd.send_event = FALSE;
|
|
event->dnd.context = g_object_ref (_gdk_quartz_drag_source_context);
|
|
|
|
gdk_event_set_device (event,
|
|
gdk_drag_context_get_device (_gdk_quartz_drag_source_context));
|
|
|
|
_gdk_event_emit (event);
|
|
|
|
gdk_event_free (event);
|
|
|
|
g_object_unref (_gdk_quartz_drag_source_context);
|
|
_gdk_quartz_drag_source_context = NULL;
|
|
}
|
|
|
|
@end
|