# -*- coding: utf-8 -*-
# vi:si:et:sw=4:sts=4:ts=4
##
## Copyright (C) 2007 Async Open Source
##
## This program is free software; you can redistribute it and/or
## modify it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
##
## You should have received a copy of the GNU Lesser General Public License
## along with this program; if not, write to the Free Software
## Foundation, Inc., or visit: http://www.gnu.org/.
##
##
## Author(s): Stoq Team <stoq-devel@async.com.br>
##
# TODO:
#  Focus could be improved, arrows shouldn't focus other widgets
#  Column/List field
"""Widget containing a Grid of fields
"""
import pickle
import gobject
import glib
import pango
import gtk
from gtk import gdk
from gtk import keysyms
from kiwi.python import clamp
from kiwi.utils import gsignal
(FIELD_NONE,
 FIELD_MOVE,
 FIELD_RESIZE) = range(3)
# Bindings
(FIELD_MOVEMENT_HORIZONTAL,
 FIELD_MOVEMENT_VERTICAL,
 FIELD_DELETION) = range(3)
_cursors = {}
def _get_cursor(gdk_pos):
    # Do a lazy initialization of those gdk.Cursor objects
    # If we initialize them too early (e.g. in the module) they would
    # break Stoq running on non graphical environments.
    c = _cursors.get(gdk_pos, None)
    if c is not None:
        return c
    return _cursors.setdefault(gdk_pos, gdk.Cursor(gdk_pos))
[docs]class Range(object):
    def __init__(self, start, end):
        self.start = start
        self.end = end
    def __contains__(self, x):
        return self.start <= x <= self.end 
[docs]class FieldInfo(object):
    def __init__(self, grid, name, widget, x, y, width=-1, height=1, model=None):
        if width == -1:
            width = len(name)
        self.grid = grid
        self.name = name
        self.widget = widget
        self.x = x
        self.y = y
        self.width = width
        self.height = height
        self.model = model
[docs]    def update_label(self, text):
        fmt = '<span letter_spacing="3072">%s</span>'
        self.widget.set_markup(fmt % (glib.markup_escape_text(text), )) 
[docs]    def allocate(self, width, height):
        req_width, req_height = self.widget.size_request()
        self.widget.size_allocate(((self.x * width) - 1,
                                   (self.y * height) - 1,
                                   (self.width * width) + 2,
                                   (self.height * height) + 3)) 
[docs]    def find_at(self, x, y):
        wx, wy, ww, wh = self.widget.allocation
        return (x in Range(wx, wx + ww) and
                y in Range(wy, wy + wh)) 
[docs]    def show(self):
        self.widget.show() 
[docs]    def get_cursor(self, x, y):
        a = self.widget.allocation
        cx = a.x + 1
        cy = a.y + 1
        cw = a.width
        ch = a.height
        intop = y in Range(cy - 1, cy + 1)
        inbottom = y in Range(cy + ch - 3, cy + ch)
        if x in Range(cx - 1, cx + 1):
            if intop:
                return  # _get_cursor(gdk.TOP_LEFT_CORNER)
            elif inbottom:
                return  # _get_cursor(gdk.BOTTOM_LEFT_CORNER)
            else:
                return  # _get_cursor(gdk.LEFT_SIDE)
        elif x in Range(cx + cw - 2, cx + cw + 1):
            if intop:
                return  # _get_cursor(gdk.TOP_RIGHT_CORNER)
            elif inbottom:
                return _get_cursor(gdk.BOTTOM_RIGHT_CORNER)
            else:
                return _get_cursor(gdk.RIGHT_SIDE)
        elif intop:
            return  # _get_cursor(gdk.TOP_SIDE)
        elif inbottom:
            return _get_cursor(gdk.BOTTOM_SIDE)  
[docs]class FieldGrid(gtk.Layout):
    """FieldGrid is a Grid like widget which you can add fields to
    * **field-added** (object): Emitted when a field is added to the grid
    * **field-removed** (object): Emitted when a field is removed
      from the grid
    * ** selection-changed** (object): Emitted when a field is selected or
      deselected by the user.
    """
    gsignal('selection-changed', object,
            flags=gobject.SIGNAL_RUN_LAST | gobject.SIGNAL_ACTION)
    gsignal('field-added', object)
    gsignal('field-removed', object)
    def __init__(self, font, width, height):
        gtk.Layout.__init__(self)
        self.set_can_focus(True)
        self.drag_dest_set(
            gtk.DEST_DEFAULT_ALL,
            [('OBJECTLIST_ROW', 0, 10),
             ('text/uri-list', 0, 11),
             ('_NETSCAPE_URL', 0, 12)],
            gdk.ACTION_LINK | gdk.ACTION_COPY | gdk.ACTION_MOVE)
        self.font = pango.FontDescription(font)
        self.width = width
        self.height = height
        self._fields = []
        self._moving_field = None
        self._moving_start_x_pointer = 0
        self._moving_start_y_pointer = 0
        self._moving_start_x_position = 0
        self._moving_start_y_position = 0
        self._action_type = FIELD_NONE
        self._selected_field = None
        self._draw_grid = True
        TEXT = '1234567890ABCDEFTI'
        self._layout = self.create_pango_layout(TEXT)
        self._layout.set_font_description(self.font)
        self._field_width = (self._layout.get_pixel_size()[0] / len(TEXT)) + 2
        self._field_height = self._layout.get_pixel_size()[1] + 2
    #
    # Private API
    #
    def _pick_field(self, window_x, window_y):
        for field in self._fields:
            if field.find_at(window_x, window_y):
                return field
        return None
    def _remove_selected_field(self):
        field = self._selected_field
        if field:
            self._remove_field(field)
    def _remove_field(self, field):
        if field == self._selected_field:
            self.select_field(None)
        self._fields.remove(field)
        self.remove(field.widget)
        self.emit('field-removed', field)
    def _add_field(self, name, description, x, y, width=-1, height=1, model=None):
        label = gtk.Label()
        label.set_alignment(0, 0)
        label.set_padding(2, 4)
        if not description:
            description = name
        label.modify_font(self.font)
        field = FieldInfo(self, name, label, x, y, width, height, model)
        field.update_label(description)
        self._fields.append(field)
        self.emit('field-added', field)
        label.connect('size-allocate',
                      self._on_field__size_allocate, field)
        self.put(label, -1, -1)
        return field
    def _set_field_position(self, field, x, y):
        x = clamp(x, 0, self.width - field.width - 1)
        y = clamp(y, 0, self.height - field.height - 1)
        if field.x == x and field.y == y:
            return
        field.x, field.y = x, y
        if field.widget.props.visible:
            self.queue_resize()
        self.emit('selection-changed', field)
    def _resize_field(self, field, width, height):
        width = clamp(width, 1, self.width - field.x - 1)
        height = clamp(height, 1, self.height - field.y - 1)
        if field.width == width and field.height == height:
            return
        field.width, field.height = width, height
        if field.widget.props.visible:
            self.queue_resize()
        self.emit('selection-changed', field)
    def _get_field_from_widget(self, widget):
        for field in self._fields:
            if field.widget == widget:
                return field
        else:
            raise AssertionError
    def _begin_move_field(self, field, x, y, time):
        if self._moving_field is not None:
            raise AssertionError("can't move two fields at once")
        mask = (gdk.BUTTON_RELEASE_MASK | gdk.BUTTON_RELEASE_MASK |
                gdk.POINTER_MOTION_MASK)
        grab = gdk.pointer_grab(self.window, False, mask, None, None,
                                long(time))
        if grab != gdk.GRAB_SUCCESS:
            raise AssertionError("grab failed")
        self._moving_field = field
        self._moving_start_x_pointer = x
        self._moving_start_y_pointer = y
        self._moving_start_x_position = field.x
        self._moving_start_y_position = field.y
        self._moving_start_width = field.width
        self._moving_start_height = field.height
        w, h = field.widget.get_size_request()
        self._moving_start_w, self._moving_start_h = w, h
    def _update_move_field(self, x, y):
        field = self._moving_field
        if not field:
            return
        if self._action_type == FIELD_MOVE:
            dx, dy = self._get_coords(
                x - self._moving_start_x_pointer,
                y - self._moving_start_y_pointer)
            self._set_field_position(field,
                                     self._moving_start_x_position + dx,
                                     self._moving_start_y_position + dy)
        elif self._action_type == FIELD_RESIZE:
            dx, dy = self._get_coords(
                x - self._moving_start_x_pointer,
                y - self._moving_start_y_pointer)
            self._resize_field(field,
                               self._moving_start_width + dx,
                               self._moving_start_height + dy)
    def _end_move_field(self, time):
        if not self._moving_field:
            return
        gdk.pointer_ungrab(long(time))
        self._moving_field = None
    def _get_coords(self, x, y):
        """Returns the grid coordinates given absolute coordinates
        :param x: absolute x
        :param y: absoluyte y
        :returns: (gridx, gridy)
        """
        return (int(float(x) / (self._field_width + 1)),
                int(float(y) / (self._field_height + 1)))
    def _move_field(self, movement_type, delta):
        field = self._selected_field
        if not field:
            return True
        x = field.x
        y = field.y
        if movement_type == FIELD_MOVEMENT_VERTICAL:
            y += delta
        elif movement_type == FIELD_MOVEMENT_HORIZONTAL:
            x += delta
        else:
            raise AssertionError
        self._set_field_position(field, x, y)
    def _on_field__size_allocate(self, label, event, field):
        field.allocate(self._field_width + 1, self._field_height + 1)
    #
    # GtkWidget
    #
[docs]    def do_realize(self):
        gtk.Layout.do_realize(self)
        # Use the same gdk.window (from gtk.Layout) to capture these events.
        self.window.set_events(self.get_events() |
                               gdk.BUTTON_PRESS_MASK |
                               gdk.BUTTON_RELEASE_MASK |
                               gdk.KEY_PRESS_MASK |
                               gdk.KEY_RELEASE_MASK |
                               gdk.ENTER_NOTIFY_MASK |
                               gdk.LEAVE_NOTIFY_MASK |
                               gdk.POINTER_MOTION_MASK)
        self.modify_bg(gtk.STATE_NORMAL, gdk.color_parse('white'))
        gc = gdk.GC(self.window,
                    line_style=gdk.LINE_ON_OFF_DASH,
                    line_width=2)
        gc.set_rgb_fg_color(gdk.color_parse('blue'))
        self._selection_gc = gc
        gc = gdk.GC(self.window)
        gc.set_rgb_fg_color(gdk.color_parse('grey80'))
        self._grid_gc = gc
        gc = gdk.GC(self.window)
        gc.set_rgb_fg_color(gdk.color_parse('black'))
        self._border_gc = gc
        gc = gdk.GC(self.window)
        gc.set_rgb_fg_color(gdk.color_parse('grey40'))
        self._field_border_gc = gc 
[docs]    def do_size_request(self, req):
        border_width = 1
        req.width = self.width * (self._field_width + border_width) + border_width
        req.height = self.height * (self._field_height + border_width) + border_width 
[docs]    def do_expose_event(self, event):
        window = event.window
        if not self.get_realized():
            return
        for c in self._fields:
            self.propagate_expose(c.widget, event)
        fw = self._field_width + 1
        fh = self._field_height + 1
        width = (self.width * fw) - 1
        height = (self.height * fh) - 1
        window.draw_rectangle(self._border_gc, False, 0, 0,
                              width + 1, height + 1)
        if self._draw_grid:
            grid_gc = self._grid_gc
            for x in range(self.width):
                window.draw_line(grid_gc,
                                 x * fw, 0,
                                 x * fw, height)
            for y in range(self.height):
                window.draw_line(grid_gc,
                                 0, y * fh,
                                 width, y * fh)
        fields = self._fields[:]
        if self._selected_field:
            gc = self._selection_gc
            field = self._selected_field
            cx, cy, cw, ch = field.widget.allocation
            window.draw_rectangle(gc, False,
                                  cx + 1, cy + 1, cw - 2, ch - 2)
            fields.remove(field)
        gc = self._field_border_gc
        for field in fields:
            cx, cy, cw, ch = field.widget.allocation
            window.draw_rectangle(gc, False,
                                  cx + 1, cy + 1, cw - 2, ch - 3) 
[docs]    def do_motion_notify_event(self, event):
        if self._moving_field is not None:
            self._update_move_field(int(event.x), int(event.y))
        else:
            field = self._pick_field(event.x, event.y)
            cursor = None
            if field:
                cursor = field.get_cursor(event.x, event.y)
            self.window.set_cursor(cursor) 
[docs]    def do_key_press_event(self, event):
        if self._moving_field:
            return
        if event.keyval == keysyms.Up:
            self._move_field(FIELD_MOVEMENT_VERTICAL, -1)
        elif event.keyval == keysyms.Down:
            self._move_field(FIELD_MOVEMENT_VERTICAL, 1)
        elif event.keyval == keysyms.Left:
            self._move_field(FIELD_MOVEMENT_HORIZONTAL, -1)
        elif event.keyval == keysyms.Right:
            self._move_field(FIELD_MOVEMENT_HORIZONTAL, 1)
        elif event.keyval == keysyms.Delete:
            self._remove_selected_field()
        if gtk.Layout.do_key_press_event(self, event):
            return True
        return True 
[docs]    def do_drag_drop(self, context, x, y, time):
        return True 
    # pylint: disable=E1120
[docs]    def do_drag_data_received(self, context, x, y, data, info, time):
        if data.type == 'OBJECTLIST_ROW':
            row = pickle.loads(data.data)
            x, y = self._get_coords(x, y)
            if self.objectlist_dnd_handler(row, x, y):
                context.finish(True, False, time)
        elif data.type == '_NETSCAPE_URL':
            d = data.data.split('\n')[1]
            d = d.replace('&', '&')
            x, y = self._get_coords(x, y)
            field = self.add_field(d, x, y)
            field.show()
            self.select_field(field)
            context.finish(True, False, time)
        context.finish(False, False, time) 
    # pylint: enable=E1120
[docs]    def do_focus(self, direction):
        self.set_can_focus(False)
        res = gtk.Layout.do_focus(self, direction)
        self.set_can_focus(True)
        return res 
    #
    # Public API
    #
[docs]    def add_field(self, text, description, x, y, width=-1, height=1, model=None):
        """Adds a new field to the grid
        :param text: text of the field
        :param description: description of the field
        :param x: x position of the field
        :param y: y position of the field
        """
        return self._add_field(text, description, x, y, width, height, model) 
[docs]    def select_field(self, field):
        """Selects a field
        :param field: the field to select, must be FieldInfo or None
        """
        if field == self._selected_field:
            return
        self._selected_field = field
        self.queue_resize()
        self.grab_focus()
        self.emit('selection-changed', field) 
[docs]    def get_selected_field(self):
        """ Returns the currently selected field
        :returns: the currently selected field
        :rtype: FieldInfo
        """
        return self._selected_field 
[docs]    def get_fields(self):
        """ Returns a list of fields in the grid
        :returns: a list of fields in the grid
        """
        return self._fields 
[docs]    def objectlist_dnd_handler(self, item, x, y):
        """A subclass can implement this to support dnd from
        an ObjectList.
        :param item: the row dragged from the objectlist
        :param x: the x position it was dragged to
        :param y: the y position it was dragged to
        """
        return False 
[docs]    def resize(self, width, height):
        """
        Resize the grid.
        :param width: the new width
        :param height: the new height
        """
        self.width = width
        self.height = height
        self.queue_resize()  
gobject.type_register(FieldGrid)