diff --git a/gramps/src/plugins/Graph.py b/gramps/src/plugins/Graph.py new file mode 100644 index 000000000..12e6455be --- /dev/null +++ b/gramps/src/plugins/Graph.py @@ -0,0 +1,394 @@ +# +# Graph.py - a graphical user interface for gramps +# +# Copyright (C) 2001 Jesper Zedlitz +# +# 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 +# + +"Graph/Graph" + +from RelLib import * +import os +import posixpath +import re +import sort +import string +import utils +import intl +_ = intl.gettext + +from gtk import * +from gnome.ui import * +from libglade import * + +pixmap = None +sizeX = 20 +sizeY = 20 +boxes = [] +lines = [] +treffer = -1 +distX = 0 +distY = 0 +spaceX = 10 +spaceY = 40 +popup_win = None +popped_up = FALSE +label = None +db = None +select = FALSE +old_selection= (0,0,0,0) +selected_boxes = {} +red_gc = None +lightgreen_gc = None +select_gc = None + +sorted = {} +done = {} + +# +# This function is called recursivly to determin the generation a person belongs to +# If nothing is know about the person (i.e. first call to this function) generation #0 is used. +# First the ancestors of a person are followed. If there are no more ancestors (or already visited = entry in +# map "done") the decendances (if any) are traced. +# The function returns imediately if the person has already been visited -> stop of recursion +# +def calc_gen( person, gen): + global sorted, done + + id = person.getId() + if done.has_key(id): + # stop the recursion + return + + sorted[ id ] = gen + done[ id ] = TRUE + + # going into the past... + family = person.getMainFamily() + if family != None: + father = family.getFather() + mother = family.getMother() + if( person!=father and person!=mother): + # person is a child of this family + if father != None: + calc_gen( father, gen-1 ) + if mother != None: + calc_gen( mother, gen-1 ) + # do I need getFamilyList() when I want to find the parents ? + for family in person.getFamilyList(): + father = family.getFather() + mother = family.getMother() + if( person!=father and person!=mother): + # person is a child of this family + if father != None: + calc_gen( father, gen-1 ) + if mother != None: + calc_gen( mother, gen-1 ) + + # going into the future... + for family in person.getFamilyList(): + father = family.getFather() + mother = family.getMother() + if( person==father or person==mother): + # person is a parent of this family + if person==father: + if mother != None: + calc_gen( mother, gen ) + if person==mother: + if father != None: + calc_gen( father, gen ) + for child in family.getChildList(): + calc_gen( child, gen+1 ) + + +def report(database,person): + global sorted, done + sorted = {} + done = {} + global db + db = database + global boxes + boxes = [] + global lines + lines = [] + winSizeX = 400 + winSizeY = 400 + global selected_boxes + selected_boxes = {} + + personList = database.getPersonMap() + + # loop over all persons in the database + # usually a lot of persons will the in "done" after the first call of "calc_gen", but if there are + # disjunct parts in the database (or completely seperated people) we have to check everyone + for id in personList.keys(): + if not done.has_key( id ): + calc_gen( personList[id], 0) + + # don't want to have negative generation numbers, so have to substract the lowest (negative) number + mini = min( sorted.values() ) + + # position the boxes + # this can be done much better - i.e. children should be put close to their parents... + length = {} + for id in sorted.keys(): + y = ( sorted[id]-mini) * (sizeY +spaceY) + 10 + x = length.get( sorted[id]-mini, 0 ) + spaceX + length[ sorted[id]-mini ] = x + sizeX + if personList.has_key( id ): + pos = personList[id].getPosition() + if pos != None: + boxes.append( (pos[0], pos[1], (personList[id].getGender() == Person.female), id ) ) + else: + boxes.append( (x, y, (personList[id].getGender() == Person.female), id ) ) + personList[id].setPosition( (x,y) ) + utils.modified() + else: + print "just lost person with key %s" % (id) + + # add lines between children and parents + for i in range( len(boxes) ): + b = boxes[i] + id = b[3] + person = personList[id] + family = person.getMainFamily() + if family != None: + father = family.getFather() + f="" + m="" + if father!=None: + f = father.getId() + mother = family.getMother() + if mother!=None: + m = mother.getId() + for j in range( len(boxes) ): + # does this box contain the id of the father or the mother? + if boxes[j][3] == f or boxes[j][3] == m: + lines.append( (j, i) ) + + # logic is done - now the graphic + + win = GtkWindow() + win.set_name("Test Input") + win.set_border_width(5) + + vbox = GtkVBox(spacing=3) + win.add(vbox) + vbox.show() + + drawing_area = GtkDrawingArea() + drawing_area.size(winSizeX, winSizeY) + vbox.pack_start(drawing_area) + drawing_area.show() + + drawing_area.connect("expose_event", expose_event) + drawing_area.connect("configure_event", configure_event) + drawing_area.connect("button_press_event", button_press_event) + drawing_area.connect("button_release_event", button_release_event) + drawing_area.connect("motion_notify_event", motion_notify_event) + drawing_area.set_events(GDK.EXPOSURE_MASK | + GDK.LEAVE_NOTIFY_MASK | + GDK.BUTTON_PRESS_MASK | + GDK.BUTTON_RELEASE_MASK | + GDK.POINTER_MOTION_MASK | + GDK.POINTER_MOTION_HINT_MASK) + + button = GtkButton("Quit") + hbox = GtkHBox(spacing=3) + vbox.pack_start(button, expand=FALSE, fill=FALSE) + button.connect("clicked", win.destroy) + button.show() + win.show() + drawing_area.get_window().set_cursor(cursor_new(132)) + +def redraw_tree( widget, area = None ): + global pixmap + global red_gc, select_gc, lightgreen_gc + if area == None: + draw_rectangle(pixmap, widget.get_style().white_gc, TRUE, 0, 0, widget.get_window().width, widget.get_window().height) + else: + draw_rectangle(pixmap, widget.get_style().white_gc, TRUE, area[0], area[1], area[2], area[3]) + + for i in range( len( boxes) ): + b = boxes[i] + if selected_boxes.has_key(i): + draw_rectangle(pixmap, lightgreen_gc, TRUE, b[0], b[1], sizeX, sizeY) + draw_rectangle(pixmap, widget.get_style().black_gc, FALSE, b[0], b[1], sizeX, sizeY) + + for l in lines: + p1 = boxes[l[0]] + p2 = boxes[l[1]] + draw_line( pixmap, red_gc, p1[0]+sizeX/2, p1[1]+sizeY, p2[0]+sizeX/2, p2[1] ) + draw_rectangle(pixmap, select_gc, FALSE, old_selection[0], old_selection[1], old_selection[2], old_selection[3]) + widget.queue_draw() + +def configure_event(widget, event): + global pixmap + global red_gc, select_gc, lightgreen_gc + win = widget.get_window() + pixmap = create_pixmap(win, win.width, win.height, -1) + + cm = widget.get_style().colormap + if( red_gc == None ): + red_gc = win.new_gc() + red_gc.foreground = cm.alloc( 60000,0,0) + if( lightgreen_gc == None ): + lightgreen_gc = win.new_gc() + lightgreen_gc.foreground = cm.alloc( 0,60000,0) + if( select_gc == None ): + select_gc = win.new_gc() + select_gc.foreground = cm.alloc( 10000,10000,10000) + select_gc.line_style = 1 + + redraw_tree( widget ) + return TRUE + +def expose_event(widget, event): + area = event.area + gc = widget.get_style().fg_gc[STATE_NORMAL] + widget.draw_pixmap(gc, pixmap, area[0], area[1], area[0], area[1], + area[2], area[3]) + return FALSE + +# +# close the popup-window for one person +# it's not nice that the window closes imediately after leaving the window - a timer should be added here +# +def popdown_cb(widget, event): + global popped_up + global popup_win + widget.hide() + popped_up = FALSE + return FALSE + +def button_press_event(widget, event): + global treffer + global distY, distX + global popped_up, popup_win, label + global selX, selY, select, old_selection, selected_boxes + state = event.window.pointer_state + + # which box has been hit? + i = 0 + for b in boxes: + if( b[0]<=event.x<=b[0]+sizeX and b[1]<=event.y<=b[1]+sizeY): + treffer=i + distX = event.x - b[0] + distY = event.y - b[1] + i=i+1 + + # left mouse button -> drag 'n drop + if (treffer > -1) and (state & GDK.BUTTON1_MASK): + if not selected_boxes.has_key(treffer): + old_selection = (0,0,0,0) + selected_boxes = { } + selected_boxes[treffer] = TRUE + widget.get_window().set_cursor(cursor_new(58)) + + # add a box to the current selection + # this should be changes to work with BUTTON1 + CTRL + if treffer>-1 and (state & GDK.BUTTON2_MASK): + if not selected_boxes.has_key(treffer): + old_selection = (0,0,0,0) + selected_boxes[treffer] = TRUE + treffer = -1 + + # left mouse button outside a box -> selection + if treffer==-1 and (state & GDK.BUTTON1_MASK): + widget.get_window().set_cursor(cursor_new(30)) + select = TRUE + old_selection = ( event.x, event.y, 0,0) + + # right mouse button -> infobox + if (treffer > -1) and (state & GDK.BUTTON3_MASK) : + if not popped_up: + if not popup_win: + popup_win = GtkWindow(WINDOW_POPUP) + popup_win.set_position(WIN_POS_MOUSE) + popup_win.set_border_width(5) + label = GtkLabel("%s"% boxes[treffer][3]) + label.show() + popup_win.add(label) + popup_win.connect("leave_notify_event", popdown_cb) + person = db.findPersonNoMap(boxes[treffer][3]) + label.set_text( person.getPrimaryName().getName()) + popup_win.show() + popped_up = TRUE + treffer=-1 + redraw_tree( widget ) + return TRUE + +# +# returns a map of numbers +# boxes is the map of all boxes - I could have used the global variable here... +# "old_selection" has the following form: ( position x, position y, width, height ) +# +def boxes_in_selection( boxes, old_selection ): + global sizeY, sizeX + res = {} + for i in range( len(boxes) ): + b = boxes[i] + if( old_selection[0]<=b[0]<=old_selection[0]+old_selection[2]-sizeX and old_selection[1]<=b[1]<=old_selection[1]+old_selection[3]-sizeY): + res[i] = TRUE + return res + +def button_release_event(widget, event): + global treffer + global distY, distX + global sizeY, sizeX + global selX, selY, select, old_selection, selected_boxes + state = event.state + + # draw a selection + if select and (state & GDK.BUTTON1_MASK): + select = FALSE + widget.get_window().set_cursor(cursor_new(132)) + selected_boxes = boxes_in_selection(boxes, old_selection) + old_selection = (0,0,0,0) + + # drag n' drop + if (treffer > -1) and (state & GDK.BUTTON1_MASK): + treffer=-1 + widget.get_window().set_cursor(cursor_new(132)) + for k in selected_boxes.keys(): + b = boxes[k] + id = b[3] + person = db.findPersonNoMap(id) + person.setPosition( (b[0], b[1]) ) + utils.modified() + redraw_tree( widget ) + return TRUE + +def motion_notify_event(widget, event): + global old_selection, selected_boxes, treffer + global distY, distX + state = event.window.pointer_state + if select and (state & GDK.BUTTON1_MASK): + old_selection = ( old_selection[0], old_selection[1], event.x-old_selection[0], event.y-old_selection[1] ) + selected_boxes = boxes_in_selection(boxes, old_selection) + redraw_tree( widget) + + if (treffer > -1) and (state & GDK.BUTTON1_MASK): + posX = event.x - distX + posY = event.y - distY + distance = ( posX-boxes[treffer][0], posY-boxes[treffer][1] ) + old_selection = (0,0,0,0) + for k in selected_boxes.keys(): + b = boxes[k] + boxes.pop(k) + boxes.insert( k, (b[0]+distance[0], b[1]+distance[1], b[2], b[3]) ) + redraw_tree( widget) + return TRUE