# Copyright (c) 2001 LOGILAB S.A. (Paris, FRANCE).
# http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# 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 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.

"""
Graphical View of the Recipe
"""

__revision__ = "$Id: RecipeGraphicView.py,v 1.39 2001/12/07 16:01:33 syt Exp $"

from os.path import normpath,join
from gtk import *
from GDK import *
from logilab.xmltools.XmlEditor import *
from RecipeModel import RecipeModel
from MemoryModel import MemoryModel
from RecipeController import RecipeController
from narval.lib import NO_NS, TRUE, FALSE, utf8_to_latin1,\
     select_one, select_many, SelectException
from narval.gui import unicode_to_gtk
from narval import config
#DTD =  parse_dtd_file(normpath(join(config.get_home(),'dtd/Narval.dtd')))

TARGET_STRING = 1212

target = [('STRING', 0, TARGET_STRING),
          ('text/plain', 0, TARGET_STRING)]

class RecipeGraphicView(GtkLayout):
    """
    A better layout
    """
    def __init__(self, controller):
        GtkLayout.__init__(self)
        self.add_events(BUTTON_MOTION_MASK)
        self.add_events(BUTTON1_MOTION_MASK)
        self.add_events(BUTTON_PRESS_MASK)
        self.add_events(BUTTON_RELEASE_MASK)
        # get controller
        self.controller = controller
        controller._rgview = self
        # set the recipe editable
        self.editable = TRUE
        # set size
        self.set_size(500,500)
        # drag and drop connection
        self.drag_dest_set(DEST_DEFAULT_ALL, target, GDK.ACTION_COPY)
        self.connect('drag_data_received',self.controller.received_drag_data)
        self.delta_x = 0
        self.delta_y = 0
        # gdk window used to draw connections
        self.window = None
        self.da = GtkDrawingArea()
        self.da.size(2000,2000)
        self.put(self.da,0,0)
        self.selected = None
        self.last = None
        self.first = None
        # list of graphical objects
        self._transitions = {}
        self._steps = {}
        self._connections = []
        # create a new gtkstyle for layout
        lstyle = self.get_style().copy()
        lstyle.fg[STATE_NORMAL] = self.get_colormap().alloc(int(65535),int(65535),int(65535))
        lstyle.bg[STATE_NORMAL] = self.get_colormap().alloc(int(65535),int(65535),int(65535))
        self.da.set_style(lstyle)
        # connect some events
        self.da.connect("expose_event",self._expose_event)
        self.connect("motion-notify-event",self._notify_event)
        self.connect("button-press-event",self._button_press)
        self.get_vadjustment().connect("value-changed",self._vadj_from_scroll)
        self.get_hadjustment().connect("value-changed",self._hadj_from_scroll)        
        # record pointer position
        self._pointer = (0,0)


    # connection functions ####################################################
    def remove_graphic_element(self,*args):
        if args[1].__class__ == RecipeTransition:
            self.controller.remove_transition(args[1].id)
        elif args[1].__class__ == RecipeStep:
            self.controller.remove_step(args[1].id)
        
    def edit_element_properties(self,*args):
        self.controller.element_properties(args[1])

    # Observer interface ################################################
    def RecipeModel_update(self, observable, changes):
        self._recipe_to_layout(observable._recipe)
        # build pattern of layout
        self._line,self._column = observable._layout_pattern.layout(observable._recipe)
        # force draw thanks to pattern
        for change in changes:
            if 'pattern' in change:
                self._draw_layout(self._pointer, TRUE)
            else:
                self._draw_layout(self._pointer, FALSE)
                
    def MemoryModel_update(self,observable,changes):
        for change in [ch for ch in changes if ch[0]=='change']:
            if change[1] == 'step':
                self.controller.update_step(change[2],change[3])
            elif change[1] == 'transition':
                self.controller.update_transition(change[2],change[3])

    # callbacks #########################################################

    def add_connection(self,*args):
        if args:
            self._add_graphic_connection(args[0])
        else:
            self._add_graphic_connection((self.first,self.last))
        self.first = None
        self.last = None

    def update_step(self,step):
        if step:
            if self._steps.has_key(step.getAttributeNS(NO_NS,'id')):
                self._steps[step.getAttributeNS(NO_NS,'id')].set_state(step.getAttributeNS(NO_NS,'state'))
                self.notify_observers()

    def update_transition(self,transition):
        if transition:
            if self._transitions.has_key(transition.getAttributeNS(NO_NS,'id')):
                self._transitions[transition.getAttributeNS(NO_NS,'id')].set_state(transition.getAttributeNS(NO_NS,'state'))
                self.notify_observers()

    def append_item(self, name, xml_node):
        if xml_node.localName=='step':
            item = RecipeStep(name,xml_node,self)
            if xml_node.hasAttributeNS(NO_NS,'state'):
                item.set_state(xml_node.getAttributeNS(NO_NS, 'state'))
            else:
                item.set_state('todo')
            self._add_graphic_step(item)
        elif xml_node.localName=='transition':
            item = RecipeTransition(name,xml_node,self)
            if xml_node.hasAttributeNS(NO_NS,'state'):
                item.set_state(xml_node.getAttributeNS(NO_NS, 'state'))
            else:
                item.set_state('wait-condition')
            self._add_graphic_transition(item)

        item.editable = self.editable
        item.show()

    ## Private ################################################################
    def _vadj_from_scroll(self,*args):
        self.delta_y = int(self.get_vadjustment().value)

    def _hadj_from_scroll(self,*args):
        self.delta_x = int(self.get_hadjustment().value)

    def _add_graphic_connection(self,connection):
        if connection[0].__class__ is RecipeStep\
           and connection[1].__class__ is RecipeStep:
            self.controller.add_connection_between_steps(connection[0].id,
                                                         connection[1].id)
        elif connection[0].__class__ is RecipeStep\
             and connection[1].__class__ is RecipeTransition:
            self.controller.add_connection_between_step_and_transition(
                connection[0].id,connection[1].id)
        elif connection[0].__class__ is RecipeTransition\
             and connection[1].__class__ is RecipeStep:
            self.controller.add_connection_between_transition_and_step(
                connection[0].id,connection[1].id)

    # expose event of layout
    def _expose_event(self,widget,event):
        self._draw_connections()

    # events of layout
    def _notify_event(self,*args):
        if self.selected:
            x = args[1].x-self.selected.size_request()[0]/2+self.delta_x
            y = args[1].y-self.selected.size_request()[1]/2+self.delta_y
            self.thaw()
            self.move(self.selected,x,y)
            self.selected.update_position(x,y)
        else:
            self.freeze()

    def _button_press(self,*args):
        self.selected = None
        self._pointer = (args[1].x,args[1].y)
        if args[1].button == 3 and self.editable:
            menu = GtkMenu()
            item = GtkMenuItem(unicode_to_gtk(_("Add step...")))
            item.connect("activate",self.controller.add_step)
            item.show()
            menu.append(item)
            item = GtkMenuItem(unicode_to_gtk(_("Add transition")))
            item.connect("activate",self.controller.add_transition)
            item.show()
            menu.append(item)
            menu.popup(None,None,None,3,args[1].time)

    # draw connections function
    def _draw_connections(self,*args):
        if self._connections and self.da.get_window():
            self.da.draw_rectangle(self.get_style().white_gc,TRUE,0,0,\
                                   self.da.get_window().width,\
                                   self.da.get_window().height)
            for connection in self._connections:
                if connection[0] and connection[1]:
                    if connection[2]:
                        gc = self.da.get_window().new_gc(foreground=self.get_colormap().alloc(65535,0,0))
                    else:
                        gc = self.get_style().black_gc 
                    self.da.draw_line(gc,
                                      connection[0].x_out,connection[0].y_out,
                                      connection[1].x_in,connection[1].y_in)

    # draw layout function
    def _draw_layout(self,pointer,force):
        # draw steps
        for step in self._steps.values():
            if (step.x_pos, step.y_pos) == (0,0) or force:
                x = self._column[step.id]*140-step.size_request()[0]/2
                y = self._line[step.id]*30
                if y==0:
                    x,y = pointer
                step.update_position(x,y)
        # draw transitions
        for transition in self._transitions.values():
            if (transition.x_pos,transition.y_pos) == (0,0) or force:
                if transition.id:
                    x = self._column[transition.id]*140-transition.size_request()[0]/2
                    y = self._line[transition.id]*30
                if y==0:
                    x,y = pointer
                transition.update_position(x,y)
        # draw connections
        self._draw_connections()
        

    # recipe to layout transform
    def _recipe_to_layout(self,recipe):
        element = recipe.firstChild
        tmp_step = []
        tmp_transition = []
        while element:
            if element.nodeType == element.ELEMENT_NODE :
                id = element.getAttributeNS(NO_NS,'id')
                # insert a step
                if element.localName == 'step':
                    try:
                        name = select_one(element,'al:label/text()').data
                    except SelectException:
                        name = element.getAttributeNS(NO_NS, 'target')
                        
                    if not id in map(lambda x:utf8_to_latin1(x), self._steps.keys()):
                        self.append_item(name, element)
                    else:
                        self._steps[element.getAttributeNS(NO_NS,'id')].set_name(name)
                        
                    tmp_step.append(id)
                    self._steps[id].set_state(element.getAttributeNS(NO_NS,'state'))
                # insert a transition
                elif element.localName == 'transition':
                    cond_list = select_many(element,'al:condition/al:match/text()')
                    if cond_list:
                        name = 'xxx'
                    else:
                        name = '---'
                    if not id in map(lambda x:utf8_to_latin1(x),self._transitions.keys()):
                        self.append_item(name, element)
                    else:
                        self._transitions[id].set_name(name)
                        self._transitions[id].set_conditions(element)
                        
                    tmp_transition.append(id)
                    self._transitions[id].set_state(element.getAttributeNS(NO_NS,'state'))
            element = element.nextSibling
        # check and destroy old steps or transitions
        for elt_id in map(lambda x:utf8_to_latin1(x),self._steps.keys()):
            if not elt_id in tmp_step:                
                self._remove_graphic_step(elt_id)
        for elt_id in map(lambda x:utf8_to_latin1(x),self._transitions.keys()):
            if not elt_id in tmp_transition:
                self._remove_graphic_transition(elt_id)
        # build connections
        self._connections=[]
        for t in select_many(recipe,'al:transition'):
            id = t.getAttributeNS(NO_NS,'id')
            try:
                connections = select_many(t,'al:in')
                for c in connections:
                    idr = c.getAttributeNS(NO_NS,'idref')
                    if c.getAttributeNS(NO_NS,'onError')=='yes':
                        self._connections.append((self._steps[idr], self._transitions[id], TRUE))
                    else:                                                            
                        self._connections.append((self._steps[idr], self._transitions[id], FALSE))
            except:
                pass
            try:
                connections = select_many(t,'al:out')
                for c in connections:
                    idr = c.getAttributeNS(NO_NS,'idref')
                    if c.getAttributeNS(NO_NS,'onError')=='yes':
                        self._connections.append((self._transitions[id], self._steps[idr], TRUE))
                    else:                                           
                        self._connections.append((self._transitions[id], self._steps[idr], FALSE))
            except:
                pass

    def _find_connections(self,item):
        result = []
        for connection in self._connections:
            if item in connection:
                result.append(connection)

        return result

    # step element functions
    def _add_graphic_step(self,item):
        self._steps[item.id] = item
    
    def _remove_graphic_step(self,id):
        item = self._steps[id]
        # destroy the links before
        link = self._find_connections(item)
        for connection in link:
            self._connections.remove(connection)
        # and the object
        del self._steps[id]
        item.destroy()

    # transition element functions
    def _add_graphic_transition(self,item):
        self._transitions[item.id] = item

    def _remove_graphic_transition(self,id):
        item = self._transitions[id]
        # destroy the links before
        link = self._find_connections(item)
        for connection in link:
            self._connections.remove(connection)
        # and the object
        del self._transitions[id]
        item.destroy()


# Recipe item class ###################################################
class RecipeItem(GtkButton):
    """
    Item used to store step and transition graphic pattern
    """
    def __init__(self,name,xml_node,layout):
        GtkButton.__init__(self,"")
        self.set_name(name)
        self.connect("button-press-event",self._item_pressed)
        self.connect("button-release-event",self._item_released)
        self._layout = layout
        self.id = xml_node.getAttributeNS(NO_NS, 'id')
        # in position on map
        self.x_in = 0
        self.y_in = 0
        # out position on map
        self.x_out = 0
        self.y_out = 0
        # position on map
        self.x_pos = 0
        self.y_pos = 0
        # can be editable
        self.editable = TRUE
        # put somewhere on the layout
        self._layout.thaw()
        self._layout.put(self,30,30)
        self._layout.freeze()
        from PrefModel import PrefModel
        self.pmodel = PrefModel()
        self.max_c = self.pmodel['recipe']['max-display-char']                         
    # Private #################################################################
    def _item_pressed(self,*args):
        if args[1].button == 3:
            menu = GtkMenu()
            item = GtkMenuItem(unicode_to_gtk(_("Edit properties")))
            item.show()
            item.connect("activate",self._layout.edit_element_properties,self)
            menu.append(item)
            if self.editable:
                item = GtkMenuItem(unicode_to_gtk(_("Remove")))
                item.connect("activate",self._layout.remove_graphic_element,self)
                item.show()
                menu.append(item)
            menu.popup(None,None,None,3,args[1].time)
        else:
            self._layout.selected = self

    def _item_released(self,*args):
        self._layout.selected = None
        if args[1].state & SHIFT_MASK:
            if self._layout.first and self._layout.first!=self:
                self._layout.last = self
                self._layout.get_window().set_cursor(cursor_new(68))
                self._layout.add_connection()
            else:
                self._layout.get_window().set_cursor(cursor_new(PLUS))
                self._layout.first = self

    def update_position(self,x,y):
        self.x_pos = x
        self.y_pos = y
        self._layout.thaw()
        self._layout.move(self,x, y)
        #self._layout.freeze()
        self.x_in = x + self.size_request()[0]/2
        self.y_in = y
        self.x_out = self.x_in
        self.y_out = y + self.size_request()[1]

    def set_name(self, name):
        name = utf8_to_latin1(name)
        if len(name)> 20:
            short = name[0:19]+'...'
            self.children()[0].set_text(short)
            tooltips = GtkTooltips()
            tooltips.set_tip(self,name)
        else:
            self.children()[0].set_text(name)
        

# Recipe step class ###################################################
class RecipeStep(RecipeItem):
    def __init__(self,name,xml_node,layout):
        RecipeItem.__init__(self,name,xml_node,layout)

    def set_state(self, state):
        if state != "":
            style = self.pmodel.steps_style[state]['gtkstyle']
            self.set_style(style)
            for child in self.children():
                child.set_style(style)
        

# Recipe transition class ###################################################
class RecipeTransition(RecipeItem):
    def __init__(self,name,xml_node,layout):
        RecipeItem.__init__(self,name,xml_node,layout)
        
        self.set_conditions(xml_node)

    def set_state(self, state):
        if state != '':
            style = self.pmodel.trans_style[state]['gtkstyle']
            self.set_style(style)
            for child in self.children():
                child.set_style(style)
                
    def set_conditions(self, xml_node):
        from cStringIO import StringIO
        conditions = select_many(xml_node,'al:condition')
        tips = StringIO()
        for cond in conditions:
                xpaths = select_many(cond,'al:match/text()')
                for xpath in xpaths:
                    tips.write(xpath.data+"\n")
        
        if conditions:
            tooltips = GtkTooltips()
            tooltips.set_tip(self,tips.getvalue())


    
