| @@ -18,7 +18,7 @@ test_map_CXXFLAGS = `pkg-config --cflags sdl gl SDL_image` | |||
| test_map_LDADD = libcommon.a | |||
| test_map_LDFLAGS = `pkg-config --libs sdl gl SDL_image` -lpipi | |||
| editor_SOURCES = gtk/editor.cpp | |||
| editor_SOURCES = gtk/editor.cpp gtk/glmapview.cpp gtk/glmapview.h | |||
| editor_CXXFLAGS = `pkg-config --cflags sdl gl SDL_image gtk+-2.0 gtkgl-2.0` | |||
| editor_LDADD = libcommon.a | |||
| editor_LDFLAGS = `pkg-config --libs sdl gl gtk+-2.0 gtkgl-2.0 SDL_image` | |||
| @@ -7,178 +7,15 @@ | |||
| # include "config.h" | |||
| #endif | |||
| #include <cstdio> | |||
| #include <cstdlib> | |||
| #include <cmath> | |||
| #include <gtk/gtk.h> | |||
| #include <gtkgl/gtkglarea.h> | |||
| #include "core.h" | |||
| #include "glmapview.h" | |||
| #include "debugfps.h" | |||
| static volatile int quit = 0; | |||
| static int ticking = 0; | |||
| static int panning = 0; | |||
| static double xpan = 0.0f, ypan = 0.0f; | |||
| static float const FPS = 30.0f; | |||
| static MapViewer *mv; | |||
| static GtkWidget *glarea; | |||
| static GtkAdjustment *hadj, *vadj; | |||
| static gint main_quit(GtkWidget *widget, GdkEventExpose *event) | |||
| { | |||
| (void)widget; | |||
| (void)event; | |||
| quit = 1; | |||
| gtk_main_quit(); | |||
| return FALSE; | |||
| } | |||
| static gboolean tick(void *widget) | |||
| { | |||
| // FIXME: do not do anything if the previous tick was too recent? | |||
| // FIXME: only quit if all entities have been cleaned | |||
| if (quit) | |||
| return FALSE; | |||
| ticking = 1; | |||
| mv->SetPOV(gtk_adjustment_get_value(hadj), gtk_adjustment_get_value(vadj)); | |||
| /* Tick the game */ | |||
| Ticker::TickGame(); | |||
| gtk_widget_draw(GTK_WIDGET(widget), NULL); | |||
| return TRUE; | |||
| } | |||
| static gboolean mouse_button(GtkWidget *widget, GdkEventButton *event, | |||
| gpointer user_data) | |||
| { | |||
| if (event->type == GDK_BUTTON_PRESS && event->button == 2) | |||
| { | |||
| panning = 1; | |||
| xpan = event->x; | |||
| ypan = event->y; | |||
| GdkCursor *cursor = gdk_cursor_new(GDK_HAND1); | |||
| gdk_window_set_cursor(widget->window, cursor); | |||
| gdk_cursor_unref(cursor); | |||
| return FALSE; | |||
| } | |||
| else if (event->type == GDK_BUTTON_RELEASE && event->button == 2) | |||
| { | |||
| panning = 0; | |||
| gdk_window_set_cursor(widget->window, NULL); | |||
| return FALSE; | |||
| } | |||
| return TRUE; | |||
| } | |||
| static gboolean mouse_motion(GtkWidget *widget, GdkEventMotion *event, | |||
| gpointer user_data) | |||
| { | |||
| if (panning) | |||
| { | |||
| if (event->x != xpan) | |||
| { | |||
| double val = gtk_adjustment_get_value(hadj); | |||
| val += xpan - event->x; | |||
| xpan = event->x; | |||
| if (val + widget->allocation.width > mv->GetWidth()) | |||
| val = mv->GetWidth() - widget->allocation.width; | |||
| gtk_adjustment_set_value(hadj, val); | |||
| gtk_adjustment_value_changed(hadj); | |||
| } | |||
| if (event->y != ypan) | |||
| { | |||
| double val = gtk_adjustment_get_value(vadj); | |||
| val += ypan - event->y; | |||
| ypan = event->y; | |||
| if (val + widget->allocation.height > mv->GetHeight()) | |||
| val = mv->GetHeight() - widget->allocation.height; | |||
| gtk_adjustment_set_value(vadj, val); | |||
| gtk_adjustment_value_changed(vadj); | |||
| } | |||
| } | |||
| return TRUE; | |||
| } | |||
| static gint init(GtkWidget *widget) | |||
| { | |||
| /* Manage adjustments */ | |||
| struct | |||
| { | |||
| GtkAdjustment *adj; | |||
| float map_size, sw_size; | |||
| } | |||
| s[2] = | |||
| { | |||
| { hadj, mv->GetWidth(), widget->allocation.width }, | |||
| { vadj, mv->GetHeight(), widget->allocation.height }, | |||
| }; | |||
| for (int i = 0; i < 2; i++) | |||
| { | |||
| gtk_adjustment_set_lower(s[i].adj, 0); | |||
| gtk_adjustment_set_upper(s[i].adj, s[i].map_size); | |||
| gtk_adjustment_set_step_increment(s[i].adj, 1); | |||
| gtk_adjustment_set_page_increment(s[i].adj, s[i].sw_size); | |||
| gtk_adjustment_set_page_size(s[i].adj, s[i].sw_size); | |||
| float val = gtk_adjustment_get_value(s[i].adj); | |||
| if (val + s[i].sw_size > s[i].map_size) | |||
| { | |||
| gtk_adjustment_set_value(s[i].adj, s[i].map_size - s[i].sw_size); | |||
| gtk_adjustment_value_changed(s[i].adj); | |||
| } | |||
| } | |||
| /* Set up display */ | |||
| if (gtk_gl_area_make_current(GTK_GL_AREA(widget))) | |||
| Video::Setup(widget->allocation.width, widget->allocation.height); | |||
| return TRUE; | |||
| } | |||
| static gint reshape(GtkWidget *widget, GdkEventConfigure *event) | |||
| { | |||
| (void)event; | |||
| return init(widget); | |||
| } | |||
| static gint draw(GtkWidget *widget, GdkEventExpose *event) | |||
| { | |||
| if (event->count > 0) | |||
| return TRUE; | |||
| /* OpenGL functions can be called only if make_current returns true */ | |||
| if (ticking && gtk_gl_area_make_current(GTK_GL_AREA(widget))) | |||
| { | |||
| ticking = 0; | |||
| /* Clear the screen, tick the renderer, show the frame and | |||
| * clamp to desired framerate */ | |||
| Video::Clear(); | |||
| Ticker::TickDraw(); | |||
| gtk_gl_area_swapbuffers(GTK_GL_AREA(widget)); | |||
| while (g_main_context_iteration(NULL, FALSE)) | |||
| ; | |||
| Ticker::ClampFps(1000.0f / FPS); | |||
| } | |||
| return TRUE; | |||
| } | |||
| int main(int argc, char **argv) | |||
| { | |||
| /* Initialize GTK */ | |||
| @@ -191,69 +28,29 @@ int main(int argc, char **argv) | |||
| return EXIT_FAILURE; | |||
| } | |||
| /* Build the application interface and keep a few member pointers */ | |||
| /* Build the application interface */ | |||
| GtkBuilder *builder = gtk_builder_new(); | |||
| if (!gtk_builder_add_from_file(builder, "src/gtk/editor.xml", NULL)) | |||
| { | |||
| g_print("Cannot build from XML\n"); | |||
| return EXIT_FAILURE; | |||
| } | |||
| gtk_builder_connect_signals(builder, NULL); | |||
| GtkWidget *window = GTK_WIDGET(gtk_builder_get_object(builder, "window1")); | |||
| GtkWidget *viewport = GTK_WIDGET( | |||
| gtk_builder_get_object(builder, "viewport1")); | |||
| hadj = gtk_range_get_adjustment(GTK_RANGE( | |||
| gtk_builder_get_object(builder, "hscrollbar1"))); | |||
| vadj = gtk_range_get_adjustment(GTK_RANGE( | |||
| gtk_builder_get_object(builder, "vscrollbar1"))); | |||
| g_object_unref(G_OBJECT(builder)); | |||
| /* Add our custom GL map viewer */ | |||
| GlMapView *glmapview = new GlMapView(builder); | |||
| /* Create new OpenGL widget */ | |||
| int attrlist[] = | |||
| { | |||
| GDK_GL_RGBA, | |||
| GDK_GL_RED_SIZE, 1, | |||
| GDK_GL_GREEN_SIZE, 1, | |||
| GDK_GL_BLUE_SIZE, 1, | |||
| GDK_GL_DOUBLEBUFFER, | |||
| GDK_GL_NONE | |||
| }; | |||
| glarea = gtk_gl_area_new(attrlist); | |||
| gtk_widget_set_usize(glarea, 400, 300); | |||
| gtk_widget_set_events(glarea, GDK_EXPOSURE_MASK | GDK_POINTER_MOTION_MASK | |||
| | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK); | |||
| gtk_container_add(GTK_CONTAINER(viewport), glarea); | |||
| /* We tick from the idle function instead of a timeout to avoid | |||
| * stealing time from the GTK loop when the callback time exceeds | |||
| * the timeout value. */ | |||
| gtk_idle_add(tick, glarea); | |||
| /* Connect signals and show window */ | |||
| gtk_signal_connect(GTK_OBJECT(window), "delete_event", | |||
| GTK_SIGNAL_FUNC(main_quit), NULL); | |||
| gtk_signal_connect(GTK_OBJECT(glarea), "expose_event", | |||
| GTK_SIGNAL_FUNC(draw), NULL); | |||
| gtk_signal_connect(GTK_OBJECT(glarea), "configure_event", | |||
| GTK_SIGNAL_FUNC(reshape), NULL); | |||
| gtk_signal_connect(GTK_OBJECT(glarea), "realize", | |||
| GTK_SIGNAL_FUNC(init), NULL); | |||
| gtk_signal_connect(GTK_OBJECT(glarea), "button_press_event", | |||
| GTK_SIGNAL_FUNC(mouse_button), NULL); | |||
| gtk_signal_connect(GTK_OBJECT(glarea), "button_release_event", | |||
| GTK_SIGNAL_FUNC(mouse_button), NULL); | |||
| gtk_signal_connect(GTK_OBJECT(glarea), "motion_notify_event", | |||
| GTK_SIGNAL_FUNC(mouse_motion), NULL); | |||
| /* Show window. We're good to go! */ | |||
| gtk_widget_show_all(GTK_WIDGET(gtk_builder_get_object(builder, "window1"))); | |||
| g_object_unref(G_OBJECT(builder)); | |||
| // FIXME: detect when the map viewer is killed | |||
| mv = new MapViewer("maps/testmap.tmx"); | |||
| new DebugFps(); | |||
| glmapview->LoadMap("maps/testmap.tmx"); | |||
| gtk_widget_show_all(window); | |||
| gtk_main(); | |||
| delete glmapview; | |||
| return EXIT_SUCCESS; | |||
| } | |||
| @@ -4,6 +4,7 @@ | |||
| <!-- interface-naming-policy project-wide --> | |||
| <object class="GtkWindow" id="window1"> | |||
| <property name="title" translatable="yes">Deus Hax Editor</property> | |||
| <signal name="delete_event" handler="gtk_main_quit"/> | |||
| <child> | |||
| <object class="GtkVBox" id="vbox1"> | |||
| <property name="visible">True</property> | |||
| @@ -160,6 +161,7 @@ | |||
| <child> | |||
| <object class="GtkHScrollbar" id="hscrollbar1"> | |||
| <property name="visible">True</property> | |||
| <property name="adjustment">gl_hadj</property> | |||
| </object> | |||
| <packing> | |||
| <property name="top_attach">1</property> | |||
| @@ -171,6 +173,7 @@ | |||
| <object class="GtkVScrollbar" id="vscrollbar1"> | |||
| <property name="visible">True</property> | |||
| <property name="orientation">vertical</property> | |||
| <property name="adjustment">gl_vadj</property> | |||
| </object> | |||
| <packing> | |||
| <property name="left_attach">1</property> | |||
| @@ -179,7 +182,7 @@ | |||
| </packing> | |||
| </child> | |||
| <child> | |||
| <object class="GtkViewport" id="viewport1"> | |||
| <object class="GtkViewport" id="gl_container"> | |||
| <property name="visible">True</property> | |||
| <property name="resize_mode">queue</property> | |||
| <child> | |||
| @@ -229,4 +232,16 @@ | |||
| </object> | |||
| </child> | |||
| </object> | |||
| <object class="GtkAdjustment" id="gl_hadj"> | |||
| <property name="upper">100</property> | |||
| <property name="step_increment">1</property> | |||
| <property name="page_increment">10</property> | |||
| <property name="page_size">10</property> | |||
| </object> | |||
| <object class="GtkAdjustment" id="gl_vadj"> | |||
| <property name="upper">100</property> | |||
| <property name="step_increment">1</property> | |||
| <property name="page_increment">10</property> | |||
| <property name="page_size">10</property> | |||
| </object> | |||
| </interface> | |||
| @@ -0,0 +1,257 @@ | |||
| // | |||
| // Deus Hax (working title) | |||
| // Copyright (c) 2010 Sam Hocevar <sam@hocevar.net> | |||
| // | |||
| #if defined HAVE_CONFIG_H | |||
| # include "config.h" | |||
| #endif | |||
| #include <gtk/gtk.h> | |||
| #include <gtkgl/gtkglarea.h> | |||
| #include "core.h" | |||
| #include "glmapview.h" | |||
| static float const FPS = 30.0f; | |||
| GlMapView::GlMapView(GtkBuilder *builder) | |||
| : hadj(GTK_ADJUSTMENT(gtk_builder_get_object(builder, "gl_hadj"))), | |||
| vadj(GTK_ADJUSTMENT(gtk_builder_get_object(builder, "gl_vadj"))), | |||
| ticking(FALSE), panning(FALSE), | |||
| mapviewer(0), | |||
| xpan(0.0), ypan(0.0) | |||
| { | |||
| /* Create new OpenGL widget */ | |||
| int attrlist[] = | |||
| { | |||
| GDK_GL_RGBA, | |||
| GDK_GL_RED_SIZE, 1, | |||
| GDK_GL_GREEN_SIZE, 1, | |||
| GDK_GL_BLUE_SIZE, 1, | |||
| GDK_GL_DOUBLEBUFFER, | |||
| GDK_GL_NONE | |||
| }; | |||
| glarea = gtk_gl_area_new(attrlist); | |||
| gtk_widget_set_usize(glarea, 400, 300); | |||
| gtk_widget_set_events(glarea, GDK_EXPOSURE_MASK | | |||
| GDK_POINTER_MOTION_MASK | | |||
| GDK_BUTTON_PRESS_MASK | | |||
| GDK_BUTTON_RELEASE_MASK); | |||
| GtkContainer *cont = GTK_CONTAINER(gtk_builder_get_object(builder, | |||
| "gl_container")); | |||
| gtk_container_add(cont, glarea); | |||
| /* We tick from the idle function instead of a timeout to avoid | |||
| * stealing time from the GTK loop when the callback time exceeds | |||
| * the timeout value. */ | |||
| g_idle_add((GSourceFunc)IdleTickSignal, this); | |||
| gtk_signal_connect(GTK_OBJECT(glarea), "realize", | |||
| GTK_SIGNAL_FUNC(SetupSignal), this); | |||
| gtk_signal_connect(GTK_OBJECT(glarea), "destroy", | |||
| GTK_SIGNAL_FUNC(DestroySignal), this); | |||
| gtk_signal_connect(GTK_OBJECT(glarea), "expose_event", | |||
| GTK_SIGNAL_FUNC(DrawSignal), this); | |||
| gtk_signal_connect(GTK_OBJECT(glarea), "configure_event", | |||
| GTK_SIGNAL_FUNC(ReshapeSignal), this); | |||
| gtk_signal_connect(GTK_OBJECT(glarea), "button_press_event", | |||
| GTK_SIGNAL_FUNC(MouseButtonSignal), this); | |||
| gtk_signal_connect(GTK_OBJECT(glarea), "button_release_event", | |||
| GTK_SIGNAL_FUNC(MouseButtonSignal), this); | |||
| gtk_signal_connect(GTK_OBJECT(glarea), "motion_notify_event", | |||
| GTK_SIGNAL_FUNC(MouseMotionSignal), this); | |||
| } | |||
| void GlMapView::LoadMap(char const *path) | |||
| { | |||
| // FIXME: detect when the map viewer is killed | |||
| mapviewer = new MapViewer(path); | |||
| } | |||
| gboolean GlMapView::IdleTick() | |||
| { | |||
| // FIXME: do not do anything if the previous tick was too recent? | |||
| ticking = TRUE; | |||
| if (mapviewer) | |||
| mapviewer->SetPOV(gtk_adjustment_get_value(hadj), | |||
| gtk_adjustment_get_value(vadj)); | |||
| /* Tick the game */ | |||
| Ticker::TickGame(); | |||
| gtk_widget_draw(GTK_WIDGET(glarea), NULL); | |||
| return TRUE; | |||
| } | |||
| gboolean GlMapView::Setup() | |||
| { | |||
| if (mapviewer) | |||
| { | |||
| /* Manage adjustments */ | |||
| struct { GtkAdjustment *adj; float map_size, sw_size; } s[2] = | |||
| { | |||
| { hadj, mapviewer->GetWidth(), glarea->allocation.width }, | |||
| { vadj, mapviewer->GetHeight(), glarea->allocation.height }, | |||
| }; | |||
| for (int i = 0; i < 2; i++) | |||
| { | |||
| gtk_adjustment_set_lower(s[i].adj, 0); | |||
| gtk_adjustment_set_upper(s[i].adj, s[i].map_size); | |||
| gtk_adjustment_set_step_increment(s[i].adj, 1); | |||
| gtk_adjustment_set_page_increment(s[i].adj, s[i].sw_size); | |||
| gtk_adjustment_set_page_size(s[i].adj, s[i].sw_size); | |||
| float val = gtk_adjustment_get_value(s[i].adj); | |||
| if (val + s[i].sw_size > s[i].map_size) | |||
| { | |||
| gtk_adjustment_set_value(s[i].adj, | |||
| s[i].map_size - s[i].sw_size); | |||
| gtk_adjustment_value_changed(s[i].adj); | |||
| } | |||
| } | |||
| } | |||
| /* Set up display */ | |||
| if (gtk_gl_area_make_current(GTK_GL_AREA(glarea))) | |||
| Video::Setup(glarea->allocation.width, glarea->allocation.height); | |||
| return TRUE; | |||
| } | |||
| gboolean GlMapView::Destroy() | |||
| { | |||
| g_idle_remove_by_data(this); | |||
| return TRUE; | |||
| } | |||
| gboolean GlMapView::Draw(GdkEventExpose *event) | |||
| { | |||
| if (event->count > 0) | |||
| return TRUE; | |||
| /* OpenGL functions can be called only if make_current returns true */ | |||
| if (ticking && gtk_gl_area_make_current(GTK_GL_AREA(glarea))) | |||
| { | |||
| ticking = FALSE; | |||
| /* Clear the screen, tick the renderer, show the frame and | |||
| * clamp to desired framerate */ | |||
| Video::Clear(); | |||
| Ticker::TickDraw(); | |||
| gtk_gl_area_swapbuffers(GTK_GL_AREA(glarea)); | |||
| while (g_main_context_iteration(NULL, FALSE)) | |||
| ; | |||
| Ticker::ClampFps(1000.0f / FPS); | |||
| } | |||
| return TRUE; | |||
| } | |||
| gboolean GlMapView::MouseButton(GdkEventButton *event) | |||
| { | |||
| if (event->type == GDK_BUTTON_PRESS && event->button == 2) | |||
| { | |||
| panning = TRUE; | |||
| xpan = event->x; | |||
| ypan = event->y; | |||
| GdkCursor *cursor = gdk_cursor_new(GDK_HAND1); | |||
| gdk_window_set_cursor(glarea->window, cursor); | |||
| gdk_cursor_unref(cursor); | |||
| return FALSE; | |||
| } | |||
| else if (event->type == GDK_BUTTON_RELEASE && event->button == 2) | |||
| { | |||
| panning = FALSE; | |||
| gdk_window_set_cursor(glarea->window, NULL); | |||
| return FALSE; | |||
| } | |||
| return TRUE; | |||
| } | |||
| gboolean GlMapView::MouseMotion(GdkEventMotion *event) | |||
| { | |||
| if (panning) | |||
| { | |||
| if (event->x != xpan) | |||
| { | |||
| double val = gtk_adjustment_get_value(hadj); | |||
| int map_width = mapviewer->GetWidth(); | |||
| val += xpan - event->x; | |||
| xpan = event->x; | |||
| if (val + glarea->allocation.width > map_width) | |||
| val = map_width - glarea->allocation.width; | |||
| gtk_adjustment_set_value(hadj, val); | |||
| gtk_adjustment_value_changed(hadj); | |||
| } | |||
| if (event->y != ypan) | |||
| { | |||
| double val = gtk_adjustment_get_value(vadj); | |||
| int map_height = mapviewer->GetHeight(); | |||
| val += ypan - event->y; | |||
| ypan = event->y; | |||
| if (val + glarea->allocation.height > map_height) | |||
| val = map_height - glarea->allocation.height; | |||
| gtk_adjustment_set_value(vadj, val); | |||
| gtk_adjustment_value_changed(vadj); | |||
| } | |||
| } | |||
| return TRUE; | |||
| } | |||
| /* Private signal slots */ | |||
| gboolean GlMapView::IdleTickSignal(GlMapView *that) | |||
| { | |||
| return that->IdleTick(); | |||
| } | |||
| gboolean GlMapView::SetupSignal(GtkWidget *w, GlMapView *that) | |||
| { | |||
| (void)w; | |||
| return that->Setup(); | |||
| } | |||
| gboolean GlMapView::DestroySignal(GtkWidget *w, GlMapView *that) | |||
| { | |||
| (void)w; | |||
| return that->Destroy(); | |||
| } | |||
| gboolean GlMapView::DrawSignal(GtkWidget *w, GdkEventExpose *event, | |||
| GlMapView *that) | |||
| { | |||
| (void)w; | |||
| return that->Draw(event); | |||
| } | |||
| gboolean GlMapView::ReshapeSignal(GtkWidget *w, GdkEventConfigure *event, | |||
| GlMapView *that) | |||
| { | |||
| (void)w; | |||
| (void)event; | |||
| return that->Setup(); | |||
| } | |||
| gboolean GlMapView::MouseButtonSignal(GtkWidget *w, GdkEventButton *event, | |||
| GlMapView *that) | |||
| { | |||
| (void)w; | |||
| return that->MouseButton(event); | |||
| } | |||
| gboolean GlMapView::MouseMotionSignal(GtkWidget *w, GdkEventMotion *event, | |||
| GlMapView *that) | |||
| { | |||
| (void)w; | |||
| return that->MouseMotion(event); | |||
| } | |||
| @@ -0,0 +1,49 @@ | |||
| // | |||
| // Deus Hax (working title) | |||
| // Copyright (c) 2010 Sam Hocevar <sam@hocevar.net> | |||
| // | |||
| #if !defined __DH_GLMAPVIEW_H__ | |||
| #define __DH_GLMAPVIEW_H__ | |||
| #include <gtk/gtk.h> | |||
| class GlMapView | |||
| { | |||
| public: | |||
| GlMapView(GtkBuilder *builder); | |||
| void LoadMap(char const *path); | |||
| private: | |||
| /* Private methods */ | |||
| gboolean IdleTick(); | |||
| gboolean Setup(); | |||
| gboolean Destroy(); | |||
| gboolean Draw(GdkEventExpose *event); | |||
| gboolean MouseButton(GdkEventButton *event); | |||
| gboolean MouseMotion(GdkEventMotion *event); | |||
| /* Private signal slots */ | |||
| static gboolean IdleTickSignal(GlMapView *that); | |||
| static gboolean SetupSignal(GtkWidget *w, GlMapView *that); | |||
| static gboolean DestroySignal(GtkWidget *w, GlMapView *that); | |||
| static gboolean DrawSignal(GtkWidget *w, GdkEventExpose *event, | |||
| GlMapView *that); | |||
| static gboolean ReshapeSignal(GtkWidget *w, GdkEventConfigure *event, | |||
| GlMapView *that); | |||
| static gboolean MouseButtonSignal(GtkWidget *w, GdkEventButton *event, | |||
| GlMapView *that); | |||
| static gboolean MouseMotionSignal(GtkWidget *w, GdkEventMotion *event, | |||
| GlMapView *that); | |||
| private: | |||
| GtkAdjustment *hadj, *vadj; | |||
| GtkWidget *glarea; | |||
| gboolean ticking, panning; | |||
| MapViewer *mapviewer; | |||
| double xpan, ypan; | |||
| }; | |||
| #endif // __DH_GLMAPVIEW_H__ | |||