/*
   Editor initialisation and callback handler.

   Copyright (C) 1996-2025
   Free Software Foundation, Inc.

   Written by:
   Paul Sheer, 1996, 1997
   Andrew Borodin <aborodin@vmail.ru> 2012-2024

   This file is part of the Midnight Commander.

   The Midnight Commander 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, either version 3 of the License,
   or (at your option) any later version.

   The Midnight Commander 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 General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/** \file
 *  \brief Source: editor initialisation and callback handler
 *  \author Paul Sheer
 *  \date 1996, 1997
 */

#include <config.h>

#include <ctype.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>

#include "lib/global.h"

#include "lib/tty/tty.h"        /* LINES, COLS */
#include "lib/tty/key.h"        /* is_idle(), bracketed_pasting_in_progress */
#include "lib/tty/color.h"      /* tty_setcolor() */
#include "lib/skin.h"
#include "lib/fileloc.h"        /* EDIT_HOME_DIR */
#include "lib/strutil.h"        /* str_term_trim() */
#include "lib/util.h"           /* mc_build_filename() */
#include "lib/widget.h"
#include "lib/mcconfig.h"
#include "lib/event.h"          /* mc_event_raise() */
#ifdef HAVE_CHARSET
#include "lib/charsets.h"
#endif

#include "src/keymap.h"         /* keybind_lookup_keymap_command() */
#include "src/setup.h"          /* home_dir */
#include "src/execute.h"        /* toggle_subshell()  */
#include "src/filemanager/cmd.h"        /* save_setup_cmd()  */
#include "src/learn.h"          /* learn_keys() */

#include "edit-impl.h"
#include "editwidget.h"
#include "editmacros.h"         /* edit_execute_macro() */
#ifdef HAVE_ASPELL
#include "spell.h"
#endif

/*** global variables ****************************************************************************/

char *edit_window_state_char = NULL;
char *edit_window_close_char = NULL;

/*** file scope macro definitions ****************************************************************/

#define WINDOW_MIN_LINES (2 + 2)
#define WINDOW_MIN_COLS (2 + LINE_STATE_WIDTH + 2)

/*** file scope type declarations ****************************************************************/

/*** forward declarations (file scope functions) *************************************************/

/*** file scope variables ************************************************************************/

static unsigned int edit_dlg_init_refcounter = 0;

/* --------------------------------------------------------------------------------------------- */
/*** file scope functions ************************************************************************/
/* --------------------------------------------------------------------------------------------- */
/**
 * Init the 'edit' subsystem
 */

static void
edit_dlg_init (void)
{
    edit_dlg_init_refcounter++;

    if (edit_dlg_init_refcounter == 1)
    {
        edit_window_state_char = mc_skin_get ("widget-editor", "window-state-char", "*");
        edit_window_close_char = mc_skin_get ("widget-editor", "window-close-char", "X");

#ifdef HAVE_ASPELL
        aspell_init ();
#endif
    }
}

/* --------------------------------------------------------------------------------------------- */
/**
 * Deinit the 'edit' subsystem
 */

static void
edit_dlg_deinit (void)
{
    if (edit_dlg_init_refcounter == 1)
    {
        g_free (edit_window_state_char);
        g_free (edit_window_close_char);

#ifdef HAVE_ASPELL
        aspell_clean ();
#endif
    }

    if (edit_dlg_init_refcounter != 0)
        edit_dlg_init_refcounter--;
}

/* --------------------------------------------------------------------------------------------- */
/**
 * Show info about editor
 */

static void
edit_about (void)
{
    char *ver;

    ver = g_strdup_printf ("MCEdit %s", mc_global.mc_version);

    {
#if defined(WIN32)  //WIN32, quick
        quick_widget_t quick_widgets[9] = {0},
            *qc = quick_widgets;
#else
        quick_widget_t quick_widgets[] = {
            /* *INDENT-OFF* */
            QUICK_LABEL (ver, NULL),
            QUICK_SEPARATOR (TRUE),
            QUICK_LABEL (N_("A user friendly text editor\n"
                            "written for the Midnight Commander."), NULL),
            QUICK_SEPARATOR (FALSE),
            QUICK_LABEL (N_("Copyright (C) 1996-2025 the Free Software Foundation"), NULL),
            QUICK_START_BUTTONS (TRUE, TRUE),
            QUICK_BUTTON (N_("&OK"), B_ENTER, NULL, NULL),
            QUICK_END
            /* *INDENT-ON* */
        };
#endif  //WIN32

        WRect r = { -1, -1, 0, 40 };

        quick_dialog_t qdlg = QUICK_DIALOG_INIT (
            &r, N_("About"), "[Internal File Editor]",
            quick_widgets, NULL, NULL
        );

#if defined(WIN32)  //WIN32, quick
        /* *INDENT-OFF* */
        qc = XQUICK_LABEL (qc, ver, NULL),
        qc = XQUICK_SEPARATOR (qc, TRUE),
        qc = XQUICK_LABEL (qc, N_("A user friendly text editor\n"
                                  "written for the Midnight Commander."), NULL),
        qc = XQUICK_SEPARATOR (qc, FALSE),
        qc = XQUICK_LABEL (qc, N_("Copyright (C) 1996-2025 the Free Software Foundation"), NULL),
        qc = XQUICK_LABEL (qc, N_("         Copyright (C) 2012-2025 Adam Young"), NULL),
        qc = XQUICK_START_BUTTONS (qc, TRUE, TRUE),
        qc = XQUICK_BUTTON (qc, N_("&OK"), B_ENTER, NULL, NULL),
        qc = XQUICK_END (qc);
        /* *INDENT-ON* */
        assert(qc == (quick_widgets + (sizeof(quick_widgets)/sizeof(quick_widgets[0]))));
#endif  //WIN32

        quick_widgets[0].pos_flags = WPOS_KEEP_TOP | WPOS_CENTER_HORZ;
        quick_widgets[2].pos_flags = WPOS_KEEP_TOP | WPOS_CENTER_HORZ;
        quick_widgets[4].pos_flags = WPOS_KEEP_TOP | WPOS_CENTER_HORZ;

        (void) quick_dialog (&qdlg);
    }

    g_free (ver);
}

/* --------------------------------------------------------------------------------------------- */
/**
 * Show a help window
 */

static void
edit_help (const WDialog *h)
{
    ev_help_t event_data = { NULL, h->help_ctx };

    mc_event_raise (MCEVENT_GROUP_CORE, "help", &event_data);
}

/* --------------------------------------------------------------------------------------------- */
/**
 * Restore saved window size.
 *
 * @param edit editor object
 */

static void
edit_restore_size (WEdit *edit)
{
    Widget *w = WIDGET (edit);

    edit->drag_state = MCEDIT_DRAG_NONE;
    w->mouse.forced_capture = FALSE;
    widget_set_size_rect (w, &edit->loc_prev);
    widget_draw (WIDGET (w->owner));
}

/* --------------------------------------------------------------------------------------------- */
/**
 * Move window by one row or column in any direction.
 *
 * @param edit    editor object
 * @param command direction (CK_Up, CK_Down, CK_Left, CK_Right)
 */

static void
edit_window_move (WEdit *edit, long command)
{
    Widget *we = WIDGET (edit);
    Widget *wo = WIDGET (we->owner);
    WRect *w = &we->rect;
    const WRect *wh = &wo->rect;

    switch (command)
    {
    case CK_Up:
        if (w->y > wh->y + 1)   /* menubar */
            w->y--;
        break;
    case CK_Down:
        if (w->y < wh->y + wh->lines - 2)       /* buttonbar */
            w->y++;
        break;
    case CK_Left:
        if (w->x + wh->cols > wh->x)
            w->x--;
        break;
    case CK_Right:
        if (w->x < wh->x + wh->cols)
            w->x++;
        break;
    default:
        return;
    }

    edit->force |= REDRAW_PAGE;
    widget_draw (wo);
}

/* --------------------------------------------------------------------------------------------- */
/**
 * Resize window by one row or column in any direction.
 *
 * @param edit    editor object
 * @param command direction (CK_Up, CK_Down, CK_Left, CK_Right)
 */

static void
edit_window_resize (WEdit *edit, long command)
{
    Widget *we = WIDGET (edit);
    Widget *wo = WIDGET (we->owner);
    WRect *w = &we->rect;
    const WRect *wh = &wo->rect;

    switch (command)
    {
    case CK_Up:
        if (w->lines > WINDOW_MIN_LINES)
            w->lines--;
        break;
    case CK_Down:
        if (w->y + w->lines < wh->y + wh->lines - 1)    /* buttonbar */
            w->lines++;
        break;
    case CK_Left:
        if (w->cols > WINDOW_MIN_COLS)
            w->cols--;
        break;
    case CK_Right:
        if (w->x + w->cols < wh->x + wh->cols)
            w->cols++;
        break;
    default:
        return;
    }

    edit->force |= REDRAW_COMPLETELY;
    widget_draw (wo);
}

/* --------------------------------------------------------------------------------------------- */
/**
 * Get hotkey by number.
 *
 * @param n number
 * @return hotkey
 */

static unsigned char
get_hotkey (int n)
{
    return (n <= 9) ? '0' + n : 'a' + n - 10;
}

/* --------------------------------------------------------------------------------------------- */

static void
edit_window_list (const WDialog *h)
{
    const WGroup *g = CONST_GROUP (h);
    const size_t offset = 2;    /* skip menu and buttonbar */
    const size_t dlg_num = g_list_length (g->widgets) - offset;
    int lines, cols;
    Listbox *listbox;
    GList *w;
    WEdit *selected;
    int i = 0;

    lines = MIN ((size_t) (LINES * 2 / 3), dlg_num);
    cols = COLS * 2 / 3;

    listbox = listbox_window_new (lines, cols, _("Open files"), "[Open files]");

    for (w = g->widgets; w != NULL; w = g_list_next (w))
        if (edit_widget_is_editor (CONST_WIDGET (w->data)))
        {
            WEdit *e = EDIT (w->data);
            char *fname;

            if (e->filename_vpath == NULL)
                fname = g_strdup_printf ("%c [%s]", e->modified != 0 ? '*' : ' ', _("NoName"));
            else
                fname =
                    g_strdup_printf ("%c%s", e->modified != 0 ? '*' : ' ',
                                     vfs_path_as_str (e->filename_vpath));

            listbox_add_item (listbox->list, LISTBOX_APPEND_AT_END, get_hotkey (i++),
                              str_term_trim (fname, WIDGET (listbox->list)->rect.cols - 2), e,
                              FALSE);
            g_free (fname);
        }

    selected = listbox_run_with_data (listbox, g->current->data);
    if (selected != NULL)
        widget_select (WIDGET (selected));
}

/* --------------------------------------------------------------------------------------------- */

static char *
edit_get_shortcut (long command)
{
    const char *ext_map;
    const char *shortcut = NULL;

    shortcut = keybind_lookup_keymap_shortcut (editor_map, command);
    if (shortcut != NULL)
        return g_strdup (shortcut);

    ext_map = keybind_lookup_keymap_shortcut (editor_map, CK_ExtendedKeyMap);
    if (ext_map != NULL)
        shortcut = keybind_lookup_keymap_shortcut (editor_x_map, command);
    if (shortcut != NULL)
        return g_strdup_printf ("%s %s", ext_map, shortcut);

    return NULL;
}

/* --------------------------------------------------------------------------------------------- */

static char *
edit_get_title (const WDialog *h, size_t len)
{
    const WEdit *edit;
    const char *modified;
    const char *file_label;
    char *filename;

    edit = edit_find_editor (h);
    modified = edit->modified != 0 ? "(*) " : "    ";

    len -= 4;

    if (edit->filename_vpath == NULL)
        filename = g_strdup (_("[NoName]"));
    else
        filename = g_strdup (vfs_path_as_str (edit->filename_vpath));

    file_label = str_term_trim (filename, len - str_term_width1 (_("Edit: ")));
    g_free (filename);

    return g_strconcat (_("Edit: "), modified, file_label, (char *) NULL);
}

/* --------------------------------------------------------------------------------------------- */

static cb_ret_t
edit_dialog_command_execute (WDialog *h, long command)
{
    WGroup *g = GROUP (h);
    cb_ret_t ret = MSG_HANDLED;

    switch (command)
    {
    case CK_EditNew:
        edit_load_file_from_filename (h, NULL);
        break;
    case CK_EditFile:
        edit_load_cmd (h);
        break;
    case CK_History:
        edit_load_file_from_history (h);
        break;
    case CK_EditSyntaxFile:
        edit_load_syntax_file (h);
        break;
    case CK_EditUserMenu:
        edit_load_menu_file (h);
        break;
    case CK_Close:
        /* if there are no opened files anymore, close MC editor */
        if (edit_widget_is_editor (CONST_WIDGET (g->current->data)) &&
            edit_close_cmd (EDIT (g->current->data)) && edit_find_editor (h) == NULL)
            dlg_close (h);
        break;
    case CK_Help:
        edit_help (h);
        break;
    case CK_Menu:
        edit_menu_cmd (h);
        break;
    case CK_Quit:
    case CK_Cancel:
        /* don't close editor due to SIGINT, but stop move/resize window */
        {
            Widget *w = WIDGET (g->current->data);

            if (edit_widget_is_editor (w) && EDIT (w)->drag_state != MCEDIT_DRAG_NONE)
                edit_restore_size (EDIT (w));
            else if (command == CK_Quit)
                dlg_close (h);
        }
        break;
    case CK_About:
        edit_about ();
        break;
    case CK_SyntaxOnOff:
        edit_syntax_onoff_cmd (h);
        break;
    case CK_ShowTabTws:
        edit_show_tabs_tws_cmd (h);
        break;
    case CK_ShowMargin:
        edit_show_margin_cmd (h);
        break;
    case CK_ShowNumbers:
        edit_show_numbers_cmd (h);
        break;
    case CK_Refresh:
        edit_refresh_cmd ();
        break;
    case CK_Shell:
        toggle_subshell ();
        break;
    case CK_LearnKeys:
        learn_keys ();
        break;
    case CK_WindowMove:
    case CK_WindowResize:
        if (edit_widget_is_editor (CONST_WIDGET (g->current->data)))
            edit_handle_move_resize (EDIT (g->current->data), command);
        break;
    case CK_WindowList:
        edit_window_list (h);
        break;
    case CK_WindowNext:
        group_select_next_widget (g);
        break;
    case CK_WindowPrev:
        group_select_prev_widget (g);
        break;
    case CK_Options:
        edit_options_dialog (h);
        break;
    case CK_OptionsSaveMode:
        edit_save_mode_cmd ();
        break;
    case CK_SaveSetup:
        save_setup_cmd ();
        break;
    default:
        ret = MSG_NOT_HANDLED;
        break;
    }

    return ret;
}

/* --------------------------------------------------------------------------------------------- */
/*
 * Translate the keycode into either 'command' or 'char_for_insertion'.
 * 'command' is one of the editor commands from lib/keybind.h.
 */

static gboolean
edit_translate_key (WEdit *edit, long x_key, int *cmd, int *ch)
{
    Widget *w = WIDGET (edit);
    long command = CK_InsertChar;
    int char_for_insertion = -1;

    /* an ordinary insertable character */
    if (!w->ext_mode && x_key < 256)
    {
#ifndef HAVE_CHARSET
        if (is_printable (x_key))
        {
            char_for_insertion = x_key;
            goto fin;
        }
#else
        int c;

        if (edit->charpoint >= MB_LEN_MAX)
        {
            edit->charpoint = 0;
            edit->charbuf[edit->charpoint] = '\0';
        }
        if (edit->charpoint < MB_LEN_MAX)
        {
            edit->charbuf[edit->charpoint++] = x_key;
            edit->charbuf[edit->charpoint] = '\0';
        }

        /* input from 8-bit locale */
        if (!mc_global.utf8_display)
        {
            /* source is in 8-bit codeset */
            c = convert_from_input_c (x_key);

            if (is_printable (c))
            {
                if (!edit->utf8)
                    char_for_insertion = c;
                else
                    char_for_insertion = convert_from_8bit_to_utf_c2 ((char) x_key);
                goto fin;
            }
        }
        else
        {
            /* UTF-8 locale */
            int res;

            res = str_is_valid_char (edit->charbuf, edit->charpoint);
            if (res < 0 && res != -2)
            {
                edit->charpoint = 0;    /* broken multibyte char, skip */
                goto fin;
            }

            if (edit->utf8)
            {
                /* source is in UTF-8 codeset */
                if (res < 0)
                {
                    char_for_insertion = x_key;
                    goto fin;
                }

                edit->charbuf[edit->charpoint] = '\0';
                edit->charpoint = 0;
                if (g_unichar_isprint (g_utf8_get_char (edit->charbuf)))
                {
                    char_for_insertion = x_key;
                    goto fin;
                }
            }
            else
            {
                /* 8-bit source */
                if (res < 0)
                {
                    /* not finished multibyte input (we're in the middle of multibyte utf-8 char) */
                    goto fin;
                }

                if (g_unichar_isprint (g_utf8_get_char (edit->charbuf)))
                {
                    c = convert_from_utf_to_current (edit->charbuf);
                    edit->charbuf[0] = '\0';
                    edit->charpoint = 0;
                    char_for_insertion = c;
                    goto fin;
                }

                /* non-printable utf-8 input, skip it */
                edit->charbuf[0] = '\0';
                edit->charpoint = 0;
            }
        }
#endif /* HAVE_CHARSET */
    }

    /* Commands specific to the key emulation */
    command = widget_lookup_key (w, x_key);
    if (command == CK_IgnoreKey)
        command = CK_InsertChar;

  fin:
    *cmd = (int) command;       /* FIXME */
    *ch = char_for_insertion;

    return !(command == CK_InsertChar && char_for_insertion == -1);
}


/* --------------------------------------------------------------------------------------------- */

static inline void
edit_quit (WDialog *h)
{
    GList *l;
    WEdit *e = NULL;
    GSList *m = NULL;
    GSList *me;

    /* don't stop the dialog before final decision */
    widget_set_state (WIDGET (h), WST_ACTIVE, TRUE);

    /* check window state and get modified files */
    for (l = GROUP (h)->widgets; l != NULL; l = g_list_next (l))
        if (edit_widget_is_editor (CONST_WIDGET (l->data)))
        {
            e = EDIT (l->data);

            if (e->drag_state != MCEDIT_DRAG_NONE)
            {
                edit_restore_size (e);
                g_slist_free (m);
                return;
            }

            /* create separate list because widget_select()
               changes the window position in Z order */
            if (e->modified != 0)
                m = g_slist_prepend (m, l->data);
        }

    for (me = m; me != NULL; me = g_slist_next (me))
    {
        e = EDIT (me->data);

        widget_select (WIDGET (e));

        if (!edit_ok_to_exit (e))
            break;
    }

    /* if all files were checked, quit editor */
    if (me == NULL)
        dlg_close (h);

    g_slist_free (m);
}

/* --------------------------------------------------------------------------------------------- */

static inline void
edit_set_buttonbar (WEdit *edit, WButtonBar *bb)
{
    Widget *w = WIDGET (edit);

    buttonbar_set_label (bb, 1, Q_ ("ButtonBar|Help"), w->keymap, NULL);
    buttonbar_set_label (bb, 2, Q_ ("ButtonBar|Save"), w->keymap, w);
    buttonbar_set_label (bb, 3, Q_ ("ButtonBar|Mark"), w->keymap, w);
    buttonbar_set_label (bb, 4, Q_ ("ButtonBar|Replac"), w->keymap, w);
    buttonbar_set_label (bb, 5, Q_ ("ButtonBar|Copy"), w->keymap, w);
    buttonbar_set_label (bb, 6, Q_ ("ButtonBar|Move"), w->keymap, w);
    buttonbar_set_label (bb, 7, Q_ ("ButtonBar|Search"), w->keymap, w);
    buttonbar_set_label (bb, 8, Q_ ("ButtonBar|Delete"), w->keymap, w);
    buttonbar_set_label (bb, 9, Q_ ("ButtonBar|PullDn"), w->keymap, NULL);
    buttonbar_set_label (bb, 10, Q_ ("ButtonBar|Quit"), w->keymap, NULL);
}

/* --------------------------------------------------------------------------------------------- */

static void
edit_total_update (WEdit *edit)
{
    edit_find_bracket (edit);
    edit->force |= REDRAW_COMPLETELY;
    edit_update_curs_row (edit);
    edit_update_screen (edit);
}

/* --------------------------------------------------------------------------------------------- */

static gboolean
edit_update_cursor (WEdit *edit, const mouse_event_t *event)
{
    int x, y;
    gboolean done;

    x = event->x - (edit->fullscreen != 0 ? 0 : 1);
    y = event->y - (edit->fullscreen != 0 ? 0 : 1);

    if (edit->mark2 != -1 && event->msg == MSG_MOUSE_UP)
        return TRUE;            /* don't do anything */

    if (event->msg == MSG_MOUSE_DOWN || event->msg == MSG_MOUSE_UP)
        edit_push_key_press (edit);

    if (!edit_options.cursor_beyond_eol)
        edit->prev_col = x - edit->start_col - edit_options.line_state_width;
    else
    {
        long line_len;

        line_len =
            edit_move_forward3 (edit, edit_buffer_get_current_bol (&edit->buffer), 0,
                                edit_buffer_get_current_eol (&edit->buffer));

        if (x > line_len - 1)
        {
            edit->over_col = x - line_len - edit->start_col - edit_options.line_state_width;
            edit->prev_col = line_len;
        }
        else
        {
            edit->over_col = 0;
            edit->prev_col = x - edit_options.line_state_width - edit->start_col;
        }
    }

    if (y > edit->curs_row)
        edit_move_down (edit, y - edit->curs_row, FALSE);
    else if (y < edit->curs_row)
        edit_move_up (edit, edit->curs_row - y, FALSE);
    else
        edit_move_to_prev_col (edit, edit_buffer_get_current_bol (&edit->buffer));

    if (event->msg == MSG_MOUSE_CLICK)
    {
        edit_mark_cmd (edit, TRUE);     /* reset */
        edit->highlight = 0;
    }

    done = (event->msg != MSG_MOUSE_DRAG);
    if (done)
        edit_mark_cmd (edit, FALSE);

    return done;
}

/* --------------------------------------------------------------------------------------------- */
/** Callback for the edit dialog */

static cb_ret_t
edit_dialog_callback (Widget *w, Widget *sender, widget_msg_t msg, int parm, void *data)
{
    WGroup *g = GROUP (w);
    WDialog *h = DIALOG (w);

    switch (msg)
    {
    case MSG_INIT:
        edit_dlg_init ();
        return MSG_HANDLED;

    case MSG_RESIZE:
        dlg_default_callback (w, NULL, MSG_RESIZE, 0, NULL);
        menubar_arrange (menubar_find (h));
        return MSG_HANDLED;

    case MSG_ACTION:
        {
            /* Handle shortcuts, menu, and buttonbar. */

            cb_ret_t result;

            result = edit_dialog_command_execute (h, parm);

            /* We forward any commands coming from the menu, and which haven't been
               handled by the dialog, to the focused WEdit window. */
            if (result == MSG_NOT_HANDLED && sender == WIDGET (menubar_find (h)))
                result = send_message (g->current->data, NULL, MSG_ACTION, parm, NULL);

            return result;
        }

    case MSG_KEY:
        {
            Widget *we = WIDGET (g->current->data);
            cb_ret_t ret = MSG_NOT_HANDLED;

            if (edit_widget_is_editor (we))
            {
                gboolean ext_mode;
                long command;

                /* keep and then extmod flag */
                ext_mode = we->ext_mode;
                command = widget_lookup_key (we, parm);
                we->ext_mode = ext_mode;

                if (command == CK_IgnoreKey)
                    we->ext_mode = FALSE;
                else
                {
                    ret = edit_dialog_command_execute (h, command);
                    /* if command was not handled, keep the extended mode
                       for the further key processing */
                    if (ret == MSG_HANDLED)
                        we->ext_mode = FALSE;
                }
            }

            /*
             * Due to the "end of bracket" escape the editor sees input with is_idle() == false
             * (expects more characters) and hence doesn't yet refresh the screen, but then
             * no further characters arrive (there's only an "end of bracket" which is swallowed
             * by tty_get_event()), so you end up with a screen that's not refreshed after pasting.
             * So let's trigger an IDLE signal.
             */
            if (!is_idle ())
                widget_idle (w, TRUE);
            return ret;
        }

        /* hardcoded menu hotkeys (see edit_drop_hotkey_menu) */
    case MSG_UNHANDLED_KEY:
        return edit_drop_hotkey_menu (h, parm) ? MSG_HANDLED : MSG_NOT_HANDLED;

    case MSG_VALIDATE:
        edit_quit (h);
        return MSG_HANDLED;

    case MSG_DESTROY:
        edit_dlg_deinit ();
        return MSG_HANDLED;

    case MSG_IDLE:
        widget_idle (w, FALSE);
        return send_message (g->current->data, NULL, MSG_IDLE, 0, NULL);

    default:
        return dlg_default_callback (w, sender, msg, parm, data);
    }
}

/* --------------------------------------------------------------------------------------------- */

/**
 * Handle mouse events of editor screen.
 *
 * @param w Widget object (the editor)
 * @param msg mouse event message
 * @param event mouse event data
 */
static void
edit_dialog_mouse_callback (Widget *w, mouse_msg_t msg, mouse_event_t *event)
{
    gboolean unhandled = TRUE;

    if (msg == MSG_MOUSE_DOWN && event->y == 0)
    {
        WGroup *g = GROUP (w);
        WDialog *h = DIALOG (w);
        WMenuBar *b;

        b = menubar_find (h);

        if (!widget_get_state (WIDGET (b), WST_FOCUSED))
        {
            /* menubar */

            GList *l;
            GList *top = NULL;
            int x;

            /* Try find top fullscreen window */
            for (l = g->widgets; l != NULL; l = g_list_next (l))
                if (edit_widget_is_editor (CONST_WIDGET (l->data))
                    && EDIT (l->data)->fullscreen != 0)
                    top = l;

            /* Handle fullscreen/close buttons in the top line */
            x = w->rect.cols - 6;

            if (top != NULL && event->x >= x)
            {
                WEdit *e = EDIT (top->data);

                if (top != g->current)
                {
                    /* Window is not active. Activate it */
                    widget_select (WIDGET (e));
                }

                /* Handle buttons */
                if (event->x - x <= 2)
                    edit_toggle_fullscreen (e);
                else
                    send_message (h, NULL, MSG_ACTION, CK_Close, NULL);

                unhandled = FALSE;
            }

            if (unhandled)
                menubar_activate (b, drop_menus, -1);
        }
    }

    /* Continue handling of unhandled event in window or menu */
    event->result.abort = unhandled;
}

/* --------------------------------------------------------------------------------------------- */

static cb_ret_t
edit_dialog_bg_callback (Widget *w, Widget *sender, widget_msg_t msg, int parm, void *data)
{
    switch (msg)
    {
    case MSG_INIT:
        w->rect = WIDGET (w->owner)->rect;
        rect_grow (&w->rect, -1, 0);
        w->pos_flags |= WPOS_KEEP_ALL;
        return MSG_HANDLED;

    default:
        return background_callback (w, sender, msg, parm, data);
    }
}

/* --------------------------------------------------------------------------------------------- */

static cb_ret_t
edit_callback (Widget *w, Widget *sender, widget_msg_t msg, int parm, void *data)
{
    WEdit *e = EDIT (w);

    switch (msg)
    {
    case MSG_FOCUS:
        edit_set_buttonbar (e, buttonbar_find (DIALOG (w->owner)));
        return MSG_HANDLED;

    case MSG_DRAW:
        e->force |= REDRAW_COMPLETELY;
        edit_update_screen (e);
        return MSG_HANDLED;

    case MSG_KEY:
        {
            int cmd, ch;
            cb_ret_t ret = MSG_NOT_HANDLED;

            /* The user may override the access-keys for the menu bar. */
            if (macro_index == -1 && !bracketed_pasting_in_progress && edit_execute_macro (e, parm))
            {
                edit_update_screen (e);
                ret = MSG_HANDLED;
            }
            else if (edit_translate_key (e, parm, &cmd, &ch))
            {
                edit_execute_key_command (e, cmd, ch);
                edit_update_screen (e);
                ret = MSG_HANDLED;
            }

            return ret;
        }

    case MSG_ACTION:
        /* command from menubar or buttonbar */
        edit_execute_key_command (e, parm, -1);
        edit_update_screen (e);
        return MSG_HANDLED;

    case MSG_CURSOR:
        {
            int y, x;

            y = (e->fullscreen != 0 ? 0 : 1) + EDIT_TEXT_VERTICAL_OFFSET + e->curs_row;
            x = (e->fullscreen != 0 ? 0 : 1) + EDIT_TEXT_HORIZONTAL_OFFSET +
                edit_options.line_state_width + e->curs_col + e->start_col + e->over_col;

            widget_gotoyx (w, y, x);
            return MSG_HANDLED;
        }

    case MSG_IDLE:
        edit_update_screen (e);
        return MSG_HANDLED;

    case MSG_DESTROY:
        edit_clean (e);
        return MSG_HANDLED;

    default:
        return widget_default_callback (w, sender, msg, parm, data);
    }
}

/* --------------------------------------------------------------------------------------------- */

/**
 * Handle move/resize mouse events.
 */
static void
edit_mouse_handle_move_resize (Widget *w, mouse_msg_t msg, mouse_event_t *event)
{
    WEdit *edit = EDIT (w);
    WRect *r = &w->rect;
    const WRect *h = &CONST_WIDGET (w->owner)->rect;
    int global_x, global_y;

    if (msg == MSG_MOUSE_UP)
    {
        /* Exit move/resize mode. */
        edit_execute_cmd (edit, CK_Enter, -1);
        edit_update_screen (edit);      /* Paint the buttonbar over our possibly overlapping frame. */
        return;
    }

    if (msg != MSG_MOUSE_DRAG)
        /**
         * We ignore any other events. Specifically, MSG_MOUSE_DOWN.
         *
         * When the move/resize is initiated by the menu, we let the user
         * stop it by clicking with the mouse. Which is why we don't want
         * a mouse down to affect the window.
         */
        return;

    /* Convert point to global coordinates for easier calculations. */
    global_x = event->x + r->x;
    global_y = event->y + r->y;

    /* Clamp the point to the dialog's client area. */
    global_y = CLAMP (global_y, h->y + 1, h->y + h->lines - 2); /* Status line, buttonbar */
    global_x = CLAMP (global_x, h->x, h->x + h->cols - 1);      /* Currently a no-op, as the dialog has no left/right margins. */

    if (edit->drag_state == MCEDIT_DRAG_MOVE)
    {
        r->y = global_y;
        r->x = global_x - edit->drag_state_start;
    }
    else if (edit->drag_state == MCEDIT_DRAG_RESIZE)
    {
        r->lines = MAX (WINDOW_MIN_LINES, global_y - r->y + 1);
        r->cols = MAX (WINDOW_MIN_COLS, global_x - r->x + 1);
    }

    edit->force |= REDRAW_COMPLETELY;   /* Not really needed as WEdit's MSG_DRAW already does this. */

    /* We draw the whole dialog because dragging/resizing exposes area beneath. */
    widget_draw (WIDGET (w->owner));
}

/* --------------------------------------------------------------------------------------------- */

/**
 * Handle mouse events of editor window
 *
 * @param w Widget object (the editor window)
 * @param msg mouse event message
 * @param event mouse event data
 */
static void
edit_mouse_callback (Widget *w, mouse_msg_t msg, mouse_event_t *event)
{
    WEdit *edit = EDIT (w);
    /* buttons' distance from right edge */
    int dx = edit->fullscreen != 0 ? 0 : 2;
    /* location of 'Close' and 'Toggle fullscreen' pictograms */
    int close_x, toggle_fullscreen_x;

    close_x = (w->rect.cols - 1) - dx - 1;
    toggle_fullscreen_x = close_x - 3;

    if (edit->drag_state != MCEDIT_DRAG_NONE)
    {
        /* window is being resized/moved */
        edit_mouse_handle_move_resize (w, msg, event);
        return;
    }

    /* If it's the last line on the screen, we abort the event to make the
     * system channel it to the overlapping buttonbar instead. We have to do
     * this because a WEdit has the WOP_TOP_SELECT flag, which makes it above
     * the buttonbar in Z-order. */
    if (msg == MSG_MOUSE_DOWN && (event->y + w->rect.y == LINES - 1))
    {
        event->result.abort = TRUE;
        return;
    }

    switch (msg)
    {
    case MSG_MOUSE_DOWN:
        widget_select (w);
        edit_update_curs_row (edit);
        edit_update_curs_col (edit);

        if (edit->fullscreen == 0)
        {
            if (event->y == 0)
            {
                if (event->x >= close_x - 1 && event->x <= close_x + 1)
                    ;           /* do nothing (see MSG_MOUSE_CLICK) */
                else if (event->x >= toggle_fullscreen_x - 1 && event->x <= toggle_fullscreen_x + 1)
                    ;           /* do nothing (see MSG_MOUSE_CLICK) */
                else
                {
                    /* start window move */
                    edit_execute_cmd (edit, CK_WindowMove, -1);
                    edit_update_screen (edit);  /* Paint the buttonbar over our possibly overlapping frame. */
                    edit->drag_state_start = event->x;
                }
                break;
            }

            if (event->y == w->rect.lines - 1 && event->x == w->rect.cols - 1)
            {
                /* bottom-right corner -- start window resize */
                edit_execute_cmd (edit, CK_WindowResize, -1);
                break;
            }
        }

        MC_FALLTHROUGH;         /* to start/stop text selection */

    case MSG_MOUSE_UP:
        edit_update_cursor (edit, event);
        edit_total_update (edit);
        break;

    case MSG_MOUSE_CLICK:
        if (event->y == 0)
        {
            if (event->x >= close_x - 1 && event->x <= close_x + 1)
                send_message (w->owner, NULL, MSG_ACTION, CK_Close, NULL);
            else if (event->x >= toggle_fullscreen_x - 1 && event->x <= toggle_fullscreen_x + 1)
                edit_toggle_fullscreen (edit);
            else if (edit->fullscreen == 0 && event->count == GPM_DOUBLE)
                /* double click on top line (toggle fullscreen) */
                edit_toggle_fullscreen (edit);
        }
        else if (event->count == GPM_DOUBLE)
        {
            /* double click */
            edit_mark_current_word_cmd (edit);
            edit_total_update (edit);
        }
        else if (event->count == GPM_TRIPLE)
        {
            /* triple click: works in GPM only, not in xterm */
            edit_mark_current_line_cmd (edit);
            edit_total_update (edit);
        }
        break;

    case MSG_MOUSE_DRAG:
        edit_update_cursor (edit, event);
        edit_total_update (edit);
        break;

    case MSG_MOUSE_SCROLL_UP:
        edit_move_up (edit, 2, TRUE);
        edit_total_update (edit);
        break;

    case MSG_MOUSE_SCROLL_DOWN:
        edit_move_down (edit, 2, TRUE);
        edit_total_update (edit);
        break;

    default:
        break;
    }
}

/* --------------------------------------------------------------------------------------------- */
/*** public functions ****************************************************************************/
/* --------------------------------------------------------------------------------------------- */
/**
 * Edit one file.
 *
 * @param file_vpath file object
 * @param line       line number
 * @return TRUE if no errors was occurred, FALSE otherwise
 */

gboolean
edit_file (const edit_arg_t *arg)
{
    GList *files;
    gboolean ok;

    files = g_list_prepend (NULL, (edit_arg_t *) arg);
    ok = edit_files (files);
    g_list_free (files);

    return ok;
}

/* --------------------------------------------------------------------------------------------- */

gboolean
edit_files (const GList *files)
{
    static gboolean made_directory = FALSE;
    WDialog *edit_dlg;
    WGroup *g;
    WMenuBar *menubar;
    Widget *w, *wd;
    const GList *file;
    gboolean ok = FALSE;

    if (!made_directory)
    {
        char *dir;

        dir = mc_build_filename (mc_config_get_cache_path (), EDIT_HOME_DIR, (char *) NULL);
        made_directory = (mkdir (dir, 0700) != -1 || errno == EEXIST);
        g_free (dir);

        dir = mc_build_filename (mc_config_get_path (), EDIT_HOME_DIR, (char *) NULL);
        made_directory = (mkdir (dir, 0700) != -1 || errno == EEXIST);
        g_free (dir);

        dir = mc_build_filename (mc_config_get_data_path (), EDIT_HOME_DIR, (char *) NULL);
        made_directory = (mkdir (dir, 0700) != -1 || errno == EEXIST);
        g_free (dir);
    }

    /* Create a new dialog and add it widgets to it */
    edit_dlg =
        dlg_create (FALSE, 0, 0, 1, 1, WPOS_FULLSCREEN, FALSE, NULL, edit_dialog_callback,
                    edit_dialog_mouse_callback, "[Internal File Editor]", NULL);
    wd = WIDGET (edit_dlg);
    widget_want_tab (wd, TRUE);
    wd->keymap = editor_map;
    wd->ext_keymap = editor_x_map;

    edit_dlg->get_shortcut = edit_get_shortcut;
    edit_dlg->get_title = edit_get_title;

    g = GROUP (edit_dlg);

    edit_dlg->bg =
        WIDGET (background_new
                (1, 0, wd->rect.lines - 2, wd->rect.cols, EDITOR_BACKGROUND, ' ',
                 edit_dialog_bg_callback));
    group_add_widget (g, edit_dlg->bg);

    menubar = menubar_new (NULL);
    w = WIDGET (menubar);
    group_add_widget_autopos (g, w, w->pos_flags, NULL);
    edit_init_menu (menubar);

    w = WIDGET (buttonbar_new ());
    group_add_widget_autopos (g, w, w->pos_flags, NULL);

    for (file = files; file != NULL; file = g_list_next (file))
    {
        gboolean f_ok;

        f_ok = edit_load_file_from_filename (edit_dlg, (const edit_arg_t *) file->data);
        /* at least one file has been opened succefully */
        ok = ok || f_ok;
    }

    if (ok)
        dlg_run (edit_dlg);

    if (!ok || widget_get_state (wd, WST_CLOSED))
        widget_destroy (wd);

    return ok;
}

/* --------------------------------------------------------------------------------------------- */

WEdit *
edit_find_editor (const WDialog *h)
{
    const WGroup *g = CONST_GROUP (h);

    if (edit_widget_is_editor (CONST_WIDGET (g->current->data)))
        return EDIT (g->current->data);
    return EDIT (widget_find_by_type (CONST_WIDGET (h), edit_callback));
}

/* --------------------------------------------------------------------------------------------- */
/**
 * Check if widget is an WEdit class.
 *
 * @param w probably editor object
 * @return TRUE if widget is an WEdit class, FALSE otherwise
 */

gboolean
edit_widget_is_editor (const Widget *w)
{
    return (w != NULL && w->callback == edit_callback);
}

/* --------------------------------------------------------------------------------------------- */

void
edit_update_screen (WEdit *e)
{
    edit_scroll_screen_over_cursor (e);
    edit_update_curs_col (e);
    edit_status (e, widget_get_state (WIDGET (e), WST_FOCUSED));

    /* pop all events for this window for internal handling */
    if (!is_idle ())
        e->force |= REDRAW_PAGE;
    else
    {
        if ((e->force & REDRAW_COMPLETELY) != 0)
            e->force |= REDRAW_PAGE;
        edit_render_keypress (e);
    }

    widget_draw (WIDGET (buttonbar_find (DIALOG (WIDGET (e)->owner))));
}

/* --------------------------------------------------------------------------------------------- */
/**
 * Save current window size.
 *
 * @param edit editor object
 */

void
edit_save_size (WEdit *edit)
{
    edit->loc_prev = WIDGET (edit)->rect;
}

/* --------------------------------------------------------------------------------------------- */
/**
 * Create new editor window and insert it into editor screen.
 *
 * @param h     editor dialog (screen)
 * @param y     y coordinate
 * @param x     x coordinate
 * @param lines window height
 * @param cols  window width
 * @param f     file object
 * @param fline line number in file
 * @return TRUE if new window was successfully created and inserted into editor screen,
 *         FALSE otherwise
 */

gboolean
edit_add_window (WDialog *h, const WRect *r, const edit_arg_t *arg)
{
    WEdit *edit;
    Widget *w;

    edit = edit_init (NULL, r, arg);
    if (edit == NULL)
        return FALSE;

    w = WIDGET (edit);
    w->callback = edit_callback;
    w->mouse_callback = edit_mouse_callback;

    group_add_widget_autopos (GROUP (h), w, WPOS_KEEP_ALL, NULL);
    edit_set_buttonbar (edit, buttonbar_find (h));
    widget_draw (WIDGET (h));

    return TRUE;
}

/* --------------------------------------------------------------------------------------------- */
/**
 * Handle move/resize events.
 *
 * @param edit    editor object
 * @param command action id
 * @return TRUE if the action was handled, FALSE otherwise
 */

gboolean
edit_handle_move_resize (WEdit *edit, long command)
{
    Widget *w = WIDGET (edit);
    gboolean ret = FALSE;

    if (edit->fullscreen != 0)
    {
        edit->drag_state = MCEDIT_DRAG_NONE;
        w->mouse.forced_capture = FALSE;
        return ret;
    }

    switch (edit->drag_state)
    {
    case MCEDIT_DRAG_NONE:
        /* possible start move/resize */
        switch (command)
        {
        case CK_WindowMove:
            edit->drag_state = MCEDIT_DRAG_MOVE;
            edit_save_size (edit);
            edit_status (edit, TRUE);   /* redraw frame and status */
            /**
             * If a user initiates a move by the menu, not by the mouse, we
             * make a subsequent mouse drag pull the frame from its middle.
             * (We can instead choose '0' to pull it from the corner.)
             */
            edit->drag_state_start = w->rect.cols / 2;
            ret = TRUE;
            break;
        case CK_WindowResize:
            edit->drag_state = MCEDIT_DRAG_RESIZE;
            edit_save_size (edit);
            edit_status (edit, TRUE);   /* redraw frame and status */
            ret = TRUE;
            break;
        default:
            break;
        }
        break;

    case MCEDIT_DRAG_MOVE:
        switch (command)
        {
        case CK_WindowResize:
            edit->drag_state = MCEDIT_DRAG_RESIZE;
            ret = TRUE;
            break;
        case CK_Up:
        case CK_Down:
        case CK_Left:
        case CK_Right:
            edit_window_move (edit, command);
            ret = TRUE;
            break;
        case CK_Enter:
        case CK_WindowMove:
            edit->drag_state = MCEDIT_DRAG_NONE;
            edit_status (edit, TRUE);   /* redraw frame and status */
            MC_FALLTHROUGH;
        default:
            ret = TRUE;
            break;
        }
        break;

    case MCEDIT_DRAG_RESIZE:
        switch (command)
        {
        case CK_WindowMove:
            edit->drag_state = MCEDIT_DRAG_MOVE;
            ret = TRUE;
            break;
        case CK_Up:
        case CK_Down:
        case CK_Left:
        case CK_Right:
            edit_window_resize (edit, command);
            ret = TRUE;
            break;
        case CK_Enter:
        case CK_WindowResize:
            edit->drag_state = MCEDIT_DRAG_NONE;
            edit_status (edit, TRUE);   /* redraw frame and status */
            MC_FALLTHROUGH;
        default:
            ret = TRUE;
            break;
        }
        break;

    default:
        break;
    }

    /**
     * - We let the user stop a resize/move operation by clicking with the
     *   mouse anywhere. ("clicking" = pressing and releasing a button.)
     * - We let the user perform a resize/move operation by a mouse drag
     *   initiated anywhere.
     *
     * "Anywhere" means: inside or outside the window. We make this happen
     * with the 'forced_capture' flag.
     */
    w->mouse.forced_capture = (edit->drag_state != MCEDIT_DRAG_NONE);

    return ret;
}

/* --------------------------------------------------------------------------------------------- */
/**
 * Toggle window fuulscreen mode.
 *
 * @param edit editor object
 */

void
edit_toggle_fullscreen (WEdit *edit)
{
    Widget *w = WIDGET (edit);

    edit->fullscreen = edit->fullscreen != 0 ? 0 : 1;
    edit->force = REDRAW_COMPLETELY;

    if (edit->fullscreen == 0)
    {
        edit_restore_size (edit);
        /* do not follow screen size on resize */
        w->pos_flags = WPOS_KEEP_DEFAULT;
    }
    else
    {
        WRect r;

        edit_save_size (edit);
        r = WIDGET (w->owner)->rect;
        rect_grow (&r, -1, 0);
        widget_set_size_rect (w, &r);
        /* follow screen size on resize */
        w->pos_flags = WPOS_KEEP_ALL;
        edit->force |= REDRAW_PAGE;
        edit_update_screen (edit);
    }
}

/* --------------------------------------------------------------------------------------------- */
