# MoleskineApp.py
#
# Moleskine: a source code editor for the GNOME desktop
#
# Copyright (c) 2000 - 2002   Michele Campeotto <micampe@micampe.it>
#
# 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.

import os
import os.path
import stat
import popen2

import gtk
import GTK
import GDK
import gnome.ui
import gnome.uiconsts

from Document import Document
from Config import Config
from Languages import LanguagesList
import menu
import toolbar

TARGET_STRING = 0
TARGET_ROOTWIN = 1
target = [
    ('STRING', 0, TARGET_STRING),
    ('text/plain', 0, TARGET_STRING),
    ('application/x-rootwin-drop', 0, TARGET_ROOTWIN)]

in_event = 0

class MoleskineApp(gnome.ui.GnomeApp):
    def __init__(self):
        gnome.ui.GnomeApp.__init__(self, 'GnomeMoleskine', 'Moleskine')
        
        self.switch = 1
        self.documents = []
        self.reload_notification = 0
        self.last_page = [0, 0]
        self.load_prefs()
        
        self.notebook = gtk.GtkNotebook()
        self.notebook.connect_after('switch-page', self.__switch_page)
        self.notebook.connect_after('set_focus_child', self.__focus_notebook_tab)
        self.notebook.set_scrollable(1)
        self.notebook.drag_dest_set(gtk.DEST_DEFAULT_ALL, target, GDK.ACTION_COPY | GDK.ACTION_MOVE)
        self.notebook.connect('drag_drop',self.drag_drop)
        self.notebook.connect('drag_data_received',self.drag_data_received)
        self.notebook.show()
        self.set_contents(self.notebook)
        
        self.status = gnome.ui.GnomeAppBar()
        self.progress = self.status.get_progress()
        self.set_statusbar(self.status)
        
        self.set_wmclass('main_window', 'Moleskine')
        self.set_geometry_hints(min_width=0, min_height=0)
        self.connect('key_press_event', self.key_press)
        if int(self.prefs['keyboard/keybindings']) == 2:
            import Emacs
            self.create_menus(Emacs.main_menu)
            self.Emacs_key_press = Emacs.key_press
        elif int(self.prefs['keyboard/keybindings']) == 1:
            import GnomeMenu
            self.create_menus(GnomeMenu.main_menu)
        else:
            self.create_menus(menu.main_menu)
        self.create_toolbar(toolbar.main_toolbar)
        self.connect('delete_event', menu.exit)
        self.drag_dest_set(gtk.DEST_DEFAULT_ALL, target, GDK.ACTION_COPY | GDK.ACTION_MOVE)
        self.connect('drag_drop',self.drag_drop)
        self.connect('drag_data_received',self.drag_data_received)
        self.connect_after('focus_in_event', self.__focus_notebook_tab)
        self.show_all()
        
        self.load_langs()
        self.load_mru()
        self.load_rsess()
    
    def key_press(self, w, event):
        """Handle keypresses."""
        if event.state & GDK.MOD1_MASK and not event.state & (GDK.SHIFT_MASK | GDK.CONTROL_MASK):
            if event.keyval >= GDK._1 and event.keyval <= GDK._9:
                self.notebook.set_page(event.keyval - GDK._1)
            elif event.keyval == GDK._0:
                self.notebook.set_page(9)
            elif int(self.prefs['keyboard/keybindings']) == 2:
            #forward alt keypresses to emacs keybindings if we don't use them
                self.Emacs_key_press(w, event)
        else:
            if int(self.prefs['keyboard/keybindings']) == 2:
                self.Emacs_key_press(w, event)

    def drag_drop(self, w, context, x, y, time):
        w.drag_get_data(context, context.targets[0], time)
    
    def drag_data_received(self, w, context, x, y, data, info, time):
        self.open_document(data.data)
    
    def new_document(self):
        """Create a new document."""
        doc = Document()
        self.documents.append(doc)
        self.notebook.append_page(doc, doc.label)
        self.notebook.set_page(-1)
        self.update_title()
    
    def open_document(self, filepath):
        """Open a document.
        
        The file name can contain relative paths, special shell chars (~) and
        environment variables.
        If the current document is empty the newly created one will replace it.
        If the given file doesn't exist you will be asked about to create it.
        """
        from string import strip
        import urlparse
        url = urlparse.urlparse(filepath)
        filepath = strip(url[2])
        
        # remove null bytes from GMC filenames.
        from string import replace
        filepath = replace(filepath, "\0","")
        filepath = strip(filepath)
        
        filepath = os.path.expanduser(filepath)
        filepath = os.path.expandvars(filepath)
        filepath = os.path.abspath(filepath)
        
        if os.path.isdir(filepath):
            gnome.ui.GnomeMessageBox(
                _('Cannot open `%s\': is a directory.') % \
                os.path.basename(filepath),
                gnome.uiconsts.MESSAGE_BOX_ERROR,
                gnome.uiconsts.STOCK_BUTTON_OK).run_and_close()
        elif not os.path.exists(filepath):
            res = gnome.ui.GnomeMessageBox(
                    _('File `%s\' not found.\nDo you want to create it?') % \
                            os.path.basename(filepath),
                    gnome.uiconsts.MESSAGE_BOX_QUESTION,
                    gnome.uiconsts.STOCK_BUTTON_YES,
                    gnome.uiconsts.STOCK_BUTTON_NO).run_and_close()
            if res == 0:
                self.__load(filepath)
                self.get_current_document().set_text(' ')
                self.get_current_document().set_text('')
            else:
                if len(self.documents) == 0:
                    self.new_document()
        elif not os.access(filepath, os.R_OK):
            gnome.ui.GnomeMessageBox(
                _('Cannot open `%s\': permission denied.') % \
                os.path.basename(filepath),
                gnome.uiconsts.MESSAGE_BOX_ERROR,
                gnome.uiconsts.STOCK_BUTTON_OK).run_and_close()
            return
        else:
            self.__load(filepath)
        self.get_current_document().grab_focus()
        self.update_title()
    
    def __load(self, filepath):
        doc = self.get_current_document()
        
        for i in range(len(self.documents)):
            if self.documents[i].filepath == filepath:
                self.notebook.set_page(i)
                if not self.documents[i].is_modified():
                    self.documents[i].revert()
                return
        if doc is None or not doc.is_empty():
            self.new_document()
            doc = self.get_current_document()
        doc.load_file(filepath)
        self.add_to_mru(filepath)
    
    def save_document(self, filepath = None):
        self.get_current_document().save_file(filepath)
    
    def close_document(self):
        """Close the active document.
        
        If the document to be closed is the last one, a new empy document will
        be created.
        If the document has been modified and not saved you will be asked if
        you want to save the changes.
        """
        def save_and_close(fs, user_data = None):
            import Moleskine
            
            app = Moleskine.app
            app.save_document(fs.get_filename())
            app.close_document()
        
        doc = self.get_current_document()
        if not doc.can_close():
            res = gnome.ui.GnomeMessageBox(
                    _('File `%s\' has been modified\nand not saved.' +
                    ' Do you want to save it now?') % doc.filename,
                    gnome.uiconsts.MESSAGE_BOX_QUESTION,
                    gnome.uiconsts.STOCK_BUTTON_YES, 
                    gnome.uiconsts.STOCK_BUTTON_NO,
                    gnome.uiconsts.STOCK_BUTTON_CANCEL).run_and_close()
            if res == 0:
                if doc.filepath is not None:
                    doc.save_file()
                else:
                    self.ask_file('save', save_and_close)
                    return
            elif res == 2 or res < 0:
                return
        page = self.notebook.get_current_page()
        if self.last_page[1] is None:
            self.last_page[1] = page
        elif self.last_page[1] > page:
            self.last_page[1] = self.last_page[1] - 1
        self.last_page[0] = self.last_page[1]
        switch = self.switch
        self.switch = 0
        self.notebook.remove_page(page)
        self.documents.remove(doc)
        self.switch = switch
        if len(self.documents) == 0:
            self.new_document()
        else:
            if self.notebook.get_current_page() == self.last_page[1]:
                force_update = 1
            else:
                force_update = 0
            self.notebook.set_page(self.last_page[1])
            if force_update == 1:
                self.update_title()
            for i in range(len(self.documents)):
                if self.documents[i].is_modified():
                    asterisk = '*'
                else:
                    asterisk = ''
                if self.documents[i].filename is not None:
                    self.documents[i].label.set_text('%s%i. %s' % (asterisk, i + 1, self.documents[i].filename))
                else:
                    self.documents[i].label.set_text('%s%i. %s' % (asterisk, i + 1, _('Unnamed')))
            self.get_current_document().grab_focus()
    
    def ask_file(self, type, ok_cb):
        if type == 'load':
            title = _('Open file...')
            wm_name = 'open_dialog'
        elif type == 'save':
            title = _('Save as...')
            wm_name = 'save_dialog'
        elif type == 'load_session':
            title = _('Open session...')
            wm_name = 'open_dialog'
        elif type == 'save_session':
            title = _('Save session as...')
            wm_name = 'save_dialog'
        else:
            title = type
            wm_name = 'file_dialog'
        
        fs = gtk.GtkFileSelection(title)
        #fs.set_wmclass(wm_name, 'Moleskine')
        fs.set_transient_for(self)
        if type[:4] != 'save':
            fs.file_list.set_selection_mode(GTK.SELECTION_MULTIPLE)
        
        doc = self.get_current_document()
        if doc is not None and doc.filepath is not None:
            startdir = os.path.dirname(doc.filepath)
            fs.set_filename(startdir + '/')
        elif self.last_page[1] is not None and \
             self.documents[self.last_page[1]] is not None and \
             self.documents[self.last_page[1]].filepath is not None:
            startdir = os.path.dirname(self.documents[self.last_page[1]].filepath)
            fs.set_filename(startdir + '/')
        if ok_cb is not None:
            fs.ok_button.connect_object('clicked', ok_cb, fs)
        
        fs.ok_button.connect_object('clicked', gtk.GtkWidget.destroy, fs)
        fs.cancel_button.connect_object('clicked', gtk.GtkWidget.destroy, fs)
        fs.show()
    
    def get_current_document(self):
        """Returns the currently active document.
        
        If there are no documents (should never happen) it will return None.
        """
        if len(self.documents) > 0:
            return self.documents[self.notebook.get_current_page()]
        else:
            return None
    
    def update_ru_menu(self, callback, menu_label, list, sort = 0):
        """ Update recently used items menu
        """
        import string
        
        fix_underscore = lambda label, replace=string.replace: \
                replace(label, '_', '__')
        
        build_item = lambda label, callback=callback: \
                (gnome.uiconsts.APP_UI_ITEM, label, None,
                 callback, None,
                 gnome.uiconsts.APP_PIXMAP_NONE, None, 0, 0)
        
        if int(sort):
            list.sort()
        list = map(fix_underscore, list)
        ru_menu = map(build_item, list)
        self.remove_menus(menu_label, 55)
        self.insert_menus(menu_label, ru_menu)
    
    def add_to_ru(self, list, filepath, size):
        if filepath in list:
            list.remove(filepath)
        list.insert(0, filepath)
        if len(list) > int(size):
            list = list[:size]
    
    def clean_up_ru(self, list):
        """Remove non-existent files from the recently used items list"""
        from os.path import exists
        
        count = 0
        start_val = len(list)
        while count < len(list):
            if not exists(list[count]):
                del(list[count])
                count = count - 1     #step back into the changed list
            count = count + 1
        
        #print start_val - len(list), " files removed"
    
    def load_mru(self):
        self.mru = Config('Moleskine/PreviousFiles')
        self.files = []
        count = self.mru['files/count']
        if count is not None:
            for i in range(int(count)):
                self.files.append(self.mru['files/file_%i' % i])
            self.update_mru_menu()
    
    def save_mru(self):
        self.files = self.files[:int(self.prefs['history/mru_size'])]
        for i in range(len(self.files)):
            self.mru['files/file_%i' % i] = self.files[i]
        self.mru['files/count'] = len(self.files)
        self.mru.save()
    
    def update_mru_menu(self):
        import copy
        
        temp_list = copy.copy(self.files)[:int(self.prefs['history/mru_size'])]
        self.update_ru_menu(menu.open_recent, menu.MENU_MRU, temp_list, int(self.prefs['history/mru_sort']))
    
    def add_to_mru(self, filepath):
        self.add_to_ru(self.files, filepath, int(self.prefs['history/mru_size']))
        self.update_mru_menu()
    
    def clean_up_mru(self):
        self.clean_up_ru(self.files)
        self.update_mru_menu()
    
    def delete_mru(self):
        self.files = []
        self.update_mru_menu()
    
    def load_rsess(self):
        self.rsess = Config('Moleskine/RecentSessions')
        self.rsessions = []
        count = self.rsess['sessions/count']
        if count is not None:
            for i in range(int(count)):
                self.rsessions.append(self.rsess['sessions/session_%i' % i])
            self.update_rsess_menu()
    
    def save_rsess(self):
        self.rsessions = self.rsessions[:10]
        for i in range(len(self.rsessions)):
            self.rsess['sessions/session_%i' % i] = self.rsessions[i]
        self.rsess['sessions/count'] = len(self.rsessions)
        self.rsess.save()
    
    def update_rsess_menu(self):
        import copy
        
        temp_list = copy.copy(self.rsessions)[:10]
        self.update_ru_menu(menu.open_recent_session, menu.MENU_RSESS, temp_list)
    
    def add_to_rsess(self, filepath):
        self.add_to_ru(self.rsessions, filepath, 10)
        self.update_rsess_menu()
    
    def clean_up_rsess(self):
        self.clean_up_ru(self.rsessions)
        self.update_rsess_menu()
    
    def delete_rsess(self):
        self.rsessions = []
        self.update_rsess_menu()
    
    def load_session(self, filename = None):
        if filename is not None:
            self.session = Config('=' + filename + '=')
            self.add_to_rsess(filename)
        else:
            self.session = Config('Moleskine/Session')
        count = self.session['files/count']
        if count is not None and int(count) > 0:
            if filename is not None:
                self.notebook.set_page(0)
                for i in range(len(self.documents)):
                    self.close_document()
            self.switch = 0
            document_index = 0
            for i in range(int(count)):
                if self.session['files/file_%i' % i] == 'None':
                    self.new_document()
                    document_index = document_index + 1
                else:
		    if self.session['files/file_%i' % i] is not None:
                        self.open_document(self.session['files/file_%i' % i])
	                if (len(self.documents) - document_index) == 1:
	                    doc = self.documents[document_index]
		            if self.session['langs/lang_%i' % i] in self.langs.keys():
		                doc.set_language(self.langs[self.session['langs/lang_%i' % i]])
			    if self.session['pos/pos_%i' % i] is not None:
			        doc.goto_pos(int(self.session['pos/pos_%i' % i]))
                            document_index = document_index + 1
            self.switch = 1
            if not document_index:
                self.new_document()
            self.notebook.set_page(int(self.session['notebook/tab']))
            self.update_title()
    
    def save_session(self, filename = None):
        if int(self.prefs['history/save_session']) or filename is not None:
            if filename is not None:
                self.session = Config('=' + filename + '=')
                self.add_to_rsess(filename)
            else:
                self.session = Config('Moleskine/Session')
            self.session['notebook/tab'] = self.notebook.get_current_page()
            self.session['files/count'] = len(self.documents)
            for i in range(len(self.documents)):
                self.session['files/file_%i' % i] = self.documents[i].filepath
                self.session['langs/lang_%i' % i] = self.documents[i].language.name
                self.session['pos/pos_%i' % i] = self.documents[i].get_current_pos()
            self.session.save()
        else:
            self.session = Config('Moleskine/Session')
            self.session.data.clear()
            self.session['files/count'] = 0
            self.session.save()
    
    def load_prefs(self):
        self.prefs = Config('Moleskine/Preferences')
        
        self.prefs.dirnames = []
        count = int(self.prefs['directory names/count'])
        for n in range(count):
            name = self.prefs['directory names/name_%i' % n]
            directory = self.prefs['directory names/directory_%i' % n]
            
            directory = os.path.expanduser(directory)
            directory = os.path.expandvars(directory)
            directory = os.path.abspath(directory)
            
            self.prefs.dirnames.append((name, directory))
        
        w = int(self.prefs['geometry/width'])
        h = int(self.prefs['geometry/height'])
        self.set_default_size(w, h)
    
    def save_prefs(self):
        count = len(self.prefs.dirnames)
        self.prefs['directory names/count'] = count
        for n in range(count):
            self.prefs['directory names/name_%i' % n] = \
                    self.prefs.dirnames[n][0]
            self.prefs['directory names/directory_%i' % n] = \
                    self.prefs.dirnames[n][1]
        
        if int(self.prefs['geometry/save_win_size']):
            self.prefs['geometry/width'] = self.get_window().width
            self.prefs['geometry/height'] = self.get_window().height
    
    # FIXME: Can't get the right window position! :( This gives
    # the position of the client window (without decorations)
    # and set_uposition places the window with deocorations. Hints?
    #    if int(self.prefs['geometry/save_win_pos']):
    #        self.prefs['geometry/pos_x'] = self.get_window().x
    #        self.prefs['geometry/pos_y'] = self.get_window().y
        self.prefs.save()
    
    def load_langs(self):
        self.langs = LanguagesList()
        self.langs.load()
        
        langs_names = self.langs.keys()
        langs_names.sort()
        build_item = lambda label: \
                (gnome.uiconsts.APP_UI_ITEM, label, None,
                 menu.set_lang, None,
                 gnome.uiconsts.APP_PIXMAP_NONE, None, 0, 0)
        langs_menu = [gnome.uiconsts.UIINFO_RADIOLIST(
                          map(build_item, langs_names))]
        self.remove_menus(menu.MENU_SETTINGS + '/' +
                          menu.MENU_CHANGE_LANGUAGE + '/', len(langs_names))
        self.insert_menus(menu.MENU_SETTINGS+'/' +
                          menu.MENU_CHANGE_LANGUAGE + '/', langs_menu)
    
    def update_styles(self):
        for doc in self.documents:
            doc.setup_default_style()
            doc.setup_caret_style()
            doc.setup_line_numbers_style()
            doc.reload_language()
    
    def update_title(self):
        if not self.switch:
            return
        doc = self.get_current_document()
        if doc.is_modified():
            sep = ' * '
        else:
            sep = ' - '
        self.set_title('Moleskine' + sep + doc.title)
    
    def __switch_page(self, notebook, notebook_page, page_num, data=None):
        if not self.switch:
            return
        doc = self.get_current_document()
        self.reload_notification = 0
        self.last_page[1] = self.last_page[0]
        self.last_page[0] = page_num
        label = notebook.get_tab_label(notebook.get_nth_page(page_num))
        label.set_rc_style()
        self.update_title()
        try:
            langs_names = self.langs.keys()
            langs_names.sort()
            index = langs_names.index(doc.language.name)
            menu_items = self.langs_menu.children()
            if len(langs_names) < len(menu_items):
                menu_items = menu_items[1:]
            menu_items[index].set_active(1)
        except AttributeError:
            pass
        doc.set_view_ws(int(self.prefs['view/ws']))
        doc.set_view_eol(int(self.prefs['view/eol']))
        doc.ensure_visible(doc.line_from_position(doc.get_current_pos()))
        doc.grab_focus()
    
    def __focus_notebook_tab(self, widget, event=None, data=None):
        global in_event
        if event is None:
            return
        #print "widget =>", widget, "event =>", event, "data =>", data
        #return
        doc = self.get_current_document()
        if doc is None:
            return
        # Compare modification times
        #print "file =>", doc.filepath
        if not self.reload_notification and \
           doc.filepath is not None and \
           not os.access(doc.filepath, os.F_OK):
            # File has been removed
            gnome.ui.GnomeMessageBox(
                    _('File `%s\' has been removed on the disk.') % \
                            os.path.basename(doc.filepath),
                    gnome.uiconsts.MESSAGE_BOX_WARNING,
                    gnome.uiconsts.STOCK_BUTTON_OK).run_and_close()
            self.reload_notification = 1
        elif not self.reload_notification and \
             doc.filepath is not None and \
             doc.mtime < os.stat(doc.filepath)[stat.ST_MTIME]:
            # File on disk has changed
            self.reload_notification = 1
            res = gnome.ui.GnomeMessageBox(
                    _('File `%s\' has changed on the disk.\nDo you want to reload it?') % \
                            os.path.basename(doc.filepath),
                    gnome.uiconsts.MESSAGE_BOX_QUESTION,
                    gnome.uiconsts.STOCK_BUTTON_YES,
                    gnome.uiconsts.STOCK_BUTTON_NO,
                    'diff').run_and_close()
            if res == 0:          # reload
                doc.revert()
            elif res == 2:        # make diff
                # os.popen2 is available only on Python 2
                #(diff_input, diff_output) = os.popen2('diff -u - %s' % doc.filepath)
                (diff_output, diff_input) = popen2.popen2('diff -u - %s' % doc.filepath)
                diff_input.write(doc.get_text())
                diff_input.close()
                diff_text = diff_output.read()
                diff_output.close()
                self.new_document()
                doc = self.get_current_document()
                doc.set_text(diff_text)
                doc.set_language(self.langs['diff output'])
        if not in_event:
            in_event = 1
            doc.grab_focus()
