Modify media reference editor to use new selection widget

svn: r23421
This commit is contained in:
Nick Hall 2013-10-26 17:56:39 +00:00
parent 5b31f8ce60
commit 767430f93f
5 changed files with 1080 additions and 341 deletions

View File

@ -6,6 +6,7 @@
# 2009 Gary Burton
# 2011 Robert Cheramy <robert@cheramy.net>
# Copyright (C) 2011 Tim G L Lyons
# Copyright (C) 2013 Nick Hall
#
# 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
@ -58,7 +59,7 @@ from ..glade import Glade
from .displaytabs import (CitationEmbedList, AttrEmbedList, MediaBackRefList,
NoteTab)
from ..widgets import (MonitoredSpinButton, MonitoredEntry, PrivacyButton,
MonitoredDate, MonitoredTagList)
MonitoredDate, MonitoredTagList, SelectionWidget, Region)
from .editreference import RefTab, EditReference
from .addmedia import AddMediaObject
@ -136,7 +137,7 @@ class EditMediaRef(EditReference):
if not self.source.get_mime_type():
self.mimetext.set_text(_('Note'))
def draw_preview(self, onlysub=False):
def draw_preview(self):
"""
Draw the two preview images. This method can be called on eg change of
the path.
@ -144,33 +145,18 @@ class EditMediaRef(EditReference):
mtype = self.source.get_mime_type()
if mtype:
fullpath = media_path_full(self.db, self.source.get_path())
if not onlysub:
pb = get_thumbnail_image(fullpath, mtype)
self.pixmap.set_from_pixbuf(pb)
subpix = get_thumbnail_image(fullpath, mtype,
self.rectangle)
self.subpixmap.set_from_pixbuf(subpix)
self.selection.load_image(fullpath)
else:
pb = find_mime_type_pixbuf('text/plain')
if not onlysub:
self.pixmap.set_from_pixbuf(pb)
self.subpixmap.set_from_pixbuf(pb)
self.selection.load_image('')
def _setup_fields(self):
ebox_shared = self.top.get_object('eventbox')
ebox_shared.connect('button-press-event', self.button_press_event)
if not self.dbstate.db.readonly:
self.button_press_coords = (0, 0)
ebox_ref = self.top.get_object('eventbox1')
ebox_ref.connect('button-press-event', self.button_press_event_ref)
ebox_ref.connect('button-release-event',
self.button_release_event_ref)
ebox_ref.connect('motion-notify-event', self.motion_notify_event_ref)
ebox_ref.add_events(Gdk.EventMask.BUTTON_PRESS_MASK)
ebox_ref.add_events(Gdk.EventMask.BUTTON_RELEASE_MASK)
self.pixmap = self.top.get_object("pixmap")
self.mimetext = self.top.get_object("type")
self.track_ref_for_deletion("mimetext")
@ -185,13 +171,22 @@ class EditMediaRef(EditReference):
):
coord = None
if coord is not None:
self.rectangle = coord
self.subpixmap = self.top.get_object("subpixmap")
self.track_ref_for_deletion("subpixmap")
else:
self.rectangle = (0, 0, 100, 100)
self.selection = SelectionWidget()
self.selection.set_multiple_selection(False)
self.selection.connect("region-modified", self.region_modified)
self.selection.connect("region-created", self.region_modified)
frame = self.top.get_object("frame9")
frame.add(self.selection)
self.track_ref_for_deletion("selection")
self.selection.show()
self.setup_filepath()
self.determine_mime()
self.draw_preview()
corners = ["corner1_x", "corner1_y", "corner2_x", "corner2_y"]
@ -278,6 +273,13 @@ class EditMediaRef(EditReference):
self.uistate, self.track,
self.db.readonly)
def _post_init(self):
"""
Initialization that must happen after the window is shown.
"""
self.draw_preview()
self.update_region()
def set_corner1_x(self, value):
"""
Callback for the signal handling of the spinbutton for the first
@ -287,10 +289,8 @@ class EditMediaRef(EditReference):
@param value: the first corner x coordinate of the subsection in int
"""
if self.rectangle is None:
self.rectangle = (0,0,100,100)
self.rectangle = (value,) + self.rectangle[1:]
self.update_subpixmap()
self.update_region()
def set_corner1_y(self, value):
"""
@ -301,10 +301,8 @@ class EditMediaRef(EditReference):
@param value: the first corner y coordinate of the subsection in int
"""
if self.rectangle is None:
self.rectangle = (0,0,100,100)
self.rectangle = self.rectangle[:1] + (value,) + self.rectangle[2:]
self.update_subpixmap()
self.update_region()
def set_corner2_x(self, value):
"""
@ -315,10 +313,8 @@ class EditMediaRef(EditReference):
@param value: the second corner x coordinate of the subsection in int
"""
if self.rectangle is None:
self.rectangle = (0,0,100,100)
self.rectangle = self.rectangle[:2] + (value,) + self.rectangle[3:]
self.update_subpixmap()
self.update_region()
def set_corner2_y(self, value):
"""
@ -329,10 +325,8 @@ class EditMediaRef(EditReference):
@param value: the second corner y coordinate of the subsection in int
"""
if self.rectangle is None:
self.rectangle = (0,0,100,100)
self.rectangle = self.rectangle[:3] + (value,)
self.update_subpixmap()
self.update_region()
def get_corner1_x(self):
"""
@ -342,11 +336,7 @@ class EditMediaRef(EditReference):
@returns: the first corner x coordinate of the subsection or 0 if
there is no selection
"""
if self.rectangle is not None:
return self.rectangle[0]
else:
return 0
def get_corner1_y(self):
"""
@ -356,11 +346,7 @@ class EditMediaRef(EditReference):
@returns: the first corner y coordinate of the subsection or 0 if
there is no selection
"""
if self.rectangle is not None:
return self.rectangle[1]
else:
return 0
def get_corner2_x(self):
"""
@ -370,11 +356,7 @@ class EditMediaRef(EditReference):
@returns: the second corner x coordinate of the subsection or 100 if
there is no selection
"""
if self.rectangle is not None:
return self.rectangle[2]
else:
return 100
def get_corner2_y(self):
"""
@ -384,17 +366,29 @@ class EditMediaRef(EditReference):
@returns: the second corner x coordinate of the subsection or 100 if
there is no selection
"""
if self.rectangle is not None:
return self.rectangle[3]
else:
return 100
def update_subpixmap(self):
def update_region(self):
"""
Updates the thumbnail of the specified subsection
Updates the thumbnail of the specified subsection.
"""
self.draw_preview(onlysub=True)
if not self.selection.is_image_loaded():
return
real = self.selection.proportional_to_real_rect(self.rectangle)
region = Region(real[0], real[1], real[2], real[3])
self.selection.set_regions([region])
self.selection.refresh()
def region_modified(self, widget):
"""
Update new values when the selection is changed.
"""
real = self.selection.get_selection()
coords = self.selection.real_to_proportional_rect(real)
self.corner1_x_spinbutton.set_value(coords[0])
self.corner1_y_spinbutton.set_value(coords[1])
self.corner2_x_spinbutton.set_value(coords[2])
self.corner2_y_spinbutton.set_value(coords[3])
def build_menu_names(self, person):
"""
@ -412,139 +406,6 @@ class EditMediaRef(EditReference):
photo_path = media_path_full(self.db, self.source.get_path())
open_file_with_default_application(photo_path)
def button_press_event_ref(self, widget, event):
"""
Handle the button-press-event generated by the eventbox
parent of the subpixmap. Remember these coordinates
so we can crop the picture when button-release-event
is received.
"""
self.button_press_coords = (event.x, event.y)
# prepare drawing of a feedback rectangle
self.orig_subpixbuf = self.subpixmap.get_pixbuf()
gtkimg_win = self.subpixmap.get_window()
self.rect_pixbuf = Gdk.pixbuf_get_from_window(
gtkimg_win, 0, 0,
gtkimg_win.get_width(),
gtkimg_win.get_height())
def motion_notify_event_ref(self, widget, event):
# get the image size and calculate the X and Y offsets
# (image is centered *horizontally* when smaller than THUMBSCALE)
w, h = self.orig_subpixbuf.get_width(), self.orig_subpixbuf.get_height()
offset_x = (THUMBSCALE - w) / 2
offset_y = 0
# get coordinates of the rectangle, so that x1 < x2 and y1 < y2
x1 = min(self.button_press_coords[0], event.x)
x2 = max(self.button_press_coords[0], event.x)
y1 = min(self.button_press_coords[1], event.y)
y2 = max(self.button_press_coords[1], event.y)
width = int(x2 - x1)
height = int(y2 - y1)
x1 = int(x1 - offset_x)
y1 = int(y1 - offset_y)
cr_pixbuf = self.subpixmap.get_window().cairo_create()
cr_pixbuf.reset_clip()
#reset the pixbuf
Gdk.cairo_set_source_pixbuf(cr_pixbuf, self.rect_pixbuf, 0, 0)
cr_pixbuf.paint()
cr_pixbuf.set_source_rgba(0, 0, 1, 0.5) #blue transparant
cr_pixbuf.move_to(x1,y1)
cr_pixbuf.line_to(x1+width, y1)
cr_pixbuf.line_to(x1+width, y1+height)
cr_pixbuf.line_to(x1, y1+height)
cr_pixbuf.close_path()
cr_pixbuf.fill()
def button_release_event_ref(self, widget, event):
"""
Handle the button-release-event generated by the eventbox
parent of the subpixmap. Crop the picture accordingly.
"""
# reset the crop on double-click or click+CTRL
if (event.button==1 and event.type == Gdk.EventType._2BUTTON_PRESS) or \
(event.button==1 and (event.get_state() & Gdk.ModifierType.CONTROL_MASK) ):
self.corner1_x_spinbutton.set_value(0)
self.corner1_y_spinbutton.set_value(0)
self.corner2_x_spinbutton.set_value(100)
self.corner2_y_spinbutton.set_value(100)
else:
if (self.orig_subpixbuf == None):
return
self.subpixmap.set_from_pixbuf(self.orig_subpixbuf)
# ensure the clicks happened at least 5 pixels away from each other
new_x1 = min(self.button_press_coords[0], event.x)
new_y1 = min(self.button_press_coords[1], event.y)
new_x2 = max(self.button_press_coords[0], event.x)
new_y2 = max(self.button_press_coords[1], event.y)
if new_x2 - new_x1 >= 5 and new_y2 - new_y1 >= 5:
# get the image size and calculate the X and Y offsets
# (image is centered *horizontally* when smaller than THUMBSCALE)
w = self.orig_subpixbuf.get_width()
h = self.orig_subpixbuf.get_height()
x = (THUMBSCALE - w) / 2
y = 0
# if the click was outside of the image,
# bring it within the boundaries
if new_x1 < x:
new_x1 = x
if new_y1 < y:
new_y1 = y
if new_x2 >= x + w:
new_x2 = x + w - 1
if new_y2 >= y + h:
new_y2 = y + h - 1
# get the old spinbutton % values
old_x1 = self.corner1_x_spinbutton.get_value()
old_y1 = self.corner1_y_spinbutton.get_value()
old_x2 = self.corner2_x_spinbutton.get_value()
old_y2 = self.corner2_y_spinbutton.get_value()
delta_x = old_x2 - old_x1 # horizontal scale
delta_y = old_y2 - old_y1 # vertical scale
# Took a while to figure out the math here.
#
# 1) figure out the current crop % values
# by doing the following:
#
# xp = click_location_x / width * 100
# yp = click_location_y / height * 100
#
# but remember that click_location_x and _y
# might not be zero-based for non-rectangular
# images, so subtract the pixbuf "x" and "y"
# to bring the values back to zero-based
#
# 2) the minimum value cannot be less than the
# existing crop value, so add the current
# minimum to the new values
new_x1 = old_x1 + delta_x * (new_x1 - x) / w
new_y1 = old_y1 + delta_y * (new_y1 - y) / h
new_x2 = old_x1 + delta_x * (new_x2 - x) / w
new_y2 = old_y1 + delta_y * (new_y2 - y) / h
# set the new values
self.corner1_x_spinbutton.set_value(new_x1)
self.corner1_y_spinbutton.set_value(new_y1)
self.corner2_x_spinbutton.set_value(new_x2)
self.corner2_y_spinbutton.set_value(new_y2)
# Free the pixbuf as it is not needed anymore
self.orig_subpixbuf = None
self.rect_pixbuf = None
self.draw_preview()
def _update_addmedia(self, obj):
"""
Called when the add media dialog has been called.

View File

@ -39,12 +39,10 @@
<child>
<object class="GtkButton" id="button84">
<property name="label">gtk-cancel</property>
<property name="use_action_appearance">False</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="can_default">True</property>
<property name="receives_default">True</property>
<property name="use_action_appearance">False</property>
<property name="use_stock">True</property>
</object>
<packing>
@ -56,13 +54,11 @@
<child>
<object class="GtkButton" id="button82">
<property name="label">gtk-ok</property>
<property name="use_action_appearance">False</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="can_default">True</property>
<property name="has_default">True</property>
<property name="receives_default">True</property>
<property name="use_action_appearance">False</property>
<property name="use_stock">True</property>
</object>
<packing>
@ -74,12 +70,10 @@
<child>
<object class="GtkButton" id="button104">
<property name="label">gtk-help</property>
<property name="use_action_appearance">False</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="can_default">True</property>
<property name="receives_default">True</property>
<property name="use_action_appearance">False</property>
<property name="use_stock">True</property>
</object>
<packing>
@ -111,24 +105,15 @@
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="border_width">12</property>
<property name="n_rows">5</property>
<property name="n_columns">5</property>
<property name="n_rows">6</property>
<property name="n_columns">3</property>
<property name="column_spacing">12</property>
<property name="row_spacing">6</property>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<object class="GtkLabel" id="label425">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
<property name="xalign">1</property>
<property name="label" translatable="yes">_Corner 2: X</property>
<property name="use_underline">True</property>
<property name="mnemonic_widget">corner2_x</property>
@ -136,46 +121,28 @@
<packing>
<property name="left_attach">1</property>
<property name="right_attach">2</property>
<property name="top_attach">3</property>
<property name="bottom_attach">4</property>
<property name="x_options"></property>
<property name="y_options"></property>
<property name="top_attach">4</property>
<property name="bottom_attach">5</property>
<property name="x_options">GTK_FILL</property>
<property name="y_options">GTK_FILL</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label428">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
<property name="xalign">1</property>
<property name="label" translatable="yes">Y coordinate|Y</property>
<property name="use_underline">True</property>
<property name="mnemonic_widget">corner2_y</property>
</object>
<packing>
<property name="left_attach">3</property>
<property name="right_attach">4</property>
<property name="top_attach">3</property>
<property name="bottom_attach">4</property>
<property name="x_options"></property>
<property name="y_options"></property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label430">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
<property name="label" translatable="yes">Referenced Region</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
<packing>
<property name="right_attach">5</property>
<property name="top_attach">1</property>
<property name="bottom_attach">2</property>
<property name="left_attach">1</property>
<property name="right_attach">2</property>
<property name="top_attach">5</property>
<property name="bottom_attach">6</property>
<property name="x_options">GTK_FILL</property>
<property name="y_options"></property>
<property name="y_options">GTK_FILL</property>
</packing>
</child>
<child>
@ -190,11 +157,12 @@ You can use the mouse on the picture to select a region, or use these spinbutton
<property name="numeric">True</property>
</object>
<packing>
<property name="left_attach">4</property>
<property name="right_attach">5</property>
<property name="top_attach">3</property>
<property name="bottom_attach">4</property>
<property name="y_options"></property>
<property name="left_attach">2</property>
<property name="right_attach">3</property>
<property name="top_attach">5</property>
<property name="bottom_attach">6</property>
<property name="x_options">GTK_FILL</property>
<property name="y_options"/>
</packing>
</child>
<child>
@ -211,9 +179,10 @@ You can use the mouse on the picture to select a region, or use these spinbutton
<packing>
<property name="left_attach">2</property>
<property name="right_attach">3</property>
<property name="top_attach">3</property>
<property name="bottom_attach">4</property>
<property name="y_options"></property>
<property name="top_attach">4</property>
<property name="bottom_attach">5</property>
<property name="x_options">GTK_FILL</property>
<property name="y_options"/>
</packing>
</child>
<child>
@ -224,19 +193,7 @@ You can use the mouse on the picture to select a region, or use these spinbutton
Select a region with clicking and holding the mouse button on the top left corner of the region you want, dragging the mouse to the bottom right corner of the region, and then releasing the mouse button.</property>
<property name="label_xalign">0</property>
<child>
<object class="GtkEventBox" id="eventbox1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkImage" id="subpixmap">
<property name="width_request">100</property>
<property name="height_request">100</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="yalign">0</property>
</object>
</child>
</object>
<placeholder/>
</child>
<child type="label">
<object class="GtkLabel" id="label707">
@ -250,17 +207,14 @@ Select a region with clicking and holding the mouse button on the top left corne
</child>
</object>
<packing>
<property name="top_attach">2</property>
<property name="bottom_attach">5</property>
<property name="x_options"></property>
<property name="y_options"></property>
<property name="bottom_attach">6</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="label426">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
<property name="xalign">1</property>
<property name="label" translatable="yes">_Corner 1: X</property>
<property name="use_underline">True</property>
<property name="mnemonic_widget">corner1_x</property>
@ -270,13 +224,10 @@ Select a region with clicking and holding the mouse button on the top left corne
<property name="right_attach">2</property>
<property name="top_attach">2</property>
<property name="bottom_attach">3</property>
<property name="x_options"></property>
<property name="y_options"></property>
<property name="x_options">GTK_FILL</property>
<property name="y_options">GTK_FILL</property>
</packing>
</child>
<child>
<placeholder/>
</child>
<child>
<object class="GtkSpinButton" id="corner1_x">
<property name="visible">True</property>
@ -285,16 +236,19 @@ Select a region with clicking and holding the mouse button on the top left corne
You can use the mouse on the picture to select a region, or use these spinbuttons to set the top left, and bottom right corner of the referenced region. Point (0,0) is the top left corner of the picture, and (100,100) the bottom right corner.
</property>
<property name="invisible_char">●</property>
<property name="shadow_type">none</property>
<property name="adjustment">adjustment2</property>
<property name="climb_rate">1</property>
<property name="numeric">True</property>
<property name="update_policy">if-valid</property>
</object>
<packing>
<property name="left_attach">2</property>
<property name="right_attach">3</property>
<property name="top_attach">2</property>
<property name="bottom_attach">3</property>
<property name="y_options"></property>
<property name="x_options">GTK_FILL</property>
<property name="y_options"/>
</packing>
</child>
<child>
@ -309,67 +263,52 @@ You can use the mouse on the picture to select a region, or use these spinbutton
<property name="numeric">True</property>
</object>
<packing>
<property name="left_attach">4</property>
<property name="right_attach">5</property>
<property name="top_attach">2</property>
<property name="bottom_attach">3</property>
<property name="y_options"></property>
<property name="left_attach">2</property>
<property name="right_attach">3</property>
<property name="top_attach">3</property>
<property name="bottom_attach">4</property>
<property name="x_options">GTK_FILL</property>
<property name="y_options"/>
</packing>
</child>
<child>
<object class="GtkLabel" id="label427">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="xalign">0</property>
<property name="xalign">1</property>
<property name="label" translatable="yes">Y coordinate|Y</property>
<property name="use_underline">True</property>
<property name="mnemonic_widget">corner1_y</property>
</object>
<packing>
<property name="left_attach">3</property>
<property name="right_attach">4</property>
<property name="top_attach">2</property>
<property name="bottom_attach">3</property>
<property name="x_options"></property>
<property name="y_options"></property>
<property name="left_attach">1</property>
<property name="right_attach">2</property>
<property name="top_attach">3</property>
<property name="bottom_attach">4</property>
<property name="x_options">GTK_FILL</property>
<property name="y_options">GTK_FILL</property>
</packing>
</child>
<child>
<placeholder/>
</child>
<child>
<object class="GtkLabel" id="label1">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="left_attach">2</property>
<property name="right_attach">3</property>
<property name="top_attach">4</property>
<property name="bottom_attach">5</property>
<property name="y_options"></property>
<property name="y_options"/>
</packing>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<object class="GtkHBox" id="hbox1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<object class="GtkToggleButton" id="private">
<property name="use_action_appearance">False</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_action_appearance">False</property>
<property name="relief">none</property>
<accelerator key="p" signal="activate" modifiers="GDK_CONTROL_MASK"/>
<child internal-child="accessible">
@ -400,10 +339,10 @@ You can use the mouse on the picture to select a region, or use these spinbutton
</child>
</object>
<packing>
<property name="left_attach">4</property>
<property name="right_attach">5</property>
<property name="left_attach">2</property>
<property name="right_attach">3</property>
<property name="x_options">GTK_FILL</property>
<property name="y_options"></property>
<property name="y_options"/>
</packing>
</child>
<child>
@ -413,22 +352,17 @@ You can use the mouse on the picture to select a region, or use these spinbutton
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
</child>
<child>
<placeholder/>
<object class="GtkLabel" id="dummy">
<property name="visible">True</property>
<property name="can_focus">False</property>
</object>
<packing>
<property name="left_attach">2</property>
<property name="right_attach">3</property>
<property name="top_attach">1</property>
<property name="bottom_attach">2</property>
<property name="x_options"/>
</packing>
</child>
</object>
</child>
@ -447,7 +381,7 @@ You can use the mouse on the picture to select a region, or use these spinbutton
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="padding">10</property>
<property name="position">0</property>
@ -499,7 +433,7 @@ You can use the mouse on the picture to select a region, or use these spinbutton
<property name="top_attach">3</property>
<property name="bottom_attach">4</property>
<property name="x_options">GTK_FILL</property>
<property name="y_options"></property>
<property name="y_options"/>
</packing>
</child>
<child>
@ -516,7 +450,7 @@ You can use the mouse on the picture to select a region, or use these spinbutton
<property name="left_attach">1</property>
<property name="right_attach">2</property>
<property name="x_options">GTK_FILL</property>
<property name="y_options"></property>
<property name="y_options"/>
</packing>
</child>
<child>
@ -550,7 +484,7 @@ You can use the mouse on the picture to select a region, or use these spinbutton
<property name="right_attach">3</property>
<property name="top_attach">1</property>
<property name="bottom_attach">2</property>
<property name="y_options"></property>
<property name="y_options"/>
</packing>
</child>
<child>
@ -595,6 +529,7 @@ You can use the mouse on the picture to select a region, or use these spinbutton
<property name="top_attach">6</property>
<property name="bottom_attach">7</property>
<property name="x_options">GTK_FILL</property>
<property name="y_options">GTK_FILL</property>
</packing>
</child>
<child>
@ -650,7 +585,7 @@ You can use the mouse on the picture to select a region, or use these spinbutton
<property name="top_attach">5</property>
<property name="bottom_attach">6</property>
<property name="x_options">GTK_FILL</property>
<property name="y_options"></property>
<property name="y_options"/>
</packing>
</child>
<child>
@ -666,7 +601,7 @@ You can use the mouse on the picture to select a region, or use these spinbutton
<property name="top_attach">5</property>
<property name="bottom_attach">6</property>
<property name="x_options">GTK_FILL</property>
<property name="y_options"></property>
<property name="y_options"/>
</packing>
</child>
<child>
@ -684,7 +619,7 @@ You can use the mouse on the picture to select a region, or use these spinbutton
<property name="top_attach">2</property>
<property name="bottom_attach">3</property>
<property name="x_options">GTK_FILL</property>
<property name="y_options"></property>
<property name="y_options"/>
</packing>
</child>
<child>
@ -698,18 +633,16 @@ You can use the mouse on the picture to select a region, or use these spinbutton
<property name="right_attach">3</property>
<property name="top_attach">2</property>
<property name="bottom_attach">3</property>
<property name="y_options"></property>
<property name="y_options"/>
</packing>
</child>
<child>
<object class="GtkButton" id="date_edit">
<property name="use_action_appearance">False</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="has_tooltip">True</property>
<property name="tooltip_text" translatable="yes">Invoke date editor</property>
<property name="use_action_appearance">False</property>
<property name="relief">none</property>
<accelerator key="d" signal="activate" modifiers="GDK_CONTROL_MASK"/>
<accessibility>
@ -739,16 +672,14 @@ You can use the mouse on the picture to select a region, or use these spinbutton
<property name="top_attach">2</property>
<property name="bottom_attach">3</property>
<property name="x_options">GTK_FILL</property>
<property name="y_options"></property>
<property name="y_options"/>
</packing>
</child>
<child>
<object class="GtkToggleButton" id="privacy">
<property name="use_action_appearance">False</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_action_appearance">False</property>
<property name="relief">none</property>
<child internal-child="accessible">
<object class="AtkObject" id="privacy-atkobject">
@ -772,6 +703,8 @@ You can use the mouse on the picture to select a region, or use these spinbutton
<packing>
<property name="left_attach">3</property>
<property name="right_attach">4</property>
<property name="x_options">GTK_FILL</property>
<property name="y_options">GTK_FILL</property>
</packing>
</child>
<child>
@ -783,17 +716,16 @@ You can use the mouse on the picture to select a region, or use these spinbutton
<packing>
<property name="left_attach">2</property>
<property name="right_attach">3</property>
<property name="y_options">GTK_FILL</property>
</packing>
</child>
<child>
<object class="GtkButton" id="file_select">
<property name="use_action_appearance">False</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="has_tooltip">True</property>
<property name="tooltip_text" translatable="yes">Select a file</property>
<property name="use_action_appearance">False</property>
<property name="relief">none</property>
<accessibility>
<relation type="labelled-by" target="label129"/>
@ -822,6 +754,8 @@ You can use the mouse on the picture to select a region, or use these spinbutton
<property name="right_attach">4</property>
<property name="top_attach">3</property>
<property name="bottom_attach">4</property>
<property name="x_options">GTK_FILL</property>
<property name="y_options">GTK_FILL</property>
</packing>
</child>
<child>
@ -835,6 +769,7 @@ You can use the mouse on the picture to select a region, or use these spinbutton
<property name="right_attach">3</property>
<property name="top_attach">3</property>
<property name="bottom_attach">4</property>
<property name="y_options">GTK_FILL</property>
</packing>
</child>
<child>
@ -852,7 +787,7 @@ You can use the mouse on the picture to select a region, or use these spinbutton
<property name="top_attach">4</property>
<property name="bottom_attach">5</property>
<property name="x_options">GTK_FILL</property>
<property name="y_options"></property>
<property name="y_options"/>
</packing>
</child>
<child>
@ -865,15 +800,14 @@ You can use the mouse on the picture to select a region, or use these spinbutton
<property name="right_attach">3</property>
<property name="top_attach">4</property>
<property name="bottom_attach">5</property>
<property name="y_options">GTK_FILL</property>
</packing>
</child>
<child>
<object class="GtkButton" id="tag_button">
<property name="use_action_appearance">False</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="use_action_appearance">False</property>
<accessibility>
<relation type="labelled-by" target="label1"/>
</accessibility>
@ -888,9 +822,14 @@ You can use the mouse on the picture to select a region, or use these spinbutton
<property name="right_attach">4</property>
<property name="top_attach">4</property>
<property name="bottom_attach">5</property>
<property name="x_options">GTK_FILL</property>
<property name="y_options">GTK_FILL</property>
</packing>
</child>
</object>
<packing>
<property name="tab_fill">False</property>
</packing>
</child>
<child type="tab">
<object class="GtkLabel" id="label617">
@ -919,14 +858,14 @@ You can use the mouse on the picture to select a region, or use these spinbutton
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">1</property>
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">1</property>
</packing>

View File

@ -31,6 +31,7 @@ from .labels import *
from .linkbox import *
from .photo import *
from .monitoredwidgets import *
from .selectionwidget import SelectionWidget, Region
from .shortlistcomboentry import *
from .springseparator import *
from .statusbar import Statusbar

View File

@ -0,0 +1,277 @@
#
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2013 Artem Glebov <artem.glebov@gmail.com>
#
# 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
#
# $Id$
#-------------------------------------------------------------------------
#
# GTK/Gnome modules
#
#-------------------------------------------------------------------------
from gi.repository import Gdk
#-------------------------------------------------------------------------
#
# grabbers constants and routines
#
#-------------------------------------------------------------------------
GRABBER_INSIDE = 0
GRABBER_OUTSIDE = 1
MIN_CORNER_GRABBER = 20
MIN_SIDE_GRABBER = 20
MIN_GRABBER_PADDING = 10
MIN_SIDE_FOR_INSIDE_GRABBERS = (2 * (MIN_CORNER_GRABBER + MIN_GRABBER_PADDING) +
MIN_SIDE_GRABBER)
INSIDE = 0
GRABBER_UPPER_LEFT = 1
GRABBER_UPPER = 2
GRABBER_UPPER_RIGHT = 3
GRABBER_RIGHT = 4
GRABBER_LOWER_RIGHT = 5
GRABBER_LOWER = 6
GRABBER_LOWER_LEFT = 7
GRABBER_LEFT = 8
def upper_left_grabber_inner(x1, y1, x2, y2):
return (x1, y1, x1 + MIN_CORNER_GRABBER, y1 + MIN_CORNER_GRABBER)
def upper_grabber_inner(x1, y1, x2, y2):
return (x1 + MIN_CORNER_GRABBER + MIN_GRABBER_PADDING,
y1,
x2 - MIN_CORNER_GRABBER - MIN_GRABBER_PADDING,
y1 + MIN_CORNER_GRABBER)
def upper_right_grabber_inner(x1, y1, x2, y2):
return (x2 - MIN_CORNER_GRABBER, y1, x2, y1 + MIN_CORNER_GRABBER)
def right_grabber_inner(x1, y1, x2, y2):
return (x2 - MIN_CORNER_GRABBER,
y1 + MIN_CORNER_GRABBER + MIN_GRABBER_PADDING,
x2,
y2 - MIN_CORNER_GRABBER - MIN_GRABBER_PADDING)
def lower_right_grabber_inner(x1, y1, x2, y2):
return (x2 - MIN_CORNER_GRABBER, y2 - MIN_CORNER_GRABBER, x2, y2)
def lower_grabber_inner(x1, y1, x2, y2):
return (x1 + MIN_CORNER_GRABBER + MIN_GRABBER_PADDING,
y2 - MIN_CORNER_GRABBER,
x2 - MIN_CORNER_GRABBER - MIN_GRABBER_PADDING,
y2)
def lower_left_grabber_inner(x1, y1, x2, y2):
return (x1, y2 - MIN_CORNER_GRABBER, x1 + MIN_CORNER_GRABBER, y2)
def left_grabber_inner(x1, y1, x2, y2):
return (x1,
y1 + MIN_CORNER_GRABBER + MIN_GRABBER_PADDING,
x1 + MIN_CORNER_GRABBER,
y2 - MIN_CORNER_GRABBER - MIN_GRABBER_PADDING)
# outer
def upper_left_grabber_outer(x1, y1, x2, y2):
return (x1 - MIN_CORNER_GRABBER, y1 - MIN_CORNER_GRABBER, x1, y1)
def upper_grabber_outer(x1, y1, x2, y2):
return (x1, y1 - MIN_CORNER_GRABBER, x2, y1)
def upper_right_grabber_outer(x1, y1, x2, y2):
return (x2, y1 - MIN_CORNER_GRABBER, x2 + MIN_CORNER_GRABBER, y1)
def right_grabber_outer(x1, y1, x2, y2):
return (x2, y1, x2 + MIN_CORNER_GRABBER, y2)
def lower_right_grabber_outer(x1, y1, x2, y2):
return (x2, y2, x2 + MIN_CORNER_GRABBER, y2 + MIN_CORNER_GRABBER)
def lower_grabber_outer(x1, y1, x2, y2):
return (x1, y2, x2, y2 + MIN_CORNER_GRABBER)
def lower_left_grabber_outer(x1, y1, x2, y2):
return (x1 - MIN_CORNER_GRABBER, y2, x1, y2 + MIN_CORNER_GRABBER)
def left_grabber_outer(x1, y1, x2, y2):
return (x1 - MIN_CORNER_GRABBER, y1, x1, y2)
# motion
def inside_moved(x1, y1, x2, y2, dx, dy):
return (x1, y1, x2, y2)
def upper_left_moved(x1, y1, x2, y2, dx, dy):
return (x1 + dx, y1 + dy, x2, y2)
def upper_moved(x1, y1, x2, y2, dx, dy):
return (x1, y1 + dy, x2, y2)
def upper_right_moved(x1, y1, x2, y2, dx, dy):
return (x1, y1 + dy, x2 + dx, y2)
def right_moved(x1, y1, x2, y2, dx, dy):
return (x1, y1, x2 + dx, y2)
def lower_right_moved(x1, y1, x2, y2, dx, dy):
return (x1, y1, x2 + dx, y2 + dy)
def lower_moved(x1, y1, x2, y2, dx, dy):
return (x1, y1, x2, y2 + dy)
def lower_left_moved(x1, y1, x2, y2, dx, dy):
return (x1 + dx, y1, x2, y2 + dy)
def left_moved(x1, y1, x2, y2, dx, dy):
return (x1 + dx, y1, x2, y2)
# switching
GRABBERS = [INSIDE,
GRABBER_UPPER_LEFT,
GRABBER_UPPER,
GRABBER_UPPER_RIGHT,
GRABBER_RIGHT,
GRABBER_LOWER_RIGHT,
GRABBER_LOWER,
GRABBER_LOWER_LEFT,
GRABBER_LEFT]
INNER_GRABBERS = [None,
upper_left_grabber_inner,
upper_grabber_inner,
upper_right_grabber_inner,
right_grabber_inner,
lower_right_grabber_inner,
lower_grabber_inner,
lower_left_grabber_inner,
left_grabber_inner]
OUTER_GRABBERS = [None,
upper_left_grabber_outer,
upper_grabber_outer,
upper_right_grabber_outer,
right_grabber_outer,
lower_right_grabber_outer,
lower_grabber_outer,
lower_left_grabber_outer,
left_grabber_outer]
MOTION_FUNCTIONS = [inside_moved,
upper_left_moved,
upper_moved,
upper_right_moved,
right_moved,
lower_right_moved,
lower_moved,
lower_left_moved,
left_moved]
GRABBERS_SWITCH = [
[INSIDE, INSIDE, INSIDE],
[GRABBER_UPPER_RIGHT, GRABBER_LOWER_RIGHT, GRABBER_LOWER_LEFT],
[GRABBER_UPPER, GRABBER_LOWER, GRABBER_LOWER],
[GRABBER_UPPER_LEFT, GRABBER_LOWER_LEFT, GRABBER_UPPER_RIGHT],
[GRABBER_LEFT, GRABBER_LEFT, GRABBER_RIGHT],
[GRABBER_LOWER_LEFT, GRABBER_UPPER_LEFT, GRABBER_UPPER_RIGHT],
[GRABBER_LOWER, GRABBER_UPPER, GRABBER_UPPER],
[GRABBER_LOWER_RIGHT, GRABBER_UPPER_RIGHT, GRABBER_UPPER_LEFT],
[GRABBER_RIGHT, GRABBER_RIGHT, GRABBER_LEFT]
]
# cursors
CURSOR_UPPER = Gdk.Cursor.new(Gdk.CursorType.TOP_SIDE)
CURSOR_LOWER = Gdk.Cursor.new(Gdk.CursorType.BOTTOM_SIDE)
CURSOR_LEFT = Gdk.Cursor.new(Gdk.CursorType.LEFT_SIDE)
CURSOR_RIGHT = Gdk.Cursor.new(Gdk.CursorType.RIGHT_SIDE)
CURSOR_UPPER_LEFT = Gdk.Cursor.new(Gdk.CursorType.TOP_LEFT_CORNER)
CURSOR_UPPER_RIGHT = Gdk.Cursor.new(Gdk.CursorType.TOP_RIGHT_CORNER)
CURSOR_LOWER_LEFT = Gdk.Cursor.new(Gdk.CursorType.BOTTOM_LEFT_CORNER)
CURSOR_LOWER_RIGHT = Gdk.Cursor.new(Gdk.CursorType.BOTTOM_RIGHT_CORNER)
CURSORS = [None,
CURSOR_UPPER_LEFT,
CURSOR_UPPER,
CURSOR_UPPER_RIGHT,
CURSOR_RIGHT,
CURSOR_LOWER_RIGHT,
CURSOR_LOWER,
CURSOR_LOWER_LEFT,
CURSOR_LEFT]
# helper functions
def grabber_position(rect):
x1, y1, x2, y2 = rect
if (x2 - x1 >= MIN_SIDE_FOR_INSIDE_GRABBERS and
y2 - y1 >= MIN_SIDE_FOR_INSIDE_GRABBERS):
return GRABBER_INSIDE
else:
return GRABBER_OUTSIDE
def grabber_generators(rect):
if grabber_position(rect) == GRABBER_INSIDE:
return INNER_GRABBERS
else:
return OUTER_GRABBERS
def switch_grabber(grabber, x1, y1, x2, y2):
switch_row = GRABBERS_SWITCH[grabber]
if x1 > x2:
if y1 > y2:
return switch_row[1]
else:
return switch_row[0]
else:
if y1 > y2:
return switch_row[2]
else:
return grabber
def inside_rect(rect, x, y):
x1, y1, x2, y2 = rect
return x1 <= x <= x2 and y1 <= y <= y2
def can_grab(rect, x, y):
"""
Checks if (x,y) lies within one of the grabbers of rect.
"""
(x1, y1, x2, y2) = rect
if (x2 - x1 >= MIN_SIDE_FOR_INSIDE_GRABBERS and
y2 - y1 >= MIN_SIDE_FOR_INSIDE_GRABBERS):
# grabbers are inside
if x < x1 or x > x2 or y < y1 or y > y2:
return None
for grabber in GRABBERS[1:]:
grabber_area = INNER_GRABBERS[grabber](x1, y1, x2, y2)
if inside_rect(grabber_area, x, y):
return grabber
return INSIDE
else:
# grabbers are outside
if x1 <= x <= x2 and y1 <= y <= y2:
return INSIDE
for grabber in GRABBERS[1:]:
grabber_area = OUTER_GRABBERS[grabber](x1, y1, x2, y2)
if inside_rect(grabber_area, x, y):
return grabber
return None

View File

@ -0,0 +1,661 @@
#
# Gramps - a GTK+/GNOME based genealogy program
#
# Copyright (C) 2013 Artem Glebov <artem.glebov@gmail.com>
#
# 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
#
# $Id$
#-------------------------------------------------------------------------
#
# Standard python modules
#
#-------------------------------------------------------------------------
from __future__ import division
#-------------------------------------------------------------------------
#
# GTK/Gnome modules
#
#-------------------------------------------------------------------------
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GdkPixbuf
from gi.repository import GObject
#-------------------------------------------------------------------------
#
# gramps modules
#
#-------------------------------------------------------------------------
from gramps.gen.display.name import displayer as name_displayer
#-------------------------------------------------------------------------
#
# grabbers constants and routines
#
#-------------------------------------------------------------------------
from .grabbers import (grabber_generators, can_grab, grabber_position,
switch_grabber, CURSORS, GRABBER_INSIDE, INSIDE,
INNER_GRABBERS, OUTER_GRABBERS, MOTION_FUNCTIONS)
#-------------------------------------------------------------------------
#
# PhotoTaggingGramplet
#
#-------------------------------------------------------------------------
RESIZE_RATIO = 1.5
MAX_ZOOM = 10
MIN_ZOOM = 0.05
MAX_SIZE = 2000
MIN_SIZE = 50
SHADING_OPACITY = 0.7
MIN_SELECTION_SIZE = 10
def scale_to_fit(orig_x, orig_y, target_x, target_y):
orig_aspect = orig_x / orig_y
target_aspect = target_x / target_y
if orig_aspect > target_aspect:
return target_x / orig_x
else:
return target_y / orig_y
def resize_keep_aspect(orig_x, orig_y, target_x, target_y):
orig_aspect = orig_x / orig_y
target_aspect = target_x / target_y
if orig_aspect > target_aspect:
return (target_x, target_x * orig_y // orig_x)
else:
return (target_y * orig_x // orig_y, target_y)
def order_coordinates(point1, point2):
"""
Returns the rectangle (x1, y1, x2, y2) based on point1 and point2,
such that x1 <= x2 and y1 <= y2.
"""
x1 = min(point1[0], point2[0])
x2 = max(point1[0], point2[0])
y1 = min(point1[1], point2[1])
y2 = max(point1[1], point2[1])
return (x1, y1, x2, y2)
class Region(object):
"""
Representation of a region of image that can be associated with
a person.
"""
def __init__(self, x1, y1, x2, y2):
self.set_coords(x1, y1, x2, y2)
self.person = None
self.mediaref = None
def coords(self):
return (self.x1, self.y1, self.x2, self.y2)
def set_coords(self, x1, y1, x2, y2):
self.x1 = x1
self.y1 = y1
self.x2 = x2
self.y2 = y2
def contains(self, x, y):
return self.x1 <= x <= self.x2 and self.y1 <= y <= self.y2
def contains_rect(self, other):
return (self.contains(other.x1, other.y1) and
self.contains(other.x2, other.y2))
def area(self):
return abs(self.x1 - self.x2) * abs(self.y1 - self.y2)
def intersects(self, other):
# assumes that x1 <= x2 and y1 <= y2
return not (self.x2 < other.x1 or self.x1 > other.x2 or
self.y2 < other.y1 or self.y1 > other.y2)
class SelectionWidget(Gtk.ScrolledWindow):
__gsignals__ = {
"region-modified": (GObject.SIGNAL_RUN_FIRST, None, ()),
"region-created": (GObject.SIGNAL_RUN_FIRST, None, ()),
"region-selected": (GObject.SIGNAL_RUN_FIRST, None, ()),
"selection-cleared": (GObject.SIGNAL_RUN_FIRST, None, ()),
"right-button-clicked": (GObject.SIGNAL_RUN_FIRST, None, ()),
"zoomed-in": (GObject.SIGNAL_RUN_FIRST, None, ()),
"zoomed-out": (GObject.SIGNAL_RUN_FIRST, None, ())
}
def __init__(self):
self.multiple_selection = True
self.loaded = False
self.start_point_screen = None
self.selection = None
self.current = None
self.in_region = None
self.grabber = None
self.regions = []
self.translation = None
self.pixbuf = None
self.scaled_pixbuf = None
self.scale = 1.0
Gtk.ScrolledWindow.__init__(self)
self.add(self.build_gui())
self.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC)
def build_gui(self):
self.image = Gtk.Image()
self.image.set_has_tooltip(True)
self.image.connect_after("draw", self.expose_handler)
self.image.connect("query-tooltip", self.show_tooltip)
self.event_box = Gtk.EventBox()
self.event_box.connect('button-press-event',
self.button_press_event)
self.event_box.connect('button-release-event',
self.button_release_event)
self.event_box.connect('motion-notify-event',
self.motion_notify_event)
self.event_box.connect('scroll-event',
self.motion_scroll_event)
self.event_box.add_events(Gdk.EventMask.BUTTON_PRESS_MASK)
self.event_box.add_events(Gdk.EventMask.BUTTON_RELEASE_MASK)
self.event_box.add_events(Gdk.EventMask.POINTER_MOTION_MASK)
self.event_box.add(self.image)
self.viewport = Gtk.Viewport()
self.viewport.add(self.event_box)
return self.viewport
# ======================================================
# field accessors
# ======================================================
def get_multiple_selection(self):
"""
Return whether multiple selection is enabled.
"""
return self.multiple_selection
def set_multiple_selection(self, enable):
"""
Enables or disables multiple selection.
"""
self.multiple_selection = enable
def set_regions(self, regions):
self.regions = regions
def get_current(self):
return self.current
def set_current(self, region):
self.current = region
def get_selection(self):
return self.selection
# ======================================================
# loading the image
# ======================================================
def load_image(self, image_path):
self.start_point_screen = None
self.selection = None
self.in_region = None
self.grabber_position = None
self.grabber_to_draw = None
try:
self.pixbuf = GdkPixbuf.Pixbuf.new_from_file(image_path)
self.original_image_size = (self.pixbuf.get_width(),
self.pixbuf.get_height())
viewport_size = self.viewport.get_allocation()
self.scale = scale_to_fit(self.pixbuf.get_width(),
self.pixbuf.get_height(),
viewport_size.width,
viewport_size.height)
self.rescale()
self.loaded = True
except (GObject.GError, OSError):
self.show_missing()
def show_missing(self):
self.pixbuf = None
self.image.set_from_stock(Gtk.STOCK_MISSING_IMAGE, Gtk.IconSize.DIALOG)
self.image.queue_draw()
# ======================================================
# utility functions for retrieving properties
# ======================================================
def is_image_loaded(self):
return self.loaded
def get_original_image_size(self):
return self.original_image_size
def get_scaled_image_size(self):
unscaled_size = self.get_original_image_size()
return (unscaled_size[0] * self.scale, unscaled_size[1] * self.scale)
def get_viewport_size(self):
rect = self.viewport.get_allocation()
return (rect.width, rect.height)
def get_used_screen_size(self):
scaled_image_size = self.get_scaled_image_size()
viewport_size = self.get_viewport_size()
return (min(scaled_image_size[0], viewport_size[0]),
min(scaled_image_size[1], viewport_size[1]))
# ======================================================
# coordinate transformations
# ======================================================
def proportional_to_real(self, coord):
"""
Translate proportional (ranging from 0 to 100) coordinates to image
coordinates (in pixels).
"""
w, h = self.original_image_size
return (int(round(coord[0] * w / 100)), int(round(coord[1] * h / 100)))
def real_to_proportional(self, coord):
"""
Translate image coordinates (in pixels) to proportional (ranging
from 0 to 100).
"""
w, h = self.original_image_size
return (int(round(coord[0] * 100 / w)), int(round(coord[1] * 100 / h)))
def proportional_to_real_rect(self, rect):
x1, y1, x2, y2 = rect
return (self.proportional_to_real((x1, y1)) +
self.proportional_to_real((x2, y2)))
def real_to_proportional_rect(self, rect):
x1, y1, x2, y2 = rect
return (self.real_to_proportional((x1, y1)) +
self.real_to_proportional((x2, y2)))
def image_to_screen(self, coords):
"""
Translate image coordinates to viewport coordinates using the current
scale and viewport size.
"""
viewport_rect = self.viewport.get_allocation()
image_rect = self.scaled_size
if image_rect[0] < viewport_rect.width:
offset_x = (image_rect[0] - viewport_rect.width) / 2
else:
offset_x = 0.0
if image_rect[1] < viewport_rect.height:
offset_y = (image_rect[1] - viewport_rect.height) / 2
else:
offset_y = 0.0
return (int(coords[0] * self.scale - offset_x),
int(coords[1] * self.scale - offset_y))
def screen_to_image(self, coords):
"""
Translate viewport coordinates to original (unscaled) image coordinates
using the current scale and viewport size.
"""
viewport_rect = self.viewport.get_allocation()
image_rect = self.scaled_size
if image_rect[0] < viewport_rect.width:
offset_x = (image_rect[0] - viewport_rect.width) / 2
else:
offset_x = 0.0
if image_rect[1] < viewport_rect.height:
offset_y = (image_rect[1] - viewport_rect.height) / 2
else:
offset_y = 0.0
return (int((coords[0] + offset_x) / self.scale),
int((coords[1] + offset_y) / self.scale))
def truncate_to_image_size(self, coords):
x, y = coords
(image_width, image_height) = self.get_original_image_size()
x = max(x, 0)
x = min(x, image_width)
y = max(y, 0)
y = min(y, image_height)
return self.proportional_to_real(self.real_to_proportional((x, y)))
def screen_to_truncated(self, coords):
return self.truncate_to_image_size(self.screen_to_image(coords))
def rect_image_to_screen(self, rect):
x1, y1, x2, y2 = rect
x1, y1 = self.image_to_screen((x1, y1))
x2, y2 = self.image_to_screen((x2, y2))
return (x1, y1, x2, y2)
# ======================================================
# drawing, refreshing and zooming the image
# ======================================================
def draw_selection(self):
if not self.scaled_size:
return
w, h = self.scaled_size
offset_x, offset_y = self.image_to_screen((0, 0))
offset_x -= 1
offset_y -= 1
cr = self.image.get_window().cairo_create()
if self.selection:
x1, y1, x2, y2 = self.rect_image_to_screen(self.selection)
# transparent shading
self.draw_transparent_shading(cr, x1, y1, x2, y2, w, h,
offset_x, offset_y)
# selection frame
self.draw_selection_frame(cr, x1, y1, x2, y2)
# draw grabber
self.draw_grabber(cr)
else:
# selection frame
for region in self.regions:
x1, y1, x2, y2 = self.rect_image_to_screen(region.coords())
self.draw_region_frame(cr, x1, y1, x2, y2)
def draw_transparent_shading(self, cr, x1, y1, x2, y2, w, h,
offset_x, offset_y):
cr.set_source_rgba(1.0, 1.0, 1.0, SHADING_OPACITY)
cr.rectangle(offset_x, offset_y, x1 - offset_x, y1 - offset_y)
cr.rectangle(offset_x, y1, x1 - offset_x, y2 - y1)
cr.rectangle(offset_x, y2, x1 - offset_x, h - y2 + offset_y)
cr.rectangle(x1, y2 + 1, x2 - x1 + 1, h - y2 + offset_y)
cr.rectangle(x2 + 1, y2 + 1, w - x2 + offset_x, h - y2 + offset_y)
cr.rectangle(x2 + 1, y1, w - x2 + offset_x, y2 - y1 + 1)
cr.rectangle(x2 + 1, offset_y, w - x2 + offset_x, y2 - offset_y)
cr.rectangle(x1, offset_y, x2 - x1 + 1, y1 - offset_y)
cr.fill()
def draw_selection_frame(self, cr, x1, y1, x2, y2):
self.draw_region_frame(cr, x1, y1, x2, y2)
def draw_region_frame(self, cr, x1, y1, x2, y2):
cr.set_source_rgb(1.0, 1.0, 1.0) # white
cr.rectangle(x1, y1, x2 - x1, y2 - y1)
cr.stroke()
cr.set_source_rgb(0.0, 0.0, 1.0) # blue
cr.rectangle(x1 - 2, y1 - 2, x2 - x1 + 4, y2 - y1 + 4)
cr.stroke()
def draw_grabber(self, cr):
if self.selection is not None and self.grabber is not None:
selection_rect = self.rect_image_to_screen(self.selection)
cr.set_source_rgb(1.0, 0, 0)
if self.grabber_position is None:
generators = grabber_generators(selection_rect)
elif self.grabber_position == GRABBER_INSIDE:
generators = INNER_GRABBERS
else:
generators = OUTER_GRABBERS
if self.grabber_to_draw is not None:
generator = generators[self.grabber_to_draw]
else:
generator = generators[self.grabber]
if generator is not None:
x1, y1, x2, y2 = generator(*selection_rect)
cr.rectangle(x1, y1, x2 - x1, y2 - y1)
cr.stroke()
def refresh(self):
self.image.queue_draw()
def rescale(self):
self.scaled_size = (int(self.original_image_size[0] * self.scale),
int(self.original_image_size[1] * self.scale))
self.scaled_image = self.pixbuf.scale_simple(self.scaled_size[0],
self.scaled_size[1],
GdkPixbuf.InterpType.BILINEAR)
self.image.set_from_pixbuf(self.scaled_image)
self.image.set_size_request(*self.scaled_size)
self.event_box.set_size_request(*self.scaled_size)
def can_zoom_in(self):
scaled_size = (self.original_image_size[0] * self.scale * RESIZE_RATIO,
self.original_image_size[1] * self.scale * RESIZE_RATIO)
return scaled_size[0] < MAX_SIZE and scaled_size[1] < MAX_SIZE
def can_zoom_out(self):
scaled_size = (self.original_image_size[0] * self.scale * RESIZE_RATIO,
self.original_image_size[1] * self.scale * RESIZE_RATIO)
return scaled_size[0] >= MIN_SIZE and scaled_size[1] >= MIN_SIZE
def zoom_in(self):
if self.can_zoom_in():
self.scale *= RESIZE_RATIO
self.rescale()
self.emit("zoomed-in")
def zoom_out(self):
if self.can_zoom_out():
self.scale /= RESIZE_RATIO
self.rescale()
self.emit("zoomed-out")
def expose_handler(self, widget, event):
if self.pixbuf:
self.draw_selection()
def select(self, region):
self.current = region
if self.current is not None:
self.selection = self.current.coords()
self.image.queue_draw()
def clear_selection(self):
self.current = None
self.selection = None
self.image.queue_draw()
# ======================================================
# managing regions
# ======================================================
def find_region(self, x, y):
result = None
for region in self.regions:
if region.contains(x, y):
if result is None or result.area() > region.area():
result = region
return result
# ======================================================
# thumbnails
# ======================================================
def get_thumbnail(self, region, thumbnail_size):
w = region.x2 - region.x1
h = region.y2 - region.y1
if w >= 1 and h >= 1:
subpixbuf = self.pixbuf.new_subpixbuf(region.x1, region.y1, w, h)
size = resize_keep_aspect(w, h, *thumbnail_size)
return subpixbuf.scale_simple(size[0], size[1],
GdkPixbuf.InterpType.BILINEAR)
else:
return None
# ======================================================
# mouse event handlers
# ======================================================
def button_press_event(self, obj, event):
if not self.is_image_loaded():
return
if event.button == 1: # left button
self.start_point_screen = (event.x, event.y)
if self.current is not None and self.grabber is None:
self.current = None
self.selection = None
self.refresh()
self.emit("selection-cleared")
elif event.button == 3: # right button
# select a region, if clicked inside one
click_point = self.screen_to_image((event.x, event.y))
self.current = self.find_region(*click_point)
self.selection = \
self.current.coords() if self.current is not None else None
self.start_point_screen = None
self.refresh()
if self.current is not None:
self.emit("region-selected")
self.emit("right-button-clicked")
else:
self.emit("selection-cleared")
return True # don't propagate the event further
def button_release_event(self, obj, event):
if not self.is_image_loaded():
return
if event.button == 1:
if self.start_point_screen:
if self.current is not None:
# a box is currently selected
if self.grabber is None:
# clicked outside of the grabbing area
self.current = None
self.selection = None
self.emit("selection-cleared")
elif self.grabber != INSIDE:
# clicked on one of the grabbers
dx, dy = (event.x - self.start_point_screen[0],
event.y - self.start_point_screen[1])
self.grabber_to_draw = self.modify_selection(dx, dy)
self.current.set_coords(*self.selection)
self.emit("region-modified")
else:
# nothing is currently selected
if (self.minimum_region(self.start_point_screen,
(event.x, event.y)) and
self.can_select()):
# region selection
region = Region(*self.selection)
self.regions.append(region)
self.current = region
self.emit("region-created")
else:
# nothing selected, just a click
click_point = \
self.screen_to_image(self.start_point_screen)
self.current = self.find_region(*click_point)
self.selection = \
self.current.coords() if self.current is not None \
else None
self.emit("region-selected")
self.start_point_screen = None
self.refresh()
def motion_notify_event(self, widget, event):
if not self.is_image_loaded():
return
end_point_orig = self.screen_to_image((event.x, event.y))
end_point = self.truncate_to_image_size(end_point_orig)
if self.start_point_screen:
# selection or dragging (mouse button pressed)
if self.grabber is not None and self.grabber != INSIDE:
# dragging the grabber
dx, dy = (event.x - self.start_point_screen[0],
event.y - self.start_point_screen[1])
self.grabber_to_draw = self.modify_selection(dx, dy)
elif self.can_select():
# making new selection
start_point = self.screen_to_truncated(self.start_point_screen)
self.selection = order_coordinates(start_point, end_point)
else:
# motion (mouse button is not pressed)
self.in_region = self.find_region(*end_point_orig)
if self.current is not None:
# a box is active, so check if the pointer is inside a grabber
rect = self.rect_image_to_screen(self.current.coords())
self.grabber = can_grab(rect, event.x, event.y)
if self.grabber is not None:
self.grabber_to_draw = self.grabber
self.grabber_position = grabber_position(rect)
self.event_box.get_window().set_cursor(CURSORS[self.grabber])
else:
self.grabber_to_draw = None
self.grabber_position = None
self.event_box.get_window().set_cursor(None)
else:
# nothing is active
self.grabber = None
self.grabber_to_draw = None
self.grabber_position = None
self.event_box.get_window().set_cursor(None)
self.image.queue_draw()
def motion_scroll_event(self, widget, event):
if not self.is_image_loaded():
return
if event.direction == Gdk.ScrollDirection.UP:
self.zoom_in()
elif event.direction == Gdk.ScrollDirection.DOWN:
self.zoom_out()
# ======================================================
# helpers for mouse event handlers
# ======================================================
def minimum_region(self, point1, point2):
return (abs(point1[0] - point2[0]) >= MIN_SELECTION_SIZE and
abs(point1[1] - point2[1]) >= MIN_SELECTION_SIZE)
def can_select(self):
return self.multiple_selection or len(self.regions) < 1
def modify_selection(self, dx, dy):
x1, y1, x2, y2 = self.rect_image_to_screen(self.current.coords())
x1, y1, x2, y2 = MOTION_FUNCTIONS[self.grabber](x1, y1, x2, y2, dx, dy)
(x1, y1) = self.screen_to_truncated((x1, y1))
(x2, y2) = self.screen_to_truncated((x2, y2))
grabber = switch_grabber(self.grabber, x1, y1, x2, y2)
self.selection = order_coordinates((x1, y1), (x2, y2))
return grabber
# ======================================================
# tooltips
# ======================================================
def show_tooltip(self, widget, x, y, keyboard_mode, tooltip):
if self.in_region:
person = self.in_region.person
if person:
name = name_displayer.display(person)
else:
return False
tooltip.set_text(name)
return True
else:
return False