2014-08-04 22:31:11 -05:00
// +build !windows,!darwin,!plan9
// 14 march 2014
package ui
import (
"fmt"
"image"
"unsafe"
)
// #include "gtk_unix.h"
2014-08-22 10:29:44 -05:00
// extern gboolean our_area_get_child_position_callback(GtkOverlay *, GtkWidget *, GdkRectangle *, gpointer);
// extern gboolean our_area_textfield_focus_out_event_callback(GtkWidget *, GdkEvent *, gpointer);
2014-08-04 22:31:11 -05:00
// extern gboolean our_area_draw_callback(GtkWidget *, cairo_t *, gpointer);
// extern gboolean our_area_button_press_event_callback(GtkWidget *, GdkEvent *, gpointer);
// extern gboolean our_area_button_release_event_callback(GtkWidget *, GdkEvent *, gpointer);
// extern gboolean our_area_motion_notify_event_callback(GtkWidget *, GdkEvent *, gpointer);
// extern gboolean our_area_enterleave_notify_event_callback(GtkWidget *, GdkEvent *, gpointer);
// extern gboolean our_area_key_press_event_callback(GtkWidget *, GdkEvent *, gpointer);
// extern gboolean our_area_key_release_event_callback(GtkWidget *, GdkEvent *, gpointer);
// /* because cgo doesn't like ... */
// static inline void gtkGetDoubleClickSettings(GtkSettings *settings, gint *maxTime, gint *maxDistance)
// {
// g_object_get(settings,
// "gtk-double-click-time", maxTime,
// "gtk-double-click-distance", maxDistance,
// NULL);
// }
import "C"
type area struct {
* areabase
_widget * C . GtkWidget
drawingarea * C . GtkDrawingArea
scroller * scroller
clickCounter * clickCounter
2014-08-22 10:29:44 -05:00
textfieldw * C . GtkWidget
textfield * C . GtkEntry
textfieldx int
textfieldy int
textfielddone * event
2014-08-04 22:31:11 -05:00
}
func newArea ( ab * areabase ) Area {
widget := C . gtk_drawing_area_new ( )
// the Area's size will be set later
// we need to explicitly subscribe to mouse events with GtkDrawingArea
C . gtk_widget_add_events ( widget ,
C . GDK_BUTTON_PRESS_MASK | C . GDK_BUTTON_RELEASE_MASK | C . GDK_POINTER_MOTION_MASK | C . GDK_BUTTON_MOTION_MASK | C . GDK_ENTER_NOTIFY_MASK | C . GDK_LEAVE_NOTIFY_MASK )
// and we need to allow focusing on a GtkDrawingArea to enable keyboard events
C . gtk_widget_set_can_focus ( widget , C . TRUE )
2014-08-22 10:29:44 -05:00
textfieldw := C . gtk_entry_new ( )
2014-08-04 22:31:11 -05:00
a := & area {
areabase : ab ,
_widget : widget ,
drawingarea : ( * C . GtkDrawingArea ) ( unsafe . Pointer ( widget ) ) ,
2014-08-22 09:52:51 -05:00
scroller : newScroller ( widget , false , false , true ) , // not natively scrollable; no border; have an overlay for OpenTextFieldAt()
2014-08-04 22:31:11 -05:00
clickCounter : new ( clickCounter ) ,
2014-08-22 10:29:44 -05:00
textfieldw : textfieldw ,
textfield : ( * C . GtkEntry ) ( unsafe . Pointer ( textfieldw ) ) ,
textfielddone : newEvent ( ) ,
2014-08-04 22:31:11 -05:00
}
2014-08-04 22:48:04 -05:00
for _ , c := range areaCallbacks {
g_signal_connect (
C . gpointer ( unsafe . Pointer ( a . drawingarea ) ) ,
c . name ,
c . callback ,
C . gpointer ( unsafe . Pointer ( a ) ) )
}
2014-08-04 22:31:11 -05:00
a . SetSize ( a . width , a . height )
2014-08-22 10:29:44 -05:00
C . gtk_overlay_add_overlay ( a . scroller . overlay , a . textfieldw )
g_signal_connect (
C . gpointer ( unsafe . Pointer ( a . scroller . overlay ) ) ,
"get-child-position" ,
area_get_child_position_callback ,
C . gpointer ( unsafe . Pointer ( a ) ) )
g_signal_connect (
C . gpointer ( unsafe . Pointer ( a . textfield ) ) ,
"focus-out-event" ,
area_textfield_focus_out_event_callback ,
C . gpointer ( unsafe . Pointer ( a ) ) )
// the widget shows up initially
2014-08-22 10:39:45 -05:00
C . gtk_widget_set_no_show_all ( a . textfieldw , C . TRUE )
2014-08-04 22:31:11 -05:00
return a
}
func ( a * area ) SetSize ( width , height int ) {
a . width = width
a . height = height
C . gtk_widget_set_size_request ( a . _widget , C . gint ( a . width ) , C . gint ( a . height ) )
}
2014-08-21 10:43:37 -05:00
func ( a * area ) Repaint ( r image . Rectangle ) {
r = image . Rect ( 0 , 0 , a . width , a . height ) . Intersect ( r )
if r . Empty ( ) {
return
}
2014-08-21 17:23:51 -05:00
println ( a . _widget , C . gint ( r . Min . X ) , C . gint ( r . Min . Y ) , C . gint ( r . Dx ( ) ) , C . gint ( r . Dy ( ) ) )
C . gtk_widget_queue_draw_area ( a . _widget , C . gint ( r . Min . X ) , C . gint ( r . Min . Y ) , C . gint ( r . Dx ( ) ) , C . gint ( r . Dy ( ) ) )
2014-08-21 10:43:37 -05:00
}
2014-08-04 22:31:11 -05:00
func ( a * area ) RepaintAll ( ) {
C . gtk_widget_queue_draw ( a . _widget )
}
2014-08-22 10:29:44 -05:00
func ( a * area ) OpenTextFieldAt ( x , y int ) {
if x < 0 || x >= a . width || y < 0 || y >= a . height {
panic ( fmt . Errorf ( "point (%d,%d) outside Area in Area.OpenTextFieldAt()" , x , y ) )
}
a . textfieldx = x
a . textfieldy = y
a . SetTextFieldText ( "" )
2014-08-22 10:39:45 -05:00
// we disabled this for the initial Area show; we don't need to anymore
C . gtk_widget_set_no_show_all ( a . textfieldw , C . FALSE )
2014-08-22 10:29:44 -05:00
C . gtk_widget_show_all ( a . textfieldw )
C . gtk_widget_grab_focus ( a . textfieldw )
}
func ( a * area ) TextFieldText ( ) string {
return fromgstr ( C . gtk_entry_get_text ( a . textfield ) )
}
func ( a * area ) SetTextFieldText ( text string ) {
ctext := togstr ( text )
defer freegstr ( ctext )
C . gtk_entry_set_text ( a . textfield , ctext )
}
func ( a * area ) OnTextFieldDismissed ( f func ( ) ) {
a . textfielddone . set ( f )
}
//export our_area_get_child_position_callback
func our_area_get_child_position_callback ( overlay * C . GtkOverlay , widget * C . GtkWidget , rect * C . GdkRectangle , data C . gpointer ) C . gboolean {
var nat C . GtkRequisition
a := ( * area ) ( unsafe . Pointer ( data ) )
rect . x = C . int ( a . textfieldx )
rect . y = C . int ( a . textfieldy )
C . gtk_widget_get_preferred_size ( a . textfieldw , nil , & nat )
rect . width = C . int ( nat . width )
rect . height = C . int ( nat . height )
return C . TRUE
}
var area_get_child_position_callback = C . GCallback ( C . our_area_get_child_position_callback )
//export our_area_textfield_focus_out_event_callback
func our_area_textfield_focus_out_event_callback ( widget * C . GtkWidget , event * C . GdkEvent , data C . gpointer ) C . gboolean {
a := ( * area ) ( unsafe . Pointer ( data ) )
C . gtk_widget_hide ( a . textfieldw )
2014-08-22 10:39:45 -05:00
a . textfielddone . fire ( )
2014-08-22 10:29:44 -05:00
return continueEventChain
}
var area_textfield_focus_out_event_callback = C . GCallback ( C . our_area_textfield_focus_out_event_callback )
2014-08-04 22:48:04 -05:00
var areaCallbacks = [ ] struct {
name string
callback C . GCallback
} {
{ "draw" , area_draw_callback } ,
{ "button-press-event" , area_button_press_event_callback } ,
{ "button-release-event" , area_button_release_event_callback } ,
{ "motion-notify-event" , area_motion_notify_event_callback } ,
{ "enter-notify-event" , area_enterleave_notify_event_callback } ,
{ "leave-notify-event" , area_enterleave_notify_event_callback } ,
{ "key-press-event" , area_key_press_event_callback } ,
{ "key-release-event" , area_key_release_event_callback } ,
}
2014-08-04 22:31:11 -05:00
//export our_area_draw_callback
func our_area_draw_callback ( widget * C . GtkWidget , cr * C . cairo_t , data C . gpointer ) C . gboolean {
var x0 , y0 , x1 , y1 C . double
a := ( * area ) ( unsafe . Pointer ( data ) )
// thanks to desrt in irc.gimp.net/#gtk+
// these are in user coordinates, which match what coordinates we want by default, even out of a draw event handler (thanks johncc3, mclasen, and Company in irc.gimp.net/#gtk+)
C . cairo_clip_extents ( cr , & x0 , & y0 , & x1 , & y1 )
// we do not need to clear the cliprect; GtkDrawingArea did it for us beforehand
cliprect := image . Rect ( int ( x0 ) , int ( y0 ) , int ( x1 ) , int ( y1 ) )
// the cliprect can actually fall outside the size of the Area; clip it by intersecting the two rectangles
2014-08-21 18:09:40 -05:00
cliprect = image . Rect ( 0 , 0 , a . width , a . height ) . Intersect ( cliprect )
2014-08-04 22:31:11 -05:00
if cliprect . Empty ( ) { // no intersection; nothing to paint
return C . FALSE // signals handled without stopping the event chain (thanks to desrt again)
}
i := a . handler . Paint ( cliprect )
surface := C . cairo_image_surface_create (
C . CAIRO_FORMAT_ARGB32 , // alpha-premultiplied; native byte order
C . int ( i . Rect . Dx ( ) ) ,
C . int ( i . Rect . Dy ( ) ) )
if status := C . cairo_surface_status ( surface ) ; status != C . CAIRO_STATUS_SUCCESS {
panic ( fmt . Errorf ( "cairo_create_image_surface() failed: %s\n" ,
C . GoString ( C . cairo_status_to_string ( status ) ) ) )
}
// the flush and mark_dirty calls are required; see the cairo docs and https://git.gnome.org/browse/gtk+/tree/gdk/gdkcairo.c#n232 (thanks desrt in irc.gimp.net/#gtk+)
C . cairo_surface_flush ( surface )
toARGB ( i , uintptr ( unsafe . Pointer ( C . cairo_image_surface_get_data ( surface ) ) ) ,
int ( C . cairo_image_surface_get_stride ( surface ) ) )
C . cairo_surface_mark_dirty ( surface )
C . cairo_set_source_surface ( cr ,
surface ,
2014-08-21 19:45:46 -05:00
x0 , y0 ) // point on cairo_t where we want to draw (thanks Company in irc.gimp.net/#gtk+)
2014-08-04 22:31:11 -05:00
// that just set the brush that cairo uses: we have to actually draw now
// (via https://developer.gnome.org/gtkmm-tutorial/stable/sec-draw-images.html.en)
2014-08-21 18:09:40 -05:00
C . cairo_rectangle ( cr , x0 , y0 , x1 , y1 ) // breaking the norm here since we have the coordinates as a C double already
2014-08-04 22:31:11 -05:00
C . cairo_fill ( cr )
C . cairo_surface_destroy ( surface ) // free surface
return C . FALSE // signals handled without stopping the event chain (thanks to desrt again)
}
var area_draw_callback = C . GCallback ( C . our_area_draw_callback )
func translateModifiers ( state C . guint , window * C . GdkWindow ) C . guint {
// GDK doesn't initialize the modifier flags fully; we have to explicitly tell it to (thanks to Daniel_S and daniels (two different people) in irc.gimp.net/#gtk+)
C . gdk_keymap_add_virtual_modifiers (
C . gdk_keymap_get_for_display ( C . gdk_window_get_display ( window ) ) ,
( * C . GdkModifierType ) ( unsafe . Pointer ( & state ) ) )
return state
}
func makeModifiers ( state C . guint ) ( m Modifiers ) {
if ( state & C . GDK_CONTROL_MASK ) != 0 {
m |= Ctrl
}
2014-08-14 17:55:54 -05:00
if ( state & C . GDK_META_MASK ) != 0 {
m |= Alt
}
if ( state & C . GDK_MOD1_MASK ) != 0 { // GTK+ itself requires this to be Alt (just read through gtkaccelgroup.c)
2014-08-04 22:31:11 -05:00
m |= Alt
}
if ( state & C . GDK_SHIFT_MASK ) != 0 {
m |= Shift
}
if ( state & C . GDK_SUPER_MASK ) != 0 {
m |= Super
}
return m
}
// shared code for finishing up and sending a mouse event
func finishMouseEvent ( widget * C . GtkWidget , data C . gpointer , me MouseEvent , mb uint , x C . gdouble , y C . gdouble , state C . guint , gdkwindow * C . GdkWindow ) {
var areawidth , areaheight C . gint
// on GTK+, mouse buttons 4-7 are for scrolling; if we got here, that's a mistake
if mb >= 4 && mb <= 7 {
return
}
a := ( * area ) ( unsafe . Pointer ( data ) )
state = translateModifiers ( state , gdkwindow )
me . Modifiers = makeModifiers ( state )
// the mb != # checks exclude the Up/Down button from Held
if mb != 1 && ( state & C . GDK_BUTTON1_MASK ) != 0 {
me . Held = append ( me . Held , 1 )
}
if mb != 2 && ( state & C . GDK_BUTTON2_MASK ) != 0 {
me . Held = append ( me . Held , 2 )
}
if mb != 3 && ( state & C . GDK_BUTTON3_MASK ) != 0 {
me . Held = append ( me . Held , 3 )
}
2014-08-14 22:28:22 -05:00
// don't check GDK_BUTTON4_MASK or GDK_BUTTON5_MASK because those are for the scrolling buttons mentioned above
// GDK expressly does not support any more buttons in the GdkModifierType; see https://git.gnome.org/browse/gtk+/tree/gdk/x11/gdkdevice-xi2.c#n763 (thanks mclasen in irc.gimp.net/#gtk+)
2014-08-04 22:31:11 -05:00
me . Pos = image . Pt ( int ( x ) , int ( y ) )
C . gtk_widget_get_size_request ( widget , & areawidth , & areaheight )
if ! me . Pos . In ( image . Rect ( 0 , 0 , int ( areawidth ) , int ( areaheight ) ) ) { // outside the actual Area; no event
return
}
// and finally, if the button ID >= 8, continue counting from 4, as above and as in the MouseEvent spec
if me . Down >= 8 {
me . Down -= 4
}
if me . Up >= 8 {
me . Up -= 4
}
2014-08-09 09:33:38 -05:00
a . handler . Mouse ( me )
2014-08-04 22:31:11 -05:00
}
// convenience name to make our intent clear
const continueEventChain C . gboolean = C . FALSE
2014-08-13 06:59:27 -05:00
const stopEventChain C . gboolean = C . TRUE
2014-08-04 22:31:11 -05:00
// checking for a mouse click that makes the program/window active is meaningless on GTK+: it's a property of the window manager/X11, and it's the WM that decides if the program should become active or not
// however, one thing is certain: the click event will ALWAYS be sent (to the window that the X11 decides to send it to)
// I assume the same is true for Wayland
// thanks Chipzz in irc.gimp.net/#gtk+
//export our_area_button_press_event_callback
func our_area_button_press_event_callback ( widget * C . GtkWidget , event * C . GdkEvent , data C . gpointer ) C . gboolean {
// clicking doesn't automatically transfer keyboard focus; we must do so manually (thanks tristan in irc.gimp.net/#gtk+)
C . gtk_widget_grab_focus ( widget )
e := ( * C . GdkEventButton ) ( unsafe . Pointer ( event ) )
me := MouseEvent {
// GDK button ID == our button ID with some exceptions taken care of by finishMouseEvent()
Down : uint ( e . button ) ,
}
var maxTime C . gint
var maxDistance C . gint
if e . _type != C . GDK_BUTTON_PRESS {
// ignore GDK's generated double-clicks and beyond; we handled those ourselves below
return continueEventChain
}
a := ( * area ) ( unsafe . Pointer ( data ) )
// e.time is unsigned and in milliseconds
// maxTime is also milliseconds; despite being gint, it is only allowed to be positive
// maxDistance is also only allowed to be positive
settings := C . gtk_widget_get_settings ( widget )
C . gtkGetDoubleClickSettings ( settings , & maxTime , & maxDistance )
me . Count = a . clickCounter . click ( me . Down , int ( e . x ) , int ( e . y ) ,
uintptr ( e . time ) , uintptr ( maxTime ) ,
int ( maxDistance ) , int ( maxDistance ) )
finishMouseEvent ( widget , data , me , me . Down , e . x , e . y , e . state , e . window )
return continueEventChain
}
var area_button_press_event_callback = C . GCallback ( C . our_area_button_press_event_callback )
//export our_area_button_release_event_callback
func our_area_button_release_event_callback ( widget * C . GtkWidget , event * C . GdkEvent , data C . gpointer ) C . gboolean {
e := ( * C . GdkEventButton ) ( unsafe . Pointer ( event ) )
me := MouseEvent {
// GDK button ID == our button ID with some exceptions taken care of by finishMouseEvent()
Up : uint ( e . button ) ,
}
finishMouseEvent ( widget , data , me , me . Up , e . x , e . y , e . state , e . window )
return continueEventChain
}
var area_button_release_event_callback = C . GCallback ( C . our_area_button_release_event_callback )
//export our_area_motion_notify_event_callback
func our_area_motion_notify_event_callback ( widget * C . GtkWidget , event * C . GdkEvent , data C . gpointer ) C . gboolean {
e := ( * C . GdkEventMotion ) ( unsafe . Pointer ( event ) )
me := MouseEvent { }
finishMouseEvent ( widget , data , me , 0 , e . x , e . y , e . state , e . window )
return continueEventChain
}
var area_motion_notify_event_callback = C . GCallback ( C . our_area_motion_notify_event_callback )
// we want switching away from the control to reset the double-click counter, like with WM_ACTIVATE on Windows
// according to tristan in irc.gimp.net/#gtk+, doing this on enter-notify-event and leave-notify-event is correct (and it seems to be true in my own tests; plus the events DO get sent when switching programs with the keyboard (just pointing that out))
// differentiating between enter-notify-event and leave-notify-event is unimportant
//export our_area_enterleave_notify_event_callback
func our_area_enterleave_notify_event_callback ( widget * C . GtkWidget , event * C . GdkEvent , data C . gpointer ) C . gboolean {
a := ( * area ) ( unsafe . Pointer ( data ) )
a . clickCounter . reset ( )
return continueEventChain
}
var area_enterleave_notify_event_callback = C . GCallback ( C . our_area_enterleave_notify_event_callback )
// shared code for doing a key event
2014-08-21 07:57:48 -05:00
func doKeyEvent ( widget * C . GtkWidget , event * C . GdkEvent , data C . gpointer , up bool ) bool {
2014-08-04 22:31:11 -05:00
var ke KeyEvent
e := ( * C . GdkEventKey ) ( unsafe . Pointer ( event ) )
a := ( * area ) ( unsafe . Pointer ( data ) )
keyval := e . keyval
// get modifiers now in case a modifier was pressed
state := translateModifiers ( e . state , e . window )
ke . Modifiers = makeModifiers ( state )
if extkey , ok := extkeys [ keyval ] ; ok {
ke . ExtKey = extkey
} else if mod , ok := modonlykeys [ keyval ] ; ok {
ke . Modifier = mod
// don't include the modifier in ke.Modifiers
ke . Modifiers &^= mod
} else if xke , ok := fromScancode ( uintptr ( e . hardware_keycode ) - 8 ) ; ok {
// see events_notdarwin.go for details of the above map lookup
// one of these will be nonzero
ke . Key = xke . Key
ke . ExtKey = xke . ExtKey
} else { // no match
2014-08-21 07:57:48 -05:00
return false
2014-08-04 22:31:11 -05:00
}
ke . Up = up
2014-08-21 07:57:48 -05:00
return a . handler . Key ( ke )
2014-08-04 22:31:11 -05:00
}
//export our_area_key_press_event_callback
func our_area_key_press_event_callback ( widget * C . GtkWidget , event * C . GdkEvent , data C . gpointer ) C . gboolean {
2014-08-21 07:57:48 -05:00
if doKeyEvent ( widget , event , data , false ) == true {
return stopEventChain
}
2014-08-04 22:31:11 -05:00
return continueEventChain
}
var area_key_press_event_callback = C . GCallback ( C . our_area_key_press_event_callback )
//export our_area_key_release_event_callback
func our_area_key_release_event_callback ( widget * C . GtkWidget , event * C . GdkEvent , data C . gpointer ) C . gboolean {
2014-08-21 07:57:48 -05:00
if doKeyEvent ( widget , event , data , true ) == true {
return stopEventChain
}
2014-08-04 22:31:11 -05:00
return continueEventChain
}
var area_key_release_event_callback = C . GCallback ( C . our_area_key_release_event_callback )
var extkeys = map [ C . guint ] ExtKey {
C . GDK_KEY_Escape : Escape ,
C . GDK_KEY_Insert : Insert ,
C . GDK_KEY_Delete : Delete ,
C . GDK_KEY_Home : Home ,
C . GDK_KEY_End : End ,
C . GDK_KEY_Page_Up : PageUp ,
C . GDK_KEY_Page_Down : PageDown ,
C . GDK_KEY_Up : Up ,
C . GDK_KEY_Down : Down ,
C . GDK_KEY_Left : Left ,
C . GDK_KEY_Right : Right ,
C . GDK_KEY_F1 : F1 ,
C . GDK_KEY_F2 : F2 ,
C . GDK_KEY_F3 : F3 ,
C . GDK_KEY_F4 : F4 ,
C . GDK_KEY_F5 : F5 ,
C . GDK_KEY_F6 : F6 ,
C . GDK_KEY_F7 : F7 ,
C . GDK_KEY_F8 : F8 ,
C . GDK_KEY_F9 : F9 ,
C . GDK_KEY_F10 : F10 ,
C . GDK_KEY_F11 : F11 ,
C . GDK_KEY_F12 : F12 ,
// numpad numeric keys and . are handled in events_notdarwin.go
C . GDK_KEY_KP_Enter : NEnter ,
C . GDK_KEY_KP_Add : NAdd ,
C . GDK_KEY_KP_Subtract : NSubtract ,
C . GDK_KEY_KP_Multiply : NMultiply ,
C . GDK_KEY_KP_Divide : NDivide ,
}
// sanity check
func init ( ) {
included := make ( [ ] bool , _nextkeys )
for _ , v := range extkeys {
included [ v ] = true
}
for i := 1 ; i < int ( _nextkeys ) ; i ++ {
if i >= int ( N0 ) && i <= int ( N9 ) { // skip numpad numbers and .
continue
}
if i == int ( NDot ) {
continue
}
if ! included [ i ] {
panic ( fmt . Errorf ( "error: not all ExtKeys defined on Unix (missing %d)" , i ) )
}
}
}
var modonlykeys = map [ C . guint ] Modifiers {
C . GDK_KEY_Control_L : Ctrl ,
C . GDK_KEY_Control_R : Ctrl ,
C . GDK_KEY_Alt_L : Alt ,
C . GDK_KEY_Alt_R : Alt ,
C . GDK_KEY_Meta_L : Alt ,
C . GDK_KEY_Meta_R : Alt ,
C . GDK_KEY_Shift_L : Shift ,
C . GDK_KEY_Shift_R : Shift ,
C . GDK_KEY_Super_L : Super ,
C . GDK_KEY_Super_R : Super ,
}
func ( a * area ) widget ( ) * C . GtkWidget {
return a . _widget
}
func ( a * area ) setParent ( p * controlParent ) {
a . scroller . setParent ( p )
}
func ( a * area ) allocate ( x int , y int , width int , height int , d * sizing ) [ ] * allocation {
return baseallocate ( a , x , y , width , height , d )
}
func ( a * area ) preferredSize ( d * sizing ) ( width , height int ) {
// the preferred size of an Area is its size
return a . width , a . height
}
func ( a * area ) commitResize ( c * allocation , d * sizing ) {
a . scroller . commitResize ( c , d )
}
func ( a * area ) getAuxResizeInfo ( d * sizing ) {
2014-08-05 19:43:56 -05:00
// a Label to the left of an Area should be vertically aligned to the top
d . shouldVAlignTop = true
2014-08-04 22:31:11 -05:00
}