# Emacs.py
#
# Moleskine: a source code editor for the GNOME desktop
#
# Copyright (c) 2000 - 2002   Michele Campeotto <micampe@micampe.it>
#
# Emacs keybindings for Moleskine by Matthew Pratt <mattpratt@yahoo.com>
# Please mail me if I haven't done your favourite emacs keystroke yet
#
# Copyright (c) 2001   Matthew Pratt <mattpratt@yahoo.com>
#
# 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; either version 2 of the License, or
# (at your option) any later version.
#
# 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.

# [Ade]
# Reimplemented the data structure once again. This time there should be no
# need for further tweaking as it is capable of dealing with all the emacs
# commands I plan to implement support for.
# Cleared all scintilla keybindings.
#Slight problem with escape key inserting spurious characters. Defined an empty
#function so that escape currently does nothing.
#Ideally we would just re-enable the scintilla binding for this key.

# Added:
# The really useful emacs commands for directional movement.
# Go to start of line, end of line, next word, etc
# Alt-/ for auto-complete, insert/overwrite,
#  shifted selections

# TODO
# It might be nice if we could switch bindings/menu commands without restarting

import GDK

import Moleskine
import menu

last_values = (None, None, None, None)

#modifier constants
CTRL="CTRL"
ALT="ALT"
SHIFT="SHIFT"

#functions have to be defined before they can be used in the dictionary

def kill_line(w):
    """emacs style kill-line, cut from position to end of line
    but if there's nothing to cut then cut the line-ending"""
    doc = Moleskine.app.get_current_document()
    pos = doc.get_current_pos()
    line = doc.line_from_position(pos)
    doc.set_sel(pos, doc.get_line_end_position(line))
    
    if (doc.get_sel_text()==''):
    #we're at the end of the line so treat it differently
        c1 = doc.get_char_at(pos)
        c2 = doc.get_char_at(pos+1)
        if c1==13 and c2==10:
        #test for CR+LF combination
        #For some reason I don't understand only numbers work
            doc.set_sel(pos, pos+2)
        else:
            doc.set_sel(pos, pos+1)
        doc.cut()
    else:
        doc.copy() #copy used as cut kills the line-ending as well
        doc.replace_sel('')
    return

def select_all(w):
    doc = Moleskine.app.get_current_document()
    doc.select_all()

def line_up(w):
    doc = Moleskine.app.get_current_document()
    doc.line_up()

def line_down(w):
    doc = Moleskine.app.get_current_document()
    doc.line_down()

def char_left(w):
    doc = Moleskine.app.get_current_document()
    doc.char_left()

def char_right(w):
    doc = Moleskine.app.get_current_document()
    doc.char_right()

def line_start(w):
    doc = Moleskine.app.get_current_document()
    doc.home()

def line_end(w):
    doc = Moleskine.app.get_current_document()
    doc.line_end()

def document_start(w):
    doc = Moleskine.app.get_current_document()
    doc.document_start()

def document_end(w):
    doc = Moleskine.app.get_current_document()
    doc.document_end()

def page_up(w):
    doc = Moleskine.app.get_current_document()
    doc.page_up()

def page_down(w):
    doc = Moleskine.app.get_current_document()
    doc.page_down()

def word_right(w):
    doc = Moleskine.app.get_current_document()
    doc.word_right()

def word_left(w):
    doc = Moleskine.app.get_current_document()
    doc.word_left()

def backspace(w):
    doc = Moleskine.app.get_current_document()
    doc.delete_back()

def delete_forwards(w):
    doc = Moleskine.app.get_current_document()
    doc.clear()

def escape(w):
    pass

def enter(w):
    """insert the appropriate line-ending depending on the document's mode"""
    import GTKSCINTILLA
    doc = Moleskine.app.get_current_document()
    mode = doc.get_eol_mode()
    if mode == GTKSCINTILLA.EOL_LF:
        text = "\n"
    elif mode == GTKSCINTILLA.EOL_CRLF:
        text = "\r\n"
    else:#EOL_CR
        text = "\r"
    pos = doc.get_current_pos()
    doc.insert_text(pos, text)

def insert_or_overtype(w):
    """Flip mode to insert or overtype depending on previous state"""
    doc = Moleskine.app.get_current_document()
    doc.set_overtype(not doc.get_overtype())

def line_up_extend(w):
    doc = Moleskine.app.get_current_document()
    doc.line_up_extend()

def line_down_extend(w):
    doc = Moleskine.app.get_current_document()
    doc.line_down_extend()

def char_left_extend(w):
    doc = Moleskine.app.get_current_document()
    doc.char_left_extend()

def char_right_extend(w):
    doc = Moleskine.app.get_current_document()
    doc.char_right_extend()

def word_left_extend(w):
    doc = Moleskine.app.get_current_document()
    doc.word_left_extend()

def word_right_extend(w):
    doc = Moleskine.app.get_current_document()
    doc.word_right_extend()

def line_start_extend(w):
    doc = Moleskine.app.get_current_document()
    doc.home_extend()

def line_end_extend(w):
    doc = Moleskine.app.get_current_document()
    doc.line_end_extend()

def document_start_extend(w):
    doc = Moleskine.app.get_current_document()
    doc.document_start_extend()

def document_end_extend(w):
    doc = Moleskine.app.get_current_document()
    doc.document_end_extend()

def page_up_extend(w):
    doc = Moleskine.app.get_current_document()
    doc.page_up_extend()

def page_down_extend(w):
    doc = Moleskine.app.get_current_document()
    doc.page_down_extend()

#valid prefixes for multi-key commands
prefixes = [
    (CTRL, None, None, GDK.x),
    (CTRL, None, None, GDK.c)
    ]

#tuples are:
#((CTRL, ALT, SHIFT, keyval),(CTRL, ALT, SHIFT, keyval))
#with tuple beings last_values followed by current_values

emacs_keys = {
    ((CTRL, None, None, GDK.x),(CTRL, None, None, GDK.c)) : menu.exit,
    ((CTRL, None, None, GDK.x),(CTRL, None, None, GDK.f)) : menu.open_document,
    ((CTRL, None, None, GDK.x),(CTRL, None, None, GDK.s)) : menu.save_document,
    ((CTRL, None, None, GDK.x),(CTRL, None, None, GDK.w)) : menu.save_as_document,
    ((CTRL, None, None, GDK.x),(None, None, None, GDK.h)) : select_all,
    ((None, None, None, None),(CTRL, None, None, GDK.k)) : kill_line,
    ((None, None, None, None),(CTRL, None, None, GDK.Delete)) : kill_line,
    ((None, None, None, None),(CTRL, None, None, GDK.y)) : menu.paste,
    ((None, None, None, None),(CTRL, None, SHIFT, GDK.underscore)) : menu.undo,
    ((None, None, None, None),(None, ALT, None, GDK.g)) : menu.goto_line,
    ((None, None, None, None),(CTRL, None, None, GDK.p)): line_up,
    ((None, None, None, None),(None, None, None, GDK.Up)): line_up,
    ((None, None, None, None),(CTRL, None, None, GDK.n)): line_down,
    ((None, None, None, None),(None, None, None, GDK.Down)): line_down,
    ((None, None, None, None),(CTRL, None, None, GDK.b)): char_left,
    ((None, None, None, None),(None, None, None, GDK.Left)): char_left,
    ((None, None, None, None),(CTRL, None, None, GDK.f)): char_right,
    ((None, None, None, None),(None, None, None, GDK.Right)): char_right,
    ((None, None, None, None),(CTRL, None, None, GDK.e)): line_end,
    ((None, None, None, None),(None, None, None, GDK.End)): line_end,
    ((None, None, None, None),(CTRL, None, None, GDK.a)): line_start,
    ((None, None, None, None),(None, None, None, GDK.Home)): line_start,
    ((None, None, None, None),(CTRL, None, None, GDK.Home)):document_start,
    ((None, None, None, None),(CTRL, None, None, GDK.End)):document_end,
    ((None, None, None, None),(CTRL, None, None, GDK.v)): page_up,
    ((None, None, None, None),(None, None, None, GDK.Page_Up)): page_up,
    ((None, None, None, None),(None, ALT, None, GDK.v)): page_down,
    ((None, None, None, None),(None, None, None, GDK.Page_Down)): page_down,
    ((None, None, None, None),(None, ALT, None, GDK.f)): word_right,
    ((None, None, None, None),(CTRL, None, None, GDK.Right)): word_right,
    ((None, None, None, None),(None, ALT, None, GDK.b)): word_left,
    ((None, None, None, None),(CTRL, None, None, GDK.Left)): word_left,
    ((None, None, None, None),(None, None, None, GDK.BackSpace)):backspace,
    ((None, None, None, None),(CTRL, None, None, GDK.d)):delete_forwards,
    ((None, None, None, None),(None, None, None, GDK.Delete)):delete_forwards,
    ((None, None, None, None),(None, ALT, None, GDK.slash)):menu.complete_word,
    ((None, None, None, None),(None, None, None, GDK.Escape)):escape,
    ((None, None, None, None),(None, None, None, GDK.Return)):enter,
    ((None, None, None, None),(None, None, None, GDK.Insert)):insert_or_overtype,
    ((None, None, None, None),(None, None, SHIFT, GDK.Up)):line_up_extend,
    ((None, None, None, None),(None, None, SHIFT, GDK.Down)):line_down_extend,
    ((None, None, None, None),(None, None, SHIFT, GDK.Left)):char_left_extend,
    ((None, None, None, None),(None, None, SHIFT, GDK.Right)):char_right_extend,
    ((None, None, None, None),(CTRL, None, SHIFT, GDK.Left)):word_left_extend,
    ((None, None, None, None),(CTRL, None, SHIFT, GDK.Right)):word_right_extend,
    ((None, None, None, None),(None, None, SHIFT, GDK.Home)):line_start_extend,
    ((None, None, None, None),(None, None, SHIFT, GDK.End)):line_end_extend,
    ((None, None, None, None),(CTRL, None, SHIFT, GDK.Home)):document_start_extend,
    ((None, None, None, None),(CTRL, None, SHIFT, GDK.End)):document_end_extend,
    ((None, None, None, None),(None, None, SHIFT, GDK.Page_Up)):page_up_extend,
    ((None, None, None, None),(None, None, SHIFT, GDK.Page_Down)):page_down_extend,
}

def key_press(w, current_key):
    global last_values, CTRL, ALT, SHIFT
    l = []

    #is ctrl pressed
    if current_key.state & GDK.CONTROL_MASK:
        l.append(CTRL)
    else:
        l.append(None)
        
    #is alt pressed
    if current_key.state & GDK.MOD1_MASK:
        l.append(ALT)
    else:
        l.append(None)
        
    #is shift pressed
    if current_key.state & GDK.SHIFT_MASK:
        l.append(SHIFT)
    else:
        l.append(None)
        
    #identify key pressed
    l.append(current_key.keyval)
    
    current_values = tuple(l)
    cmd = (last_values, current_values)

    #process key if it's registered
    if emacs_keys.has_key(cmd):
        func = emacs_keys[cmd]
        func(w)
        w.emit_stop_by_name('key_press_event')
        last_values = (None, None, None, None)
        return

    #if key is a valid prefix, store it, otherwise ignore it
    else:
        if current_values in prefixes:
            last_values = current_values
            return
        else:
            last_values = (None, None, None, None)
            return


##############################################################################
# We must redefine the menus, sice most of the default key bindings clash with
# emacs key strokes
#
# Idealy we would just change the bindings of the existing menu items but
# I dont know how to get to them
from gnome.ui import *
from gnome.uiconsts import *

file_menu = [
    UIINFO_ITEM_STOCK(_('_New'), _('Create a new file'),
                       menu.new_document,STOCK_MENU_NEW),
    UIINFO_ITEM_STOCK(_('_Open...'), _('Load a file from disk'),
                       menu.open_document, STOCK_MENU_OPEN),
    UIINFO_SUBTREE_STOCK(menu.MENU_OPEN_PREVIOUS, [], STOCK_MENU_BOOK_OPEN),
    UIINFO_SEPARATOR,
    UIINFO_MENU_SAVE_ITEM(menu.save_document),
    UIINFO_MENU_SAVE_AS_ITEM(menu.save_as_document),
    UIINFO_ITEM_STOCK(_('Save all'), None, menu.save_all, STOCK_MENU_SAVE),
    UIINFO_MENU_REVERT_ITEM(menu.revert_document),
    UIINFO_SEPARATOR,
    (APP_UI_ITEM, _('Load session...'), _('Load session from a file'),
     menu.load_session, None, APP_PIXMAP_STOCK, STOCK_MENU_TIMER, GDK.F11, 0),
    (APP_UI_ITEM, _('Save session...'), _('Save current session to a file'),
     menu.save_session, None, APP_PIXMAP_STOCK, STOCK_MENU_TIMER_STOP, GDK.F12, 0),
    UIINFO_SUBTREE_STOCK(MENU_RECENT_SESSIONS, [], STOCK_MENU_BOOK_OPEN),
    UIINFO_SEPARATOR,
    UIINFO_ITEM_STOCK(_('Next'), None, menu.file_next, STOCK_MENU_FORWARD),
    UIINFO_ITEM_STOCK(_('Previous'), None, menu.file_previous, STOCK_MENU_BACK),
    (APP_UI_ITEM, _('Last used'), None, menu.file_last_used, None,
     APP_PIXMAP_NONE, None, GDK.P, GDK.CONTROL_MASK),
    UIINFO_SEPARATOR,
    UIINFO_ITEM_STOCK(_('_Close'), None, menu.close_document, STOCK_MENU_CLOSE),
    UIINFO_ITEM_STOCK(_('Close all'), None, menu.close_all, STOCK_MENU_CLOSE),
    UIINFO_ITEM_STOCK(_('E_xit'), None, menu.exit, STOCK_MENU_EXIT)
]

edit_menu = [
    (APP_UI_ITEM, _('_Undo'), _('Undo last action'),
     menu.undo, None, APP_PIXMAP_STOCK, STOCK_MENU_UNDO,
     ord('_'), GDK.CONTROL_MASK),
    UIINFO_ITEM_STOCK(_('_Redo'), _('Redo last action'), menu.redo, STOCK_MENU_REDO),
    UIINFO_SEPARATOR,
    (APP_UI_ITEM, _('C_ut'), _('Cut the current selection'),
     menu.cut, None, APP_PIXMAP_STOCK, STOCK_MENU_CUT,
     ord('w'), GDK.CONTROL_MASK),
    UIINFO_ITEM_STOCK(_('_Copy'), None, menu.copy, STOCK_MENU_COPY ),
    (APP_UI_ITEM, _('_Paste'), _('Paste from clipboard'),
     menu.paste, None, APP_PIXMAP_STOCK, STOCK_MENU_PASTE,
     ord('y'), GDK.CONTROL_MASK),
    UIINFO_SEPARATOR,
    (APP_UI_ITEM, _('_Search...'), _('Search a string in current file'),
     menu.search, None, APP_PIXMAP_STOCK, STOCK_MENU_SEARCH,
     ord('s'), GDK.CONTROL_MASK),
    (APP_UI_ITEM, _('Search in _files...'), _('Search a string in multiple files'),
     menu.search_in_files, None, APP_PIXMAP_STOCK, STOCK_MENU_SEARCH,
     0, 0),
    (APP_UI_ITEM, _('Search _Next'), _('Search the next occurrence of the last searched string'),
     menu.search_next, None, APP_PIXMAP_STOCK, STOCK_MENU_SEARCH,
     ord('g'), GDK.CONTROL_MASK),
    (APP_UI_ITEM, _('Search _Previous'), _('Search the previous occurrence of the last searched string'),
     menu.search_previous, None, APP_PIXMAP_STOCK, STOCK_MENU_SEARCH,
     ord('g'), GDK.CONTROL_MASK + GDK.SHIFT_MASK),
    (APP_UI_ITEM, _('_Replace...'), _('Search and replace a string in the current file'),
     menu.replace, None, APP_PIXMAP_STOCK, STOCK_MENU_SRCHRPL,
     ord('r'), GDK.CONTROL_MASK),
    UIINFO_SEPARATOR,
    (APP_UI_ITEM, _('_Goto line...'), _('Go to specified line in current file'),
     menu.goto_line, None, APP_PIXMAP_STOCK, STOCK_MENU_JUMP_TO,
     GDK.l, GDK.CONTROL_MASK + GDK.MOD1_MASK),
]

settings_menu = [
    UIINFO_ITEM_STOCK(_('_Configuration'), None, menu.options, STOCK_MENU_PROP),
    UIINFO_SEPARATOR,
    UIINFO_ITEM_STOCK(_('Languages _settings...'), None,
                      menu.lang_settings, STOCK_MENU_PREF),
    UIINFO_SUBTREE_STOCK(menu.MENU_CHANGE_LANGUAGE, [], STOCK_MENU_JUMP_TO),
    UIINFO_SEPARATOR,
    (APP_UI_TOGGLEITEM, _('Show _white space'), None, menu.toggle_ws, None,
     APP_PIXMAP_NONE, None, 0, 0),
    (APP_UI_TOGGLEITEM, _('Show _end of line'), None, menu.toggle_eol, None,
     APP_PIXMAP_NONE, None, 0, 0),
]

tools_menu = [
    (APP_UI_ITEM, _('_Next bookmark'), None, menu.next_bookmark, None,
     APP_PIXMAP_STOCK, STOCK_MENU_FORWARD, 0, 0),
    (APP_UI_ITEM, _('_Previous bookmark'), None, menu.prev_bookmark, None,
     APP_PIXMAP_STOCK, STOCK_MENU_BACK, 0, 0),
    (APP_UI_ITEM, _('_Toggle bookmark'), None, menu.toggle_bookmark, None,
     APP_PIXMAP_STOCK, STOCK_MENU_BOOK_YELLOW, 0, 0),
    UIINFO_ITEM_STOCK(_('_Clear bookmarks'), None,
                      menu.clear_bookmarks, STOCK_MENU_TRASH_FULL),
    UIINFO_SEPARATOR,
    (APP_UI_ITEM, _('_Complete word'),
     '',
     menu.complete_word, None, APP_PIXMAP_STOCK, STOCK_MENU_JUMP_TO,
     GDK.space, GDK.CONTROL_MASK),
    UIINFO_SUBTREE(_('Convert _EOLs'), [
        UIINFO_ITEM(_('LF (_Unix)'), None, menu.eol_to_lf),
        UIINFO_ITEM(_('CR (_Mac)'), None, menu.eol_to_cr),
        UIINFO_ITEM(_('CR+LF (Dos/_Win)'), None, menu.eol_to_crlf)]),
    UIINFO_SEPARATOR,
    UIINFO_SUBTREE(_('CVS'), [
        UIINFO_ITEM(_('Commit file to CVS'), None, menu.cvs_commit),
        UIINFO_ITEM(_('Update file from CVS'), None, menu.cvs_update),
        UIINFO_ITEM(_('CVS diff file'), None, menu.cvs_diff),
        UIINFO_ITEM(_('File\'s CVS status'), None, menu.cvs_status),
        UIINFO_ITEM(_('File\'s CVS log'), None, menu.cvs_log),
        UIINFO_ITEM(_('Add file to CVS'), None, menu.cvs_add),
        UIINFO_ITEM(_('Remove file from CVS'), None, menu.cvs_remove),
        UIINFO_SEPARATOR,
        UIINFO_ITEM(_('Commit directory to CVS'), None, menu.cvs_commit_dir),
        UIINFO_ITEM(_('Update directory from CVS'), None, menu.cvs_update_dir),
        UIINFO_ITEM(_('CVS diff directory'), None, menu.cvs_diff_dir),
        UIINFO_ITEM(_('Directory\'s CVS status'), None, menu.cvs_status_dir),
        UIINFO_ITEM(_('Directory\'s CVS log'), None, menu.cvs_log_dir),
        UIINFO_ITEM(_('Add directory to CVS'), None, menu.cvs_add_dir)#,
        #UIINFO_ITEM(_('Remove directory from CVS'), None, menu.cvs_directory)
        ]),
    UIINFO_SEPARATOR,
    (APP_UI_ITEM, _('Clean-up recent files list'), None, menu.clean_up_mru, None,
     APP_PIXMAP_NONE, None, 0, 0),
    (APP_UI_ITEM, _('Delete recent files list'), None, menu.delete_mru, None,
     APP_PIXMAP_NONE, None, 0, 0),
]

help_menu = [
#    UIINFO_MENU_ABOUT_ITEM(menu.about)
    UIINFO_ITEM_STOCK(_('Moleskine info'), None, menu.about, STOCK_MENU_ABOUT)
]

main_menu = [
    UIINFO_SUBTREE(menu.MENU_FILE, file_menu),
    UIINFO_SUBTREE(_('_Edit'), edit_menu),
    UIINFO_SUBTREE(menu.MENU_SETTINGS, settings_menu),
    UIINFO_SUBTREE(_('_Tools'), tools_menu),
    UIINFO_SUBTREE(_('_Help'), help_menu)
]
