## Filename    : chart_view.py
## Author(s)   : Michel Le Borgne
## Created     : 4/3/2010
## Revision    :
## Source      :
##
## Copyright 2009 - 2010 - 2020 IRISA/IRSET
##
## This library 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.1 of the License, or
## any later version.
##
## This library 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.  The software and
## documentation provided hereunder is on an "as is" basis, and IRISA has
## no obligations to provide maintenance, support, updates, enhancements
## or modifications.
## In no event shall IRISA be liable to any party for direct, indirect,
## special, incidental or consequential damages, including lost profits,
## arising out of the use of this software and its documentation, even if
## IRISA have been advised of the possibility of such damage.  See
## the GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this library; if not, write to the Free Software Foundation,
## Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA.
##
## The original code contained here was initially developed by:
##
##     Michel Le Borgne.
##     IRISA
##     Symbiose team
##     IRISA  Campus de Beaulieu
##     35042 RENNES Cedex, FRANCE
##
##
## Contributor(s): Geoffroy Andrieux, Nolwenn Le Meur
##
"""
Main views for the Cadbiom gui
Classes available and inheritance hierarchy:
    gtk.DrawingArea:
        :class:`ChartView`: Used by the graph editor window/page of charter
            :class:`NavView`: overview widget
    gtk.Frame:
        :class:`ChartPage`: Graph editor window/page of charter
"""
import gtk
[docs]class ChartView(gtk.DrawingArea):
    """Class for the main drawing window of charter
    .. note:: Also instanciated by the overview widget.
    .. note:: DrawingArea can capture key-press-events:
        - add gtk.gdk.KEY_PRESS_MASK mask to event
        - be sure that the DrawingArea has the focus by using grab_focus() on
          mouse events.
    .. TODO:: Optimize events handling:
        - on_motion_notify: Mouse over the widget, causing multiple calls to
          find_element() of the top node.
        - on_expose_event: When the window is covered/partially covered;
          all the pixmap is redrawn.
    """
    def __init__(self, width, height, drawing_style, controler, model):
        """
        @param width, height: int - size of the view
        @param drawing_style: a Drawing style associated with the pattern
        @param controler: the controller as in the MVC pattern
            NavControler or ChartControler objects
        @param model: a ChartModel
        """
        gtk.DrawingArea.__init__(self)
        self.controler = controler
        controler.set_view(self)
        self.model = model
        # attach as model observer
        model.attach(self)
        # signals
        self.add_events(
            gtk.gdk.EXPOSURE_MASK
            | gtk.gdk.LEAVE_NOTIFY_MASK
            | gtk.gdk.BUTTON_PRESS_MASK
            | gtk.gdk._2BUTTON_PRESS
            | gtk.gdk.BUTTON_RELEASE_MASK
            | gtk.gdk.POINTER_MOTION_MASK
            | gtk.gdk.POINTER_MOTION_HINT_MASK
        )
        # Get the focus
        self.set_flags(gtk.HAS_FOCUS | gtk.CAN_FOCUS)
        self.grab_focus()
        self.connect("expose-event", self.on_expose_event)
        self.connect("configure_event", self.on_configure_event)
        # controlled events
        self.connect("button_press_event", self.on_button_press)
        self.connect("button_release_event", controler.on_button_release)
        self.connect("motion_notify_event", controler.on_motion_notify)
        # drawing stuffs
        drawing_style.view = self
        self.drawing_style = drawing_style
        controler.drawing_style = drawing_style
        self.draw_width = width  # drawing window
        self.min_width = width
        self.draw_height = height
        self.min_height = height
        self.set_size_request(width, height)
        self.stylepango = self.create_pango_layout("")
        self.pixmap = None  # cannot be initialized before self.window creation
        self.page = None
        self.zoom_count = 0
[docs]    def on_expose_event(self, widget, event):
        """
        Redisplay drawing after cover and exposure of the window
        """
        self.pixmap.draw_rectangle(
            self.get_style().white_gc, True, 0, 0, self.draw_width, self.draw_height
        )
        # draw the full model from its top node in the pixmap
        self.model.draw(self)
        # expose the drawing
        self.window.draw_drawable(self.get_style().fg_gc[gtk.STATE_NORMAL],
                                self.pixmap, 0, 0, 0, 0,
                                self.draw_width, self.draw_height)
        if self.page:
            self.page.notify(widget)
        return True 
[docs]    def update(self):
        """Display a new drawing of the model
        Notified by the model when changes have been made (move/delete/add item)
        Or by the ChartControler when a transition/node is selected.
        """
        # update only visible views
        if not self.window:
            return
        # resize if virtual drawing window size changed
        size = self.pixmap.get_size()
        if size[0] != self.draw_width or size[1] != self.draw_height:
            self.set_size_request(self.draw_width, self.draw_height)
            self.pixmap = gtk.gdk.Pixmap(self.window, self.draw_width, self.draw_height)
        self.pixmap.draw_rectangle(
            self.get_style().white_gc, True, 0, 0, self.draw_width, self.draw_height
        )
        # draw in the pixmap
        self.model.draw(self)
        # expose the drawing
        self.window.draw_drawable(self.get_style().fg_gc[gtk.STATE_NORMAL],
                                self.pixmap, 0, 0, 0, 0,
                                self.draw_width, self.draw_height) 
[docs]    def zoom_minus(self):
        """
        Zoom out function
        """
        if self.zoom_count < -10:
            return
        self.min_height = int(self.min_height * 0.9)
        self.draw_height = self.min_height
        self.min_width = int(self.min_width * 0.9)
        self.draw_width = self.min_width
        self.update()
        if self.page:
            self.page.notify(None)
        self.zoom_count = self.zoom_count - 1 
[docs]    def zoom_plus(self):
        """
        Zoom in function
        """
        if self.zoom_count > 15:
            return
        self.min_height = int(self.min_height * 1.1)
        self.draw_height = self.min_height
        self.min_width = int(self.min_width * 1.1)
        self.draw_width = self.min_width
        self.update()
        if self.page:
            self.page.notify(None)
        self.zoom_count += 1  
[docs]class NavView(ChartView):
    """Specialized ChartView for navigator (used by the overview widget)"""
    def __init__(self, width, height, drst, controler, model):
        """
        same parameters than in upper class constructor
        """
        ChartView.__init__(self, width, height, drst, controler, model)
        self.x_ret = 0
        self.y_ret = 0
        self.w_ret = 1
        self.h_ret = 1
        self.show_all()
[docs]    def on_expose_event(self, widget, event):
        """
        Redisplay drawing after cover and exposure of the window
        """
        self.pixmap.draw_rectangle(
            self.get_style().white_gc, True, 0, 0, self.draw_width, self.draw_height
        )
        # draw in the pixmap
        self.model.draw(self)
        # expose the drawing
        self.window.draw_drawable(self.get_style().fg_gc[gtk.STATE_NORMAL],
                                self.pixmap, 0, 0, 0, 0,
                                self.draw_width, self.draw_height)
        # draw reticule
        self.window.draw_rectangle(
            self.ret_gc, False, self.x_ret, self.y_ret, self.w_ret, self.h_ret
        )
        return True 
[docs]    def update(self):
        """Display a new drawing of the model
        Notified by the model when changes have been made (move/delete/add item)
        """
        # update only visible views
        if not self.window:
            return
        # hack for notebook
        if not self.pixmap:
            return
        # draw in the pixmap
        self.pixmap.draw_rectangle(
            self.get_style().white_gc, True, 0, 0, self.draw_width, self.draw_height
        )
        self.model.draw(self)
        # expose the drawing
        self.window.draw_drawable(self.get_style().fg_gc[gtk.STATE_NORMAL],
                                self.pixmap, 0, 0, 0, 0,
                                self.draw_width, self.draw_height)
        # draw reticule
        self.window.draw_rectangle(
            self.ret_gc, False, self.x_ret, self.y_ret, self.w_ret, self.h_ret
        ) 
[docs]    def clear(self):
        """
        As it says
        """
        self.window.draw_rectangle(
            self.get_style().white_gc, True, 0, 0, self.draw_width, self.draw_height
        )  
[docs]class ChartPage(gtk.Frame):
    """
    A simple graph editor for pages: a graph area wrapped in a scroll window
    """
    def __init__(self, width, height, drst, controler, model):
        """
        """
        gtk.Frame.__init__(self)
        # for observers
        self.__observer_list = []
        # chart view and model
        self.draw = ChartView(width, height, drst, controler, model)
        self.draw.page = self
        # wrap in a scroll window
        scr_w = gtk.ScrolledWindow()
        self.scr_w = scr_w
        self.vadj = scr_w.get_vadjustment()
        self.hadj = scr_w.get_hadjustment()
        self.hadj_h_id = self.hadj.connect("value_changed", self.notify)
        self.vadj_h_id = self.vadj.connect("value_changed", self.notify)
        self.allow_adj = True
        scr_w.set_policy(gtk.POLICY_ALWAYS, gtk.POLICY_ALWAYS)
        viewport = gtk.Viewport()
        scr_w.add(viewport)
        viewport.add(self.draw)
        self.add(scr_w)
        self.show_all()
    # observer pattern methods
[docs]    def attach(self, obs):
        """
        standard in observer pattern
        """
        if not obs in self.__observer_list:
            self.__observer_list.append(obs) 
[docs]    def detach(self, obs):
        """
        standard in observer pattern
        """
        self.__observer_list.remove(obs) 
[docs]    def notify(self, widget):
        """
        used when the viewport is moved from inside (hbar and vbar)
        """
        if self.allow_adj:
            x_pos = float(self.hadj.value) / self.draw.draw_width
            y_pos = float(self.vadj.value) / self.draw.draw_height
            width = self.hadj.page_size / self.draw.draw_width
            height = self.vadj.page_size / self.draw.draw_height
            for obs in self.__observer_list:
                obs.scroll_update(x_pos, y_pos, width, height) 
    # signal call back
[docs]    def update(self, dxx, dyy):
        """
        Used when the viewport is moved from outside (navigator)
        """
        d_hadj = dxx * self.draw.draw_width
        d_vadj = dyy * self.draw.draw_height
        self.allow_adj = False
        self.hadj.value = int(self.hadj.value + d_hadj)
        self.vadj.value = int(self.vadj.value + d_vadj)
        self.scr_w.set_hadjustment(self.hadj)
        self.scr_w.set_vadjustment(self.vadj)
        self.allow_adj = True
        self.notify(None)