/*
 *  xfce4_mcs_cursor_plugin - Cursor theme plugin for xfce4 mcs manager
 *  Copyright (c) 2005 Pasi Orovuo <pasi.ov@gmail.com>
 *  Copyright (c) 2005 Brian Tarricone <bjt23@cornell.edu>
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; version 2 of the License ONLY.
 *
 *  This program 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 Library General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#ifdef HAVE_XCURSOR_EXTENSION

#ifdef HAVE_STRING_H
#include <string.h>
#endif

#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif

#include <X11/Xlib.h>
#include <X11/Xcursor/Xcursor.h>

#include <gtk/gtk.h>

#include <libxfce4util/libxfce4util.h>

#include <libxfce4mcs/mcs-common.h>
#include <libxfce4mcs/mcs-manager.h>
#include <xfce-mcs-manager/manager-plugin.h>

#include <libxfcegui4/libxfcegui4.h>

#include "mouse-plugin-internal.h"


#define BORDER          ( 6 )
#define CURSOR_SIZE_MIN ( 8 )
#define CURSOR_SIZE_MAX ( 64 )

static gchar            *cursor_theme = NULL;
static guint            cursor_size = 0;
/* XDG? */
static gchar        *cursor_dirs[][2] = {
    { "%s/.icons/",                     "HOME" },
    { "%s/.themes/",                    "HOME" },
    { "/usr/share/cursors/xorg-x11/",   NULL },
    { "/usr/share/cursors/xfree/",      NULL },
    { "/usr/X11R6/lib/X11/icons/",      NULL },
    { "/usr/Xorg/lib/X11/icons/",       NULL },
    { "/usr/share/icons",               NULL },
    { NULL, NULL }
};
/* Number of icons in preview */
#define NUM_PREVIEW         ( 6 )
#define PREVIEW_SIZE        ( 24 )
/* List from kde kcontrol */
const static gchar     *preview_filenames[] = {
    "left_ptr",             "left_ptr_watch",       "watch",                "hand2",
    "question_arrow",       "sb_h_double_arrow",    "sb_v_double_arrow",    "bottom_left_corner",
    "bottom_right_corner",  "fleur",                "pirate",               "cross",
    "X_cursor",             "right_ptr",            "right_side",           "right_tee",
    "sb_right_arrow",       "sb_right_tee",         "base_arrow_down",      "base_arrow_up",
    "bottom_side",          "bottom_tee",           "center_ptr",           "circle",
    "dot",                  "dot_box_mask",         "dot_box_mask",         "double_arrow",
    "draped_box",           "left_side",            "left_tee",             "ll_angle",
    "top_side",             "top_tee"
};
#define NUM_PREVIEW_NAMES   ( sizeof( preview_filenames ) / sizeof( preview_filenames[0] ) )

static void
show_cursor_apply_warning_dlg(Itf *dialog)
{
    GtkWidget *dlg, *hbox, *vbox, *img, *lbl, *chk;

    dlg = gtk_dialog_new_with_buttons(_( "Mouse Settings" ),
            GTK_WINDOW(dialog->mouse_dialog),
            GTK_DIALOG_DESTROY_WITH_PARENT|GTK_DIALOG_NO_SEPARATOR,
            GTK_STOCK_CLOSE, GTK_RESPONSE_ACCEPT, NULL);

    vbox = gtk_vbox_new(FALSE, 0);
    gtk_widget_show(vbox);
    gtk_box_pack_start(GTK_BOX(GTK_DIALOG(dlg)->vbox), vbox, TRUE, TRUE, 0);

    hbox = gtk_hbox_new(FALSE, BORDER);
    gtk_container_set_border_width(GTK_CONTAINER(hbox), BORDER);
    gtk_widget_show(hbox);
    gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0);

    img = gtk_image_new_from_stock(GTK_STOCK_DIALOG_INFO, GTK_ICON_SIZE_DIALOG);
    gtk_misc_set_alignment(GTK_MISC(img), 0.0, 0.0);
    gtk_widget_show(img);
    gtk_box_pack_start(GTK_BOX(hbox), img, FALSE, FALSE, 0);

    lbl = gtk_label_new("");
    gtk_label_set_markup(GTK_LABEL(lbl), _("<span weight='bold' size='large'>Cursor settings saved.</span>\n\nMouse cursor settings may not be applied until you restart Xfce."));
    gtk_label_set_use_markup(GTK_LABEL(lbl), TRUE);
    gtk_label_set_line_wrap(GTK_LABEL(lbl), TRUE);
    gtk_widget_show(lbl);
    gtk_box_pack_start(GTK_BOX(hbox), lbl, TRUE, TRUE, 0);

    chk = gtk_check_button_new_with_mnemonic(_("_Don't show this again"));
    gtk_widget_show(chk);
    gtk_box_pack_start(GTK_BOX(vbox), chk, FALSE, FALSE, 0);

    gtk_dialog_run(GTK_DIALOG(dlg));

    if(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(chk))) {
        mcs_manager_set_int(dialog->mcs_plugin->manager, "Cursor/ShowApplyWarning", CHANNEL2, 0);
        mcs_manager_notify(dialog->mcs_plugin->manager, CHANNEL2);
        mouse_plugin_write_options(dialog->mcs_plugin);
    }

    gtk_widget_destroy(dlg);
}

static void
cursor_theme_set( const gchar *theme, guint size )
{
    gchar *xrdb_file, *xrdb_file_new, *cmd;
    FILE *fp;
    GError *error = NULL;

    if ( !theme || size <= 0 ) {
        g_warning( "Mouse Settings: Can't set theme %s (%u)", theme ? theme : "(null)", size );
        return;
    }

    xrdb_file = xfce_resource_save_location(XFCE_RESOURCE_CONFIG, "xfce4/Xcursor.xrdb", TRUE);
    if(!xrdb_file) {
        g_critical(_("Mouse Settings: Unable to create %s"), xrdb_file);
        g_free(xrdb_file);
        return;
    }

    xrdb_file_new = g_strconcat(xrdb_file, ".new", NULL);
    fp = fopen(xrdb_file_new, "w");
    if(!fp) {
        g_critical(_("Mouse Settings: Unable to create %s"), xrdb_file_new);
        g_free(xrdb_file_new);
        g_free(xrdb_file);
        return;
    }
    fprintf(fp, "Xcursor.theme: %s\n"
                "Xcursor.theme_core: true\n"
                "Xcursor.size: %d\n", theme, size );
    fclose(fp);

    if(rename(xrdb_file_new, xrdb_file)) {
        g_critical(_("Mouse Settings: Unable to move %s to %s.  Cursor settings may not be reapplied correctly on restart."), xrdb_file_new, xrdb_file);
        g_free(xrdb_file_new);
        g_free(xrdb_file);
        return;
    }

    g_free(xrdb_file_new);

    cmd = g_strdup_printf("xrdb -nocpp -merge \"%s\"", xrdb_file);
    if(!g_spawn_command_line_async(cmd, &error)) {
        g_critical(_("Mouse Settings: Failed to run xrdb.  Cursor settings may not be applied correctly. (Error was: %s)"), error->message);
        g_error_free(error);
    }

    g_free(cmd);
    g_free(xrdb_file);

    return;
}

static void
cursor_plugin_pixbuf_destroy_notify_cb( guchar *pixels, gpointer data )
{
    g_free( pixels );
}

static GdkPixbuf *
cursor_image_get_pixbuf( XcursorImage *cursor )
{
    GdkPixbuf   *pixbuf = NULL;
    guchar      *data, *p;
    gsize       dlen = cursor->width * cursor->height * sizeof( XcursorPixel );
    guint       i;

    data = g_malloc( dlen );
    for ( i = 0, p = (guchar *) cursor->pixels; i < dlen; i += 4, p += 4 ) {
        data[i]         = p[2];
        data[i + 1]     = p[1];
        data[i + 2]     = p[0];
        data[i + 3]     = p[3];
    }

    pixbuf = gdk_pixbuf_new_from_data( data,
                                       GDK_COLORSPACE_RGB,
                                       TRUE,
                                       8,
                                       cursor->width, cursor->height,
                                       cursor->width * sizeof( XcursorPixel ),
                                       cursor_plugin_pixbuf_destroy_notify_cb,
                                       NULL );
    if ( pixbuf == NULL ) {
        g_free( data );
        return ( NULL );
    }

    if ( cursor->width != PREVIEW_SIZE || cursor->height != PREVIEW_SIZE ) {
        gfloat          f;
        GdkPixbuf       *tmp;
        guint           w, h;

        f = (gfloat) cursor->width / (gfloat) cursor->height;
        if ( f >= 1.0f ) {
            w = (gfloat) PREVIEW_SIZE / f;
            h = PREVIEW_SIZE;
        }
        else {
            w = PREVIEW_SIZE;
            h = (gfloat) PREVIEW_SIZE * f;
        }
        tmp = gdk_pixbuf_scale_simple( pixbuf, w, h, GDK_INTERP_BILINEAR );

        g_return_val_if_fail( tmp != NULL, pixbuf );

        gdk_pixbuf_unref( pixbuf );
        pixbuf = tmp;
    }

    return ( pixbuf );
}

static GdkPixbuf *
generate_preview_image( GtkWidget *widget, const gchar *theme_path )
{
    gint i, num_loaded;
    GdkPixbuf *preview_pix = NULL;
    GdkPixmap *pmap;
    GtkStyle *style;

    if(!GTK_WIDGET_REALIZED(widget))
        gtk_widget_realize(widget);

    pmap = gdk_pixmap_new(GDK_DRAWABLE(widget->window),
                          NUM_PREVIEW*PREVIEW_SIZE, PREVIEW_SIZE, -1);
    style = gtk_widget_get_style(widget);

    gdk_draw_rectangle(GDK_DRAWABLE(pmap), style->bg_gc[GTK_STATE_NORMAL], TRUE,
                       0, 0, NUM_PREVIEW*PREVIEW_SIZE, PREVIEW_SIZE);

    for ( i = 0, num_loaded = 0; i < NUM_PREVIEW_NAMES && num_loaded < NUM_PREVIEW; i++ ) {
        XcursorImage    *cursor;
        gchar           *fn = g_build_filename( theme_path, preview_filenames[i], NULL );

        cursor = XcursorFilenameLoadImage( fn, PREVIEW_SIZE );

        if ( cursor ) {
            GdkPixbuf *pb = cursor_image_get_pixbuf( cursor );
            if ( pb ) {
                gdk_draw_pixbuf(GDK_DRAWABLE(pmap),
                                style->bg_gc[GTK_STATE_NORMAL], pb, 0, 0,
                                num_loaded*PREVIEW_SIZE, 0,
                                gdk_pixbuf_get_width(pb),
                                gdk_pixbuf_get_height(pb),
                                GDK_RGB_DITHER_NONE, 0, 0);
                g_object_unref( G_OBJECT(pb) );
                num_loaded++;
            }
            else {
                g_warning( "pb == NULL" );
            }
            XcursorImageDestroy( cursor );
        }
    }

    if(num_loaded > 0) {
        preview_pix = gdk_pixbuf_get_from_drawable(NULL, GDK_DRAWABLE(pmap),
                                                   NULL, 0, 0, 0, 0,
                                                   NUM_PREVIEW*PREVIEW_SIZE,
                                                   PREVIEW_SIZE);
    }

    g_object_unref(G_OBJECT(pmap));

    return preview_pix;
}

static GtkWidget *
preview_list_create( void )
{
    GtkWidget       *preview_list;

    preview_list = gtk_image_new();

    gtk_widget_set_size_request( preview_list,
                                 NUM_PREVIEW * PREVIEW_SIZE,
                                 PREVIEW_SIZE + BORDER );

    return ( preview_list );
}

enum {
    TLIST_THEME_NAME,
    TLIST_THEME_PATH,
    TLIST_NUM_COLUMNS
};

static gint
tree_sort_cmp_alpha(GtkTreeModel *model, GtkTreeIter *a, GtkTreeIter *b,
                    gpointer user_data)
{
    gchar *a_s = NULL, *b_s = NULL;
    gint ret = 0;

    gtk_tree_model_get(model, a, TLIST_THEME_NAME, &a_s, -1);
    gtk_tree_model_get(model, b, TLIST_THEME_NAME, &b_s, -1);

    if(!a_s)
        ret = -1;
    else if(!b_s)
        ret = 1;
    else
        ret = g_ascii_strcasecmp(a_s, b_s);

    g_free(a_s);
    g_free(b_s);

    return ret;
}

static void
theme_list_populate( GtkWidget *widget, const gchar *current_theme )
{
    GDir            *dir = NULL;
    guint           i;
    GtkTreeIter     iter;
    GtkListStore    *store;
    GHashTable      *themes;

    store = GTK_LIST_STORE( gtk_tree_view_get_model( GTK_TREE_VIEW( widget ) ) );

    gtk_list_store_append( store, &iter );
    gtk_list_store_set( store, &iter, 0, "default", -1 );

    themes = g_hash_table_new_full(g_str_hash, g_str_equal,
                                   (GDestroyNotify)g_free, NULL);

    for ( i = 0; cursor_dirs[i][0]; i++ ) {
        gchar       *curdir = cursor_dirs[i][0];

        if ( cursor_dirs[i][1] ) {
            curdir = g_strdup_printf( cursor_dirs[i][0], g_getenv( cursor_dirs[i][1] ) );
        }

        if ( ( dir = g_dir_open( curdir, 0, NULL ) ) ) {
            const gchar     *theme;

            for ( theme = g_dir_read_name( dir ); theme != NULL; theme = g_dir_read_name( dir ) ) {
                gchar       *full_path = g_build_filename( curdir, theme, "cursors", NULL );

                if ( g_file_test( full_path, G_FILE_TEST_IS_DIR )
                    && !g_hash_table_lookup(themes, theme))
                {
                    gtk_list_store_append( store, &iter );
                    gtk_list_store_set( store, &iter,
                                        TLIST_THEME_NAME, theme,
                                        TLIST_THEME_PATH, full_path,
                                        -1 );
                    g_hash_table_insert(themes, g_strdup(theme), GINT_TO_POINTER(1));

                    if ( current_theme && !strcmp( current_theme, theme ) ) {
                        GtkTreePath         *path;

                        path = gtk_tree_model_get_path( GTK_TREE_MODEL( store ), &iter );
                        gtk_tree_view_set_cursor( GTK_TREE_VIEW( widget ), path, NULL, FALSE );
                        gtk_tree_view_scroll_to_cell( GTK_TREE_VIEW( widget ), path, NULL, FALSE, 0.5, 0.0 );
                        gtk_tree_path_free( path );
                    }
                }
                g_free( full_path );
            }
            g_dir_close( dir );
        }

        if ( cursor_dirs[i][1] ) {
            g_free( curdir );
        }
    }

    g_hash_table_destroy(themes);

    gtk_tree_sortable_set_sort_func(GTK_TREE_SORTABLE(store), TLIST_THEME_NAME,
                                    tree_sort_cmp_alpha, NULL, NULL);
    gtk_tree_sortable_set_sort_column_id(GTK_TREE_SORTABLE(store),
                                         TLIST_THEME_NAME, GTK_SORT_ASCENDING);
}

static void
theme_list_selection_changed_cb( GtkTreeSelection *selection, Itf *dialog )
{
    GtkTreeIter         iter;
    GtkTreeModel        *theme_model = NULL;

    if ( gtk_tree_selection_get_selected( selection, &theme_model, &iter ) ) {
        gchar           *path = NULL, *theme = NULL;
        GdkPixbuf *pb = NULL;

        gtk_tree_model_get( theme_model, &iter,
                           TLIST_THEME_PATH, &path,
                           TLIST_THEME_NAME, &theme,
                           -1 );

        if ( path ) {
            pb = generate_preview_image( dialog->cursor_preview_list, path );
            g_free( path );
        }

        gtk_image_set_from_pixbuf(GTK_IMAGE(dialog->cursor_preview_list), pb);
        if(pb)
            g_object_unref(G_OBJECT(pb));

        if(theme) {
            McsSetting *setting;

            g_free(cursor_theme);
            cursor_theme = theme;

            mcs_manager_set_string( dialog->mcs_plugin->manager, "Gtk/CursorThemeName", CHANNEL1, cursor_theme );
            mcs_manager_notify( dialog->mcs_plugin->manager, CHANNEL1 );
            mouse_plugin_write_options( dialog->mcs_plugin );

            cursor_theme_set(cursor_theme, cursor_size);

            setting = mcs_manager_setting_lookup(dialog->mcs_plugin->manager, "Cursor/ShowApplyWarning", CHANNEL2);
            if(!setting || setting->data.v_int)
                show_cursor_apply_warning_dlg(dialog);
        }
    }
}

static void
cursor_size_changed_cb(GtkWidget *w, gpointer user_data)
{
    Itf *dialog = user_data;
    guint new_size;

    new_size = gtk_spin_button_get_value_as_int( GTK_SPIN_BUTTON( dialog->cursor_size_spinbtn ) );

    if(new_size != cursor_size) {
        cursor_size = new_size;

        mcs_manager_set_int( dialog->mcs_plugin->manager, "Gtk/CursorThemeSize", CHANNEL1, cursor_size );
        mcs_manager_notify( dialog->mcs_plugin->manager, CHANNEL1 );
        mouse_plugin_write_options( dialog->mcs_plugin );

        cursor_theme_set(cursor_theme, cursor_size);
    }
}

static GtkWidget *
theme_list_create()
{
    GtkWidget           *theme_list;
    GtkCellRenderer     *renderer;
    GtkListStore        *store;
    GtkTreeSelection    *selection;

    store = gtk_list_store_new( TLIST_NUM_COLUMNS, G_TYPE_STRING, G_TYPE_STRING );
    theme_list = gtk_tree_view_new_with_model( GTK_TREE_MODEL( store ) );
    g_object_unref( store );

    renderer = gtk_cell_renderer_text_new();
    gtk_tree_view_insert_column_with_attributes( GTK_TREE_VIEW( theme_list ),
                                                 -1,
                                                 _( "Cursor theme" ),
                                                 renderer,
                                                 "text", TLIST_THEME_NAME,
                                                 NULL );

    selection = gtk_tree_view_get_selection( GTK_TREE_VIEW( theme_list ) );
    gtk_tree_selection_set_mode( selection, GTK_SELECTION_SINGLE );
    gtk_tree_view_set_headers_visible( GTK_TREE_VIEW( theme_list ), FALSE );

    return ( theme_list );
}

void
mouse_plugin_set_initial_cursor_values(McsPlugin *mcs_plugin)
{
    McsSetting      *setting;

    setting = mcs_manager_setting_lookup( mcs_plugin->manager, "Gtk/CursorThemeName", CHANNEL1 );
    if ( setting ) {
        cursor_theme = g_strdup( setting->data.v_string );
    }
    else {
        cursor_theme = g_strdup( "default" );
        mcs_manager_set_string( mcs_plugin->manager, "Gtk/CursorThemeName",
                                CHANNEL1, cursor_theme );
    }

    setting = mcs_manager_setting_lookup( mcs_plugin->manager, "Gtk/CursorThemeSize", CHANNEL1 );
    if ( setting ) {
        cursor_size = setting->data.v_int;
    }
    else {
        cursor_size = XcursorGetDefaultSize( GDK_DISPLAY() );
        mcs_manager_set_int( mcs_plugin->manager, "Gtk/CursorThemeSize",
                             CHANNEL1, cursor_size );
    }

    /*
    if ( strcmp( cursor_theme, "default" ) ) {
        cursor_theme_set( cursor_theme, cursor_size );
    }
    */
}

void
mouse_plugin_create_cursor_page(Itf *dialog)
{
    GtkWidget           *hbox, *vbox, *fbox, *frame_bin;
    GtkWidget           *scrolledwindow;
    GtkTreeSelection *sel;
    GtkTreeModel *model = NULL;
    GtkTreeIter itr;

    dialog->cursor_page = gtk_hbox_new( FALSE, 0 );
    gtk_container_set_border_width(GTK_CONTAINER(dialog->cursor_page), BORDER);
    gtk_widget_show( dialog->cursor_page );

    scrolledwindow = gtk_scrolled_window_new( NULL, NULL );
    gtk_widget_show( scrolledwindow );
    gtk_box_pack_start( GTK_BOX( dialog->cursor_page ), scrolledwindow, TRUE, TRUE, 0 );
    gtk_scrolled_window_set_policy( GTK_SCROLLED_WINDOW( scrolledwindow ),
                                    GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC );
    gtk_scrolled_window_set_shadow_type( GTK_SCROLLED_WINDOW( scrolledwindow ), GTK_SHADOW_IN );

    dialog->cursor_theme_list = theme_list_create();
    gtk_widget_show( dialog->cursor_theme_list );
    gtk_container_add( GTK_CONTAINER( scrolledwindow ), dialog->cursor_theme_list );

    vbox = gtk_vbox_new( FALSE, 0 );
    gtk_widget_show( vbox );
    gtk_box_pack_start( GTK_BOX( dialog->cursor_page ), vbox, TRUE, TRUE, 0 );

    fbox = xfce_create_framebox( _( "Cursor Size" ), &frame_bin );
    gtk_widget_show( fbox );
    gtk_box_pack_start( GTK_BOX( vbox ), fbox, FALSE, FALSE, 0 );

    hbox = gtk_hbox_new( FALSE, 0 );
    gtk_widget_show( hbox );
    gtk_container_add( GTK_CONTAINER( frame_bin ), hbox );

    dialog->cursor_size_spinbtn = gtk_spin_button_new_with_range( CURSOR_SIZE_MIN, CURSOR_SIZE_MAX, 1.0 );
    gtk_widget_show( dialog->cursor_size_spinbtn );
    gtk_box_pack_start( GTK_BOX( hbox ), dialog->cursor_size_spinbtn, FALSE, FALSE, 0 );
    gtk_spin_button_set_numeric( GTK_SPIN_BUTTON( dialog->cursor_size_spinbtn ), TRUE );
    gtk_spin_button_set_value( GTK_SPIN_BUTTON( dialog->cursor_size_spinbtn ), cursor_size );
    gtk_spin_button_set_wrap( GTK_SPIN_BUTTON( dialog->cursor_size_spinbtn ), FALSE );
    g_signal_connect(G_OBJECT(dialog->cursor_size_spinbtn), "changed",
                     G_CALLBACK(cursor_size_changed_cb), dialog);

    fbox = xfce_create_framebox( _( "Preview" ), &frame_bin );
    gtk_widget_show( fbox );
    gtk_box_pack_start( GTK_BOX( vbox ), fbox, FALSE, FALSE, 0 );

    hbox = gtk_hbox_new( FALSE, 0 );
    gtk_widget_show( hbox );
    gtk_container_add( GTK_CONTAINER( frame_bin ), hbox );
    gtk_container_set_border_width( GTK_CONTAINER( hbox ), BORDER );

    dialog->cursor_preview_list = preview_list_create();
    gtk_widget_show( dialog->cursor_preview_list );
    gtk_box_pack_start( GTK_BOX( hbox ), dialog->cursor_preview_list, FALSE, FALSE, 0 );

    theme_list_populate( dialog->cursor_theme_list, cursor_theme );
    sel = gtk_tree_view_get_selection(GTK_TREE_VIEW(dialog->cursor_theme_list));
    if(gtk_tree_selection_get_selected(sel, &model, &itr)) {
        gchar *path = NULL;
        gtk_tree_model_get(model, &itr, TLIST_THEME_PATH, &path, -1);
        if(path) {
            GdkPixbuf *pb = generate_preview_image(dialog->mouse_dialog, path);
            if(pb) {
                gtk_image_set_from_pixbuf(GTK_IMAGE(dialog->cursor_preview_list), pb);
                g_object_unref(G_OBJECT(pb));
            }
            g_free(path);
        }
    }
    g_signal_connect( G_OBJECT(sel), "changed",
                      G_CALLBACK( theme_list_selection_changed_cb ), dialog );
}

#endif  /* HAVE_XCURSOR_EXTENSION */


syntax highlighted by Code2HTML, v. 0.9.1