#!/usr/bin/env python
"""
Fisheye Edit - A basic text editor which does Fisheye-esqe zooming of line 
surrounding the cursor.

(Copyleft 2007 Andrew Gwozdziewycz)

$Id: $

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 3 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 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/>


HAS:
   * support for basic syntax highlighting for Python
   * Cut/Copy/Paste, Save, SaveAs

NEEDS:
   * more customization options
   * <insert tons of needs here>

ISSUES:
   * should do smarter highlighting.
   * should also do smarter fisheyeing...

"""

__version__ = '0.1'
__author__ = "Andrew Gwozdziewycz <hg@apgwoz.com>"

import Tkinter, tkFont, tkFileDialog
import math, re

import token, tokenize, keyword
import cStringIO

class FisheyeText(Tkinter.Text):
    def __init__(self, *args, **kwargs):
        Tkinter.Text.__init__(self, *args, **kwargs)
        self.bind('<KeyRelease>', self.on_key_release)
        self.bind('<ButtonRelease-1>', self.on_mouse_release)
        self.bind('<MouseWheel>', self.on_mouse_wheel)
        self.old_linenum = 0
        self.linenum = 1
        self.column = 0
        self.old_column  = 0
        self.syntax_tags = []
        self.focus()

        self.update_font_sizes(25, 8, 3)

        self.config(font=tkFont.Font(size=self.fontsize, family="Monoco"))

    def update_font_sizes(self, fisheyesize=None, fontsize=None, fisheyedeltas=None):
        if fisheyesize:
            self.fisheye_size = fisheyesize
        if fontsize:
            self.fontsize = fontsize
            self.config(font=tkFont.Font(size=int(fontsize)))
        if fisheyedeltas:
            self.fisheye_deltas = fisheyedeltas

        if fisheyesize or fontsize or fisheyedeltas:
            delta = abs((self.fisheye_size - self.fontsize) / float(self.fisheye_deltas))
            print delta
            self.fisheye_fonts = []

            fs = max(self.fisheye_size, self.fontsize)
            ms = min(self.fisheye_size, self.fontsize)

            while fs > ms:
                print 'FS: ', fs
                self.fisheye_fonts.append(tkFont.Font(size=int(fs)))
                fs -= delta

            if self.fisheye_size < self.fontsize:
                self.fisheye_fonts.reverse()

    def reset_cursor(self):
        self.mark_set(Tkinter.INSERT, 1.0)

    def update_cursor(self):
        self.old_linenum = self.linenum
        self.old_column = self.column
        self.linenum, self.column = \
            tuple(map(int, self.index(Tkinter.INSERT).split('.')))

    def update_see(self):
        self.see('%d.%d' % (self.linenum, self.column))        
    
    def on_key_release(self, event):
        self.update_cursor()
        self.update_fisheye()
        self.update_syntax()
        self.update_see()

    def on_mouse_release(self, event):
        self.update_cursor()
        self.update_fisheye()
        self.update_see()

    def update_syntax(self):
#         contents = cStringIO.StringIO(self.get(1.0, Tkinter.END))
#         toIndex = lambda x: '%d.%d' % (x[0], x[1])

#         tags = tuple(filter(lambda x: x.startswith('syntax'), self.tag_names()))
#         if len(tags):
#             self.tag_delete(*tags)

#         i = 0
#         for typ, tok, strt, end, ln in tokenize.generate_tokens(contents.readline):
#             ignore = False
#             if typ == tokenize.NAME:
#                 if keyword.iskeyword(tok):
#                     color = "blue"
#                 else:
#                     ignore = True
#             elif typ == tokenize.OP:
#                 color = "red"
#             elif typ == tokenize.STRING:
#                 color = "green"
#             elif typ == tokenize.COMMENT:
#                 color = "orange"
#             else:
#                 ignore = True
            
#             if not ignore:
#                 self.tag_add('syntax%d' % i, toIndex(strt), toIndex(end))
#                 self.tag_config('syntax%d' % i, foreground=color)
#                 i += 1
        pass

    def update_fisheye(self):
        # no need to run this function on all of the lines, just the local ones

        tags = tuple(filter(lambda x: x.startswith('line'), self.tag_names()))

        if len(tags):
            self.tag_delete(*tags)
        
        lines = self.get(1.0, Tkinter.END).split('\n')

        lff = len(self.fisheye_fonts)-1
        for i, l in enumerate(lines):
            findx = abs(i+1-self.linenum)
            if findx <= lff:
                nonspace = re.search('[^\s]', l)
                if nonspace:
                    startcol = nonspace.start()
                else:
                    startcol = 0                
                indx1 = '%d.%d' % ((i+1), startcol)
                indx2 = '%d.%d' % (i+1, len(l)*2)
                self.tag_add('line%d' % (i+1), indx1, indx2)
                self.tag_config('line%d' % (i+1), font=self.fisheye_fonts[findx])

    def on_mouse_wheel(self, event):
        self.yview(Tkinter.SCROLL, -event.delta//120, Tkinter.UNITS)

    #
    #edit
    def on_select_all(self):
        try: 
            self.tag_delete(Tkinter.SEL)
            self.tag_add(Tkinter.SEL, "1.0", Tkinter.END)
        except:
            pass

    def on_copy(self):
        try:
            selection = self.get(Tkinter.SEL_FIRST, Tkinter.SEL_LAST)
            self.clipboard_clear()
            self.clipboard_append(selection)
        except:
            pass

    def on_cut(self):
        try:
            selection = self.get(Tkinter.SEL_FIRST, Tkinter.SEL_LAST)
            self.clipboard_clear()
            self.clipboard_append(selection)
            self.delete(Tkinter.SEL_FIRST, Tkinter.SEL_LAST)
        except:
            pass
        self.update_cursor()
        self.update_fisheye()
        self.update_syntax()
        self.update_see()

    def on_paste(self):
        topaste = self.selection_get(selection="CLIPBOARD")
        self.insert(Tkinter.INSERT, topaste)
        self.update_cursor()
        self.update_fisheye()
        self.update_syntax()
        self.update_see()
        
class TextEditor(Tkinter.Frame):
    def __init__(self, win, *args, **kwargs):
        Tkinter.Frame.__init__(self, win, *args, **kwargs)
        self.win = win

        # create the status bar
        self.statusbar = Tkinter.Frame(self.win, border=3)
        self.status_fisheye = Tkinter.Entry(self.statusbar, width=5, font=tkFont.Font(size=9), relief=Tkinter.SUNKEN, border=2, disabledforeground="black")
        self.status_fontsize = Tkinter.Entry(self.statusbar, width=5, font=tkFont.Font(size=9), relief=Tkinter.SUNKEN, border=2, disabledforeground="black")
        self.status_fisheye_deltas = Tkinter.Entry(self.statusbar, width=5, font=tkFont.Font(size=9), relief=Tkinter.SUNKEN, border=2, disabledforeground="black")

        self.status_fisheye.pack(side=Tkinter.LEFT)
        self.status_fontsize.pack(side=Tkinter.LEFT)
        self.status_fisheye_deltas.pack(side=Tkinter.LEFT)
        self.statusbar.pack(side=Tkinter.BOTTOM, fill=Tkinter.X)

        # text editor
        self.xscrollbar = Tkinter.Scrollbar(win, orient=Tkinter.HORIZONTAL)
        self.yscrollbar = Tkinter.Scrollbar(win)

        self.xscrollbar.pack(side=Tkinter.BOTTOM, fill=Tkinter.X)
        self.yscrollbar.pack(side=Tkinter.RIGHT, fill=Tkinter.Y)

        self.text = FisheyeText(win, wrap=Tkinter.NONE, \
                       xscrollcommand=self.xscrollbar.set,
                       yscrollcommand=self.yscrollbar.set)

        self.text.pack(expand=Tkinter.YES, fill=Tkinter.BOTH)        

        self.xscrollbar.config(command=self.text.xview)
        self.yscrollbar.config(command=self.text.yview)

        menubar = Tkinter.Menu(win)
        filemenu = Tkinter.Menu(menubar, tearoff=0)
        filemenu.add_command(label="Open", command=self.on_open)
        filemenu.add_command(label="Save", command=self.on_save)
        filemenu.add_command(label="Save As", command=self.on_save_as)
        filemenu.add_separator()
        filemenu.add_command(label="Exit", command=self.win.quit)
        menubar.add_cascade(label="File", menu=filemenu)


        editmenu = Tkinter.Menu(menubar, tearoff=0)
        editmenu.add_command(label="Select All", command=self.text.on_select_all)
        editmenu.add_command(label="Cut", command=self.text.on_cut)
        editmenu.add_command(label="Copy", command=self.text.on_copy)
        editmenu.add_command(label="Paste", command=self.text.on_paste)
        editmenu.add_command(label="Syntax", command=self.text.update_syntax)
        menubar.add_cascade(label="Edit", menu=editmenu)

        self.menubar = menubar
        self.filemenu = filemenu
        self.editmenu = editmenu

        self.update_status_fontsize()
        self.update_status_fisheye()
        self.update_status_fisheye_deltas()

        self.win.bind('<Control-+>', self.on_fontsize_plus)
        self.win.bind('<Control-_>', self.on_fontsize_minus)

        self.win.bind('<Control-}>', self.on_fisheye_plus)
        self.win.bind('<Control-{>', self.on_fisheye_minus)

        self.win.bind('<Control-)>', self.on_fisheye_deltas_plus)
        self.win.bind('<Control-(>', self.on_fisheye_deltas_minus)

        self.win.config(menu=self.menubar)

        # editor variables
        self.current_file = None


    def update_status_fisheye(self):
        self.status_fisheye.config(state=Tkinter.NORMAL)
        self.status_fisheye.delete(0, Tkinter.END)
        self.status_fisheye.insert(0, 'F: %d' % self.text.fisheye_size)
        self.status_fisheye.config(state=Tkinter.DISABLED)

    def update_status_fisheye_deltas(self):
        self.status_fisheye_deltas.config(state=Tkinter.NORMAL)
        self.status_fisheye_deltas.delete(0, Tkinter.END)
        self.status_fisheye_deltas.insert(0, 'C: %d' % self.text.fisheye_deltas)
        self.status_fisheye_deltas.config(state=Tkinter.DISABLED)

    def update_status_fontsize(self):
        self.status_fontsize.config(state=Tkinter.NORMAL)
        self.status_fontsize.delete(0, Tkinter.END)
        self.status_fontsize.insert(0, 'S: %d' % self.text.fontsize)
        self.status_fontsize.config(state=Tkinter.DISABLED)

    def on_save(self):
        if self.current_file and len(self.current_file):
            self.write_file(self.current_file)
        else:
            self.on_save_as()

    def on_save_as(self):
        dig = tkFileDialog.SaveAs()
        f = dig.show()
        if len(f):
            self.current_file = f
            self.write_file(f)

    def on_open(self):
        dig = tkFileDialog.Open()
        f = dig.show()
        if len(f):
            self.load_file(f)

    def on_fisheye_plus(self, event):
        self.text.update_font_sizes(fisheyesize=self.text.fisheye_size+1)
        self.update_status_fisheye()

    def on_fisheye_minus(self, event):
        if self.text.fisheye_size > 2:
            self.text.update_font_sizes(fisheyesize=self.text.fisheye_size-1)
        self.update_status_fisheye()

    def on_fisheye_deltas_plus(self, event):
        self.text.update_font_sizes(fisheyedeltas=self.text.fisheye_deltas+1)
        self.update_status_fisheye_deltas()

    def on_fisheye_deltas_minus(self, event):
        if self.text.fisheye_deltas > 2:
            self.text.update_font_sizes(fisheyedeltas=self.text.fisheye_deltas-1)
        self.update_status_fisheye_deltas()

    def on_fontsize_plus(self, event):
        self.text.update_font_sizes(fontsize=self.text.fontsize+1)
        self.update_status_fontsize()

    def on_fontsize_minus(self, event):
        if self.text.fontsize > 2:
            self.text.update_font_sizes(fontsize=self.text.fontsize-1)
        self.update_status_fontsize()

    def load_file(self, f):
        try:
            contents = open(f).read()
            self.current_file = f
        except IOError, m:
            contents = ""
            tkMessageBox.showerror("Open File...", m)
        
        self.text.delete(1.0, Tkinter.END)
        self.text.insert(Tkinter.END, contents)
        self.text.reset_cursor()
        self.text.update_fisheye()
        self.text.update_syntax()
        self.text.update_see()

    def write_file(self, file):
        contents = self.text.get('1.0', Tkinter.END)
        try:
            fo = open(file, 'w')
            fo.write(contents)
            fo.close()
        except IOError, m:
            tkMessageBox.showerror("Write File...", m)
            

if __name__ == '__main__':
    win = Tkinter.Tk()
    win.geometry("%dx%d%+d%+d" % (600, 680, 30, 22))
    win.title("FisheyeEdit")
    win.focus()
    x = TextEditor(win)
    x.pack()
    win.mainloop()
