#!/usr/bin/env python # -*- coding: utf-8 -*- # Base import import logging from gettext import gettext as _ # Sugar import from sugar3.activity import activity from sugar3.presence import presenceservice from sugar3.graphics.toolbarbox import ToolbarBox from sugar3.bundle.activitybundle import ActivityBundle from sugar3.activity.widgets import ActivityToolbarButton from sugar3.activity.widgets import StopButton from sugar3.graphics.toolbutton import ToolButton from sugar3.graphics.toolbarbox import ToolbarButton import gi gi.require_version('Gtk', '3.0') from gi.repository import GObject from gi.repository import Gdk from gi.repository import GdkPixbuf from gi.repository import Gtk from gi.repository import Pango from gi.repository import PangoCairo from urlparse import urlparse from vboxcolor import VBoxColor # Local import from src.person import Person from src.union import Union from src.tree import Tree from src.tree import empty_tree, sample_family1, sample_family2 import src.const as const # Init position const.tree_initx = (875 - const._person_width) / 2 const.tree_inity = (780 - const._person_height) / 2 const.bg_color = (0.7, 0.7, 0.7) # Init log _logger = logging.getLogger('roots-activity') _logger.setLevel(logging.DEBUG) _consolehandler = logging.StreamHandler() _consolehandler.setLevel(logging.DEBUG) _consolehandler.setFormatter(logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")) _logger.addHandler(_consolehandler) # Init presence presenceService = presenceservice.get_instance() buddyName = presenceService.get_owner().props.nick # Activity class class RootsActivity(activity.Activity): def __init__(self, handle): "Set up the activity." # Sugar init activity.Activity.__init__(self, handle) # Until we support it, turn off sharing self.max_participants = 1 # Create the toolbars toolbox = ToolbarBox() activity_button = ActivityToolbarButton(self) toolbox.toolbar.insert(activity_button, 0) activity_button.show() toolbarview = Gtk.Toolbar() view_toolbar_button = ToolbarButton( label=_('View'), page=toolbarview, icon_name='toolbar-view') toolbox.toolbar.insert(view_toolbar_button, -1) toolbarsample = Gtk.Toolbar() sample_toolbar_button = ToolbarButton( label=_('Samples'), page=toolbarsample, icon_name='sample1') toolbox.toolbar.insert(sample_toolbar_button, -1) toolbarentry = Gtk.Toolbar() self.entry_toolbar_button = ToolbarButton( label=_('Entry'), page=toolbarentry, icon_name='cell-format') toolbox.toolbar.insert(self.entry_toolbar_button, -1) self.entry_toolbar_button.connect('clicked', self.entry_toolbar_clicked) self.set_toolbar_box(toolbox) toolbox.show() # And add the buttons tool = ToolButton('zoom-out') tool.set_tooltip(_('Zoom out')) tool.set_accelerator(_('minus')) tool.connect('clicked', self.zoom_out) tool.show() toolbarview.insert(tool, -1) tool = ToolButton('zoom-in') tool.set_tooltip(_('Zoom in')) tool.set_accelerator(_('equal')) tool.connect('clicked', self.zoom_in) tool.show() toolbarview.insert(tool, -1) toolbarview.show() tool = ToolButton('emptytree') tool.set_tooltip(_('Empty tree')) tool.connect('clicked', self.emptytree) tool.show() toolbarsample.insert(tool, -1) tool = ToolButton('sample1') tool.set_tooltip(_('Test')) tool.connect('clicked', self.sample1) tool.show() toolbarsample.insert(tool, -1) tool = ToolButton('sample2') tool.set_tooltip(_('Napoléon')) tool.connect('clicked', self.sample2) tool.show() toolbarsample.insert(tool, -1) toolbarsample.show() separator = Gtk.SeparatorToolItem() separator.props.draw = False separator.set_expand(True) separator.show() toolbox.toolbar.insert(separator, -1) stop_button = StopButton(self) stop_button.props.accelerator = 'q' toolbox.toolbar.insert(stop_button, -1) stop_button.show() button = ToolButton('edit-paste') button.set_tooltip(_("Click here to paste an image")) button.connect('clicked', self.image_release_button) button.show() toolbarentry.insert(button, -1) self.detail_name = Gtk.Entry() self.detail_name.set_size_request(320, 50) self.detail_name.connect("changed", self.detail_changed) self.detail_name.show() toolitem = Gtk.ToolItem() toolitem.add(self.detail_name) toolbarentry.insert(toolitem, -1) toolitem.show() chkbox = Gtk.HBox(False) self.detail_chkmale = Gtk.RadioButton(None, _("Male")) self.detail_chkmale.connect("toggled", self.sexradio_checked, 'M') self.detail_chkmale.set_active(True) self.detail_chkmale.show() self.detail_chkfemale = Gtk.RadioButton(self.detail_chkmale, _("Female")) self.detail_chkfemale.join_group(self.detail_chkmale) self.detail_chkfemale.connect("toggled", self.sexradio_checked, 'F') self.detail_chkfemale.show() chkbox.add(self.detail_chkmale) chkbox.add(self.detail_chkfemale) toolitem = Gtk.ToolItem() toolitem.add(chkbox) toolitem.show() toolbarentry.insert(toolitem, -1) toolitem.show() self.detail_btnaddparent = ToolButton('addparent') self.detail_btnaddparent.set_tooltip(_('Add parents')) self.detail_btnaddparent.connect('clicked', self.addparent_clicked) self.detail_btnaddparent.show() toolbarentry.insert(self.detail_btnaddparent, -1) self.detail_btnaddbrother = ToolButton('addbrother') self.detail_btnaddbrother.set_tooltip(_('Add brother')) self.detail_btnaddbrother.connect('clicked', self.addbrother_clicked) self.detail_btnaddbrother.show() toolbarentry.insert(self.detail_btnaddbrother, -1) self.detail_btnaddunion = ToolButton('addunion') self.detail_btnaddunion.set_tooltip(_('Add union')) self.detail_btnaddunion.connect('clicked', self.addunion_clicked) self.detail_btnaddunion.show() toolbarentry.insert(self.detail_btnaddunion, -1) self.detail_btnaddchild = ToolButton('addchild') self.detail_btnaddchild.set_tooltip(_('Add child')) self.detail_btnaddchild.connect('clicked', self.addchild_clicked) self.detail_btnaddchild.show() toolbarentry.insert(self.detail_btnaddchild, -1) self.detail_btndelete = ToolButton('delete') self.detail_btndelete.set_tooltip(_('Delete')) self.detail_btndelete.connect('clicked', self.delete_clicked) self.detail_btndelete.show() toolbarentry.insert(self.detail_btndelete, -1) # Create drawing area self.zoomlevel = 0 self.area = Gtk.DrawingArea() self.area.set_size_request(875, 780) self.area.set_events(Gdk.EventMask.BUTTON_PRESS_MASK|Gdk.EventMask.BUTTON_RELEASE_MASK|Gdk.EventMask.BUTTON_MOTION_MASK|Gdk.EventMask.POINTER_MOTION_MASK) self.area.connect("draw", self.area_expose_cb) self.area.connect("button_press_event", self.press_button) self.area.connect("button_release_event", self.release_button) self.area.connect("motion_notify_event", self.mouse_move) self.moving = False # Create detail view self.box = Gtk.HBox(False) vbox = Gtk.VBox(False, 10) vbox.set_size_request(200, 780) self.imagezone = Gtk.Image() self.imagezone.set_size_request(200, 200) self.image_paste = GdkPixbuf.Pixbuf.new_from_file('images/edit-paste.svg') self.image_hand1 = GdkPixbuf.Pixbuf.new_from_file('images/hand1.png') self.imagezone.set_from_pixbuf(self.image_hand1) self.detail_description = Gtk.TextView() self.detail_description.set_wrap_mode(Gtk.WrapMode.WORD) self.detail_description.set_size_request(200, 200) self.detail_description.get_buffer().connect("changed", self.description_changed) self.detail_description.show() vbox.show() vbox.pack_start(self.imagezone, False, True, 10) vbox.pack_start(self.detail_description, True, True, 10) self.box.pack_start(vbox, True, True, 0) self.box.pack_start(self.area, True, True, 0) self.set_canvas(self.box) # Create empty tree self.tree = None self.selected = None self.init_tree(empty_tree(buddyName)) # Show all self.show_all() self.area.get_window().set_cursor(Gdk.Cursor(Gdk.CursorType.ARROW)) def create_button(self, label, image, callback): "Create a bitmap button" button = Gtk.Button() btncontainer = Gtk.HBox() btnimage = Gtk.Image() btnimage.set_from_pixbuf(GdkPixbuf.Pixbuf.new_from_file(image)) btncontainer.pack_start(btnimage, False, False, 2) btncontainer.pack_start(Gtk.Label(label), False, False, 10) button.add(btncontainer) button.connect("clicked", callback) return button def init_tree(self, tree): "Create and init a tree" self.zoomlevel = 0 self.tree = tree (self.initx, self.inity) = (const.tree_initx, const.tree_inity) self.tree.set_position(self.initx, self.inity) self.show_detail(None) self.redraw() self.entry_toolbar_button.set_expanded(True) def set_center(self, person): "Set the center of the draw on a person" rect = self.area.get_allocation() dx = (rect.width/2) - self.tree.root.x0 - (person.x0 - self.tree.root.x0) - (person.x1 - person.x0)/2 dy = (rect.height/2) - self.tree.root.y0 - (person.y0 - self.tree.root.y0) - (person.y1 - person.y0)/2 self.translate(dx, dy) def show_detail(self, person): "Show detail information for a person" # Change selection if self.selected is not None: self.selected.isselected = False self.selected = person # No selection if person == None: self.detail_name.set_sensitive(False) self.detail_description.set_sensitive(False) self.detail_name.set_text("") self.detail_description.get_buffer().set_text("") self.detail_chkmale.set_active(True) self.detail_chkmale.set_sensitive(False) self.detail_chkfemale.set_sensitive(False) self.detail_btnaddparent.set_sensitive(False) self.detail_btnaddbrother.set_sensitive(False) self.detail_btnaddunion.set_sensitive(False) self.detail_btnaddchild.set_sensitive(False) self.detail_btndelete.set_sensitive(False) self.imagezone.set_from_pixbuf(self.image_hand1) return # A node is selected person.isselected = True self.detail_name.set_sensitive(True) self.detail_name.grab_focus() self.detail_description.set_sensitive(True) self.detail_description.get_buffer().set_text(person.description) self.detail_name.set_text(person.name) if person.sex == 'M': self.detail_chkmale.set_active(True) else: self.detail_chkfemale.set_active(True) if person.image is None: self.imagezone.set_from_pixbuf(self.image_paste) else: self.imagezone.set_from_pixbuf(person.image) # Compute button status checkable = (len(person.unions) == 0) self.detail_chkmale.set_sensitive(checkable) self.detail_chkfemale.set_sensitive(checkable) self.detail_btnaddchild.set_sensitive(len(person.unions) > 0) unionscount = len(person.unions) childcount = person.child_count() isfamily = self.tree.is_family(person) isdescendant = self.tree.is_descendant(person) isascendant = self.tree.is_ascendant(person) isroot = (self.tree.root == person) if person == self.tree.root: deletable = False elif unionscount == 0: deletable = True elif unionscount > 1: deletable = False elif unionscount == 1 and isdescendant: deletable = False elif childcount > 0: if isascendant and (person.parents is None) and (childcount == 1): deletable = True else: deletable = False else: deletable = True self.detail_btndelete.set_sensitive(deletable) self.detail_btnaddunion.set_sensitive(isfamily) self.detail_btnaddparent.set_sensitive(person.parents is None and (isascendant or isroot)) self.detail_btnaddbrother.set_sensitive(person.parents is not None) def area_expose_cb(self, area, event): "Draw tree event" # Create context then draw tree inside gc = self.area.get_window().cairo_create() pc = self.create_pango_context() self.tree.draw(gc, pc) gc.stroke() def press_button(self, widget, event): "Mouse button clicked, detail a person or start the moving mode" # Look if click in a person p = self.tree.person_at(event.x, event.y) if p is not None: self.set_center(p) self.show_detail(p) self.moving = False return # Not found, pass in moving mode self.show_detail(None) self.movingStart = (event.x, event.y) self.moving = True self.area.get_window().set_cursor(Gdk.Cursor(Gdk.CursorType.FLEUR)) def translate(self, dx, dy): # Translate all the tree from deltax, deltay (self.initx, self.inity) = (self.initx+dx, self.inity+dy) self.tree.translate(dx, dy) self.redraw() def mouse_move(self, widget, event): "Mouse move event, in moving mode translate draw if in moving mode else change cursor on person" # In moving mode ? if self.moving: # Compute translation self.translate(event.x-self.movingStart[0], event.y-self.movingStart[1]) self.movingStart = (event.x, event.y) else: # Look if a person is under the cursor p = self.tree.person_at(event.x, event.y) if p is not None: # Found one, change cursor self.area.get_window().set_cursor(Gdk.Cursor(Gdk.CursorType.HAND1)) return # Not found, set standard cursor self.area.get_window().set_cursor(Gdk.Cursor(Gdk.CursorType.ARROW)) def release_button(self, widget, event): "Mouse button released, stop moving mode" self.area.get_window().set_cursor(Gdk.Cursor(Gdk.CursorType.ARROW)) self.moving = False def zoom_in(self, event): "ToolBar button zoom in clicked" self.zoomlevel = self.zoomlevel + 1 self.tree.scale(20) self.redraw() def zoom_out(self, event): "ToolBar button zoom out clicked" if self.zoomlevel == -3: return self.zoomlevel = self.zoomlevel - 1 self.tree.scale(-20) self.redraw() def detail_changed(self, event): "Textfield for the detail has changed, change the matching person name" if self.selected == None: self.redraw() return self.selected.name = self.detail_name.get_text() self.redraw() def description_changed(self, event): "Description has changed, change the matching person description" if self.selected == None: return buffer = self.detail_description.get_buffer() self.selected.description = \ buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter(), True) self.redraw() def sexradio_checked(self, widget, data): "Radio button for sex checked, change sex for the selected person" if self.selected is not None: self.selected.sex = data self.redraw() def addparent_clicked(self, event): "Add parent clicked" self.zoomlevel = 0 (x, y) = (self.tree.root.x0, self.tree.root.y0) dad = self.tree.Person("", "M") mum = self.tree.Person("", "F") self.tree.Union(dad, mum).append_child(self.selected) self.tree.set_position(x, y) self.set_center(dad) self.show_detail(dad) self.redraw() def addbrother_clicked(self, event): "Add brother clicked" self.zoomlevel = 0 newchild = self.tree.Person("", "M") self.selected.parents.append_child(newchild) self.tree.set_position(self.tree.root.x0, self.tree.root.y0) self.set_center(newchild) self.show_detail(newchild) self.redraw() def addchild_clicked(self, event): "Add child clicked" self.zoomlevel = 0 newchild = self.tree.Person("", "M") self.selected.unions[0].append_child(newchild) # HACK: Child is add to the first union self.tree.set_position(self.tree.root.x0, self.tree.root.y0) self.set_center(newchild) self.show_detail(newchild) self.redraw() def addunion_clicked(self, event): "Add union clicked" self.zoomlevel = 0 if self.selected.sex == "M": male = self.selected female = newunion = self.tree.Person("", "F") else: male = newunion = self.tree.Person("", "M") female = self.selected self.tree.Union(male, female) self.tree.set_position(self.tree.root.x0, self.tree.root.y0) self.set_center(newunion) self.show_detail(newunion) self.redraw() def delete_clicked(self, event): "Delete button clicked" # Delete as union or person if self.tree.is_ascendant(self.selected) and (self.selected.parents is None) and (self.selected.child_count() == 1): self.tree.remove_union(self.selected.unions[0]) else: self.tree.remove(self.selected) # Recompute and redraw self.zoomlevel = 0 self.tree.set_position(self.tree.root.x0, self.tree.root.y0) self.show_detail(None) def entry_toolbar_clicked(self, event): self.box.queue_draw() def redraw(self): "Redraw area" self.area.queue_draw() def image_release_button(self, widget, event=None): "Mouse button released on the image, drop image" if self.selected is None: return # Get image from clipboard _logger.debug('Before clipboard') clipboard = Gtk.Clipboard() image = None if True: #clipboard.wait_is_image_available(): _logger.debug('wait_is_image_available() is True') image = clipboard.wait_for_image() if image is None: return # HACK: Get image from URIs list else: _logger.debug('wait_is_image_available() is False') selection = clipboard.wait_for_contents('text/uri-list') if selection is None: _logger.debug('selection is none') return for uri in selection.get_uris(): image = GdkPixbuf.Pixbuf.new_from_file(urlparse(uri).path) break if image is None: return # Match image to rectangle rect = self.imagezone.get_allocation() image_width = image.get_width() image_height = image.get_height() _logger.debug(str(image_width)+'x'+str(image_height)) if image_width > rect.width or image_height > rect.height: if image_width > image_height: target_width = rect.width target_height = (image_height * target_width) / image_width else: target_height = rect.height target_width = (image_width * target_height) / image_height image = image.scale_simple(target_width, target_height, Gdk.INTERP_HYPER) self.selected.image = image self.redraw() def emptytree(self, event): "Init with an empty tree" self.init_tree(empty_tree(buddyName)) def sample1(self, event): "Init with a sample tree" self.init_tree(sample_family1()) def sample2(self, event): "Init with a sample tree" self.init_tree(sample_family2()) def write_file(self, file_path): "Called when activity is saved, save the tree in the file" file = open(file_path, 'wb') try: self.tree.write_to(file) finally: file.close() def read_file(self, file_path): "Called when activity is loaded, load the tree from the file" file = open(file_path, 'rb') try: tree = Tree().read_from(file) finally: file.close() self.init_tree(tree)