Revert "Merge pull request #14 from danyeaw/master"

This reverts commit 81ebdf8641e962aadf3c14262c453b154ba78290, reversing
changes made to c9e9f81b62f6c710dd3a361e0219d148ff08a477.
This commit is contained in:
Dan Yeaw 2018-01-11 22:23:57 -05:00
parent 63b7110945
commit b4f7950f7a
52 changed files with 1538 additions and 2358 deletions

View File

@ -1,2 +0,0 @@
Dan Yeaw <dan@yeaw.me> danyeaw <danyeaw@gmail.com>
Artur Wroblewski <wrobell@pld-linux.org> wrobell <wrobell@pld-linux.org>

View File

@ -1,27 +0,0 @@
[project]
name: Gaphas
vcs: Git
[files]
authors: yes
files: yes
ignored: LICENSE.txt | README.rst | .update-copyright.conf | .git* | *.md | *.txt | MANIFEST.in | NEWS | gaphor.desktop | pylintc | pygtk.vext | tox.ini
pyfile: ../update-copyright-0.6.2/update_copyright/license.py
[copyright]
short: {project} comes with ABSOLUTELY NO WARRANTY and is licensed under the GNU General Public License.
long: This file is part of {project}.
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Library General Public
License as published by the Free Software Foundation; either
version 2 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Library General Public License for more details.
You should have received a copy of the GNU Library General Public
License along with this library; if not, see <http://www.gnu.org/licenses/>.

View File

@ -1,6 +0,0 @@
Gaphas was written by:
Adrian Boguszewski <adrbogus1@student.pg.gda.pl>
Antony N. Pavlov <antony@niisi.msk.ru>
Arjan Molenaar <gaphor@gmail.com>
Artur Wroblewski <wrobell@pld-linux.org>
Dan Yeaw <dan@yeaw.me>

View File

@ -45,8 +45,8 @@ in the item true (e.g. make sure a box maintains it's rectangular shape).
View (from view.py) is used to visualize a canvas. On a View, a Tool
(from tool.py) can be applied, which will handle user input (button presses,
key presses, etc.). Painters (from painter.py) are used to do the actual
drawing. Keep the painters modular could allow you to draw to other media than
the screen, such as a printer or PDF document.
drawing. This way it should be easy do draw to other media than the screen,
such as a printer or PDF document.
Updating item state
-------------------
@ -54,13 +54,13 @@ If an items needs updating, it sets out an update request on the Canvas
(Canvas.request_update()). The canvas performs an update by calling:
1. Item.pre_update(context) for each item marked for update
2. Updating Canvas-to-Item (c2i) matrices, for fast transformation of coordinates
2. Updating Canvas-to-Item matrices, for fast transformation of coordinates
from the canvas' to the items' coordinate system.
The c2i matrix is stored on the Item as Item._matrix_c2i.
3. Solve constraints.
4. Normalize items by setting the coordinates of the first handle to (0, 0).
5. Updating Canvas-to-Item matrices for items that have been changed by
normalization.
normalization, just to be on the save side.
6. Item.post_update(context) for each item marked for update, including items
that have been marked during constraint solving.
@ -74,15 +74,16 @@ The context contains:
NOTE: updating is done from the canvas, items should not update sub-items.
After an update, the item should be ready to be drawn.
After an update, the Item should be ready to be drawn.
Constraint solving
------------------
A word about the constraint solver since it is one of the big features of this
library. The Solver is able to solve constraints. Constraints can be applied to
items (variables owned by the item actually). For example, element items use
constraints to maintain their recangular shape. Constraints can be created
*between* items (for example a line that connects to a box).
A word about the constraint solver seems in place. It is one of the big
features of this library after all. The Solver is able to solve constraints.
Constraints can be applied to items (Variables owned by the item actually).
Element items, for example, uses constraints to maintain their recangular
shape. Constraints can be created *between* items (for example a line that
connects to a box).
Constraints that apply to one item are pretty straight forward, as all variables
live in the same coordinate system (of the item). The variables (in most cases
@ -90,19 +91,19 @@ a Handle's x and y coordinate) can simply be put in a constraint.
When two items are connected to each other and constraints are created, a
problem shows up: variables live in separate coordinate systems. To overcome
this problem a projection (from solver.py) has been defined. With a projection
this problem a Projection (from solver.py) has been defined. With a Projection
instance, a variable can be "projected" on another coordinate system. In this
case, where two items are connecting to each other, the canvas' coordinate
case, where two items are connecting to each other, the Canvas' coordinate
system is used.
Drawing
-------
Drawing is done by the View. All items marked for redraw (i.e. the items
Drawing is done by the View. All items marked for redraw (e.i. the items
that had been updated) will be drawn in the order in which they reside in the
canvas (first root item, then it's children; second root item, etc.)
Canvas (first root item, then it's children; second root item, etc.)
The view context passed to the item's draw() method has the following properties:
The view context passed to the Items draw() method has the following properties:
:view: the view we're drawing to
:cairo: the CairoContext to draw to
@ -116,7 +117,7 @@ The view context passed to the item's draw() method has the following properties
:draw_all: True if everything drawable on the item should be drawn (e.g. when
calculating the bounding boxes).
The view automatically calculates the bounding box for the item, based on the
The View automatically calculates the bounding box for the item, based on the
items drawn in the draw(context) function (this is only done when really
necessary, e.g. after an update of the item). The bounding box is in viewport
coordinates.
@ -182,7 +183,7 @@ is called at the moment the item it's connected to is removed from the canvas.
Undo
====
Gaphas has a simple built-in system for registering changes in its classes and
Gaphas has a simple build-in system for registering changes in it's classes and
notifying the application. This code resides in state.py.
There is also a "reverter" framework in place. This "framework" is notified
@ -195,6 +196,8 @@ See state.txt and undo.txt for details and usage examples.
Guidelines
==========
Documentation should be in UK English.
Following the `Python coding guidelines`_ indentation should be 4 spaces
(no tabs), function and method names should be ``lowercase_with_underscore()``.
We're using two white lines as separator between methods, as it makes method

219
demo.py
View File

@ -1,24 +1,4 @@
#!/usr/bin/env python
# Copyright (C) 2006-2017 Arjan Molenaar <gaphor@gmail.com>
# Artur Wroblewski <wrobell@pld-linux.org>
# Dan Yeaw <dan@yeaw.me>
#
# This file is part of Gaphas.
#
# This library is free software; you can redistribute it and/or modify it under
# the terms of the GNU Library General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for
# more details.
#
# You should have received a copy of the GNU Library General Public License
# along with this library; if not, see <http://www.gnu.org/licenses/>.
"""
A simple demo app.
@ -34,40 +14,39 @@ It sports a small canvas and some trivial operations:
"""
from __future__ import absolute_import
from __future__ import print_function
__version__ = "$Revision$"
# $HeadURL$
try:
import gi
import pygtk
except ImportError:
pass
else:
gi.require_version('Gtk', '3.0')
pygtk.require('2.0')
import math
from gi.repository import Gtk
import gtk
import cairo
from gaphas import Canvas, GtkView, View
from gaphas.examples import Box, PortoBox, Text, FatLine, Circle
from gaphas.item import Line
from gaphas.item import Line, NW, SE
from gaphas.tool import PlacementTool, HandleTool
from gaphas.segment import Segment
import gaphas.guide
from gaphas.painter import PainterChain, ItemPainter, HandlePainter, FocusedItemPainter, ToolPainter, BoundingBoxPainter
from gaphas import state
from gaphas.util import text_extents, text_underline
from gaphas.freehand import FreeHandPainter
__version__ = "$Revision$"
# $HeadURL$
# painter.DEBUG_DRAW_BOUNDING_BOX = True
from gaphas import painter
#painter.DEBUG_DRAW_BOUNDING_BOX = True
# Ensure data gets picked well:
import gaphas.picklers
# Global undo list
undo_list = []
def undo_handler(event):
global undo_list
undo_list.append(event)
@ -77,12 +56,10 @@ def factory(view, cls):
"""
Simple canvas item factory.
"""
def wrapper():
item = cls()
view.canvas.add(item)
return item
return wrapper
@ -90,11 +67,9 @@ class MyBox(Box):
"""Box with an example connection protocol.
"""
class MyLine(Line):
"""Line with experimental connection protocol.
"""
def __init__(self):
super(MyLine, self).__init__()
self.fuzziness = 2
@ -114,11 +89,13 @@ class MyLine(Line):
cr.stroke()
class MyText(Text):
"""
Text with experimental connection protocol.
"""
def draw(self, context):
Text.draw(self, context)
cr = context.cairo
@ -129,10 +106,11 @@ class MyText(Text):
class UnderlineText(Text):
def draw(self, context):
cr = context.cairo
text_underline(cr, 0, 0, "Some text(y)")
def create_window(canvas, title, zoom=1.0):
view = GtkView()
@ -142,32 +120,32 @@ def create_window(canvas, title, zoom=1.0):
append(FocusedItemPainter()). \
append(ToolPainter())
view.bounding_box_painter = FreeHandPainter(BoundingBoxPainter())
w = Gtk.Window()
w = gtk.Window()
w.set_title(title)
h = Gtk.HBox()
h = gtk.HBox()
w.add(h)
# VBox contains buttons that can be used to manipulate the canvas:
v = Gtk.VBox()
v = gtk.VBox()
v.set_property('border-width', 3)
v.set_property('spacing', 2)
f = Gtk.Frame()
f = gtk.Frame()
f.set_property('border-width', 1)
f.add(v)
h.pack_start(f, False, True, 0)
h.pack_start(f, expand=False)
v.add(Gtk.Label(label='Item placement:'))
b = Gtk.Button('Add box')
v.add(gtk.Label('Item placement:'))
b = gtk.Button('Add box')
def on_clicked(button, view):
# view.window.set_cursor(Gdk.Cursor.new(Gdk.CursorType.CROSSHAIR))
#view.window.set_cursor(gtk.gdk.Cursor(gtk.gdk.CROSSHAIR))
view.tool.grab(PlacementTool(view, factory(view, MyBox), HandleTool(), 2))
b.connect('clicked', on_clicked, view)
v.add(b)
b = Gtk.Button('Add line')
b = gtk.Button('Add line')
def on_clicked(button):
view.tool.grab(PlacementTool(view, factory(view, MyLine), HandleTool(), 1))
@ -175,9 +153,9 @@ def create_window(canvas, title, zoom=1.0):
b.connect('clicked', on_clicked)
v.add(b)
v.add(Gtk.Label(label='Zooming:'))
b = Gtk.Button('Zoom in')
v.add(gtk.Label('Zooming:'))
b = gtk.Button('Zoom in')
def on_clicked(button):
view.zoom(1.2)
@ -185,17 +163,17 @@ def create_window(canvas, title, zoom=1.0):
b.connect('clicked', on_clicked)
v.add(b)
b = Gtk.Button('Zoom out')
b = gtk.Button('Zoom out')
def on_clicked(button):
view.zoom(1 / 1.2)
view.zoom(1/1.2)
b.connect('clicked', on_clicked)
v.add(b)
v.add(Gtk.Label(label='Misc:'))
v.add(gtk.Label('Misc:'))
b = Gtk.Button('Split line')
b = gtk.Button('Split line')
def on_clicked(button):
if isinstance(view.focused_item, Line):
@ -206,55 +184,55 @@ def create_window(canvas, title, zoom=1.0):
b.connect('clicked', on_clicked)
v.add(b)
b = Gtk.Button('Delete focused')
b = gtk.Button('Delete focused')
def on_clicked(button):
if view.focused_item:
canvas.remove(view.focused_item)
# print 'items:', canvas.get_all_items()
#print 'items:', canvas.get_all_items()
b.connect('clicked', on_clicked)
v.add(b)
v.add(Gtk.Label(label='State:'))
b = Gtk.ToggleButton('Record')
v.add(gtk.Label('State:'))
b = gtk.ToggleButton('Record')
def on_toggled(button):
global undo_list
if button.get_active():
print('start recording')
print 'start recording'
del undo_list[:]
state.subscribers.add(undo_handler)
else:
print('stop recording')
print 'stop recording'
state.subscribers.remove(undo_handler)
b.connect('toggled', on_toggled)
v.add(b)
b = Gtk.Button('Play back')
b = gtk.Button('Play back')
def on_clicked(self):
global undo_list
apply_me = list(undo_list)
del undo_list[:]
print('Actions on the undo stack:', len(apply_me))
print 'Actions on the undo stack:', len(apply_me)
apply_me.reverse()
saveapply = state.saveapply
for event in apply_me:
print('Undo: invoking', event)
print 'Undo: invoking', event
saveapply(*event)
print('New undo stack size:', len(undo_list))
print 'New undo stack size:', len(undo_list)
# Visualize each event:
# while Gtk.events_pending():
# Gtk.main_iteration()
#while gtk.events_pending():
# gtk.main_iteration()
b.connect('clicked', on_clicked)
v.add(b)
v.add(Gtk.Label(label='Export:'))
v.add(gtk.Label('Export:'))
b = Gtk.Button('Write demo.png')
b = gtk.Button('Write demo.png')
def on_clicked(button):
svgview = View(view.canvas)
@ -267,7 +245,7 @@ def create_window(canvas, title, zoom=1.0):
svgview.update_bounding_box(tmpcr)
tmpcr.show_page()
tmpsurface.flush()
w, h = svgview.bounding_box.width, svgview.bounding_box.height
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, int(w), int(h))
cr = cairo.Context(surface)
@ -282,7 +260,7 @@ def create_window(canvas, title, zoom=1.0):
b.connect('clicked', on_clicked)
v.add(b)
b = Gtk.Button('Write demo.svg')
b = gtk.Button('Write demo.svg')
def on_clicked(button):
svgview = View(view.canvas)
@ -295,7 +273,7 @@ def create_window(canvas, title, zoom=1.0):
svgview.update_bounding_box(tmpcr)
tmpcr.show_page()
tmpsurface.flush()
w, h = svgview.bounding_box.width, svgview.bounding_box.height
surface = cairo.SVGSurface('demo.svg', w, h)
cr = cairo.Context(surface)
@ -308,7 +286,8 @@ def create_window(canvas, title, zoom=1.0):
b.connect('clicked', on_clicked)
v.add(b)
b = Gtk.Button('Dump QTree')
b = gtk.Button('Dump QTree')
def on_clicked(button, li):
view._qtree.dump()
@ -316,12 +295,13 @@ def create_window(canvas, title, zoom=1.0):
b.connect('clicked', on_clicked, [0])
v.add(b)
b = Gtk.Button('Pickle (save)')
b = gtk.Button('Pickle (save)')
def on_clicked(button, li):
f = open('demo.pickled', 'w')
try:
import six.moves.cPickle as pickle
import cPickle as pickle
pickle.dump(view.canvas, f)
finally:
f.close()
@ -329,12 +309,13 @@ def create_window(canvas, title, zoom=1.0):
b.connect('clicked', on_clicked, [0])
v.add(b)
b = Gtk.Button('Unpickle (load)')
b = gtk.Button('Unpickle (load)')
def on_clicked(button, li):
f = open('demo.pickled', 'r')
try:
import six.moves.cPickle as pickle
import cPickle as pickle
canvas = pickle.load(f)
canvas.update_now()
finally:
@ -344,23 +325,25 @@ def create_window(canvas, title, zoom=1.0):
b.connect('clicked', on_clicked, [0])
v.add(b)
b = Gtk.Button('Unpickle (in place)')
b = gtk.Button('Unpickle (in place)')
def on_clicked(button, li):
f = open('demo.pickled', 'r')
try:
import six.moves.cPickle as pickle
import cPickle as pickle
canvas = pickle.load(f)
finally:
f.close()
# [i.request_update() for i in canvas.get_all_items()]
#[i.request_update() for i in canvas.get_all_items()]
canvas.update_now()
view.canvas = canvas
b.connect('clicked', on_clicked, [0])
v.add(b)
b = Gtk.Button('Reattach (in place)')
b = gtk.Button('Reattach (in place)')
def on_clicked(button, li):
view.canvas = None
@ -369,42 +352,42 @@ def create_window(canvas, title, zoom=1.0):
b.connect('clicked', on_clicked, [0])
v.add(b)
# Add the actual View:
view.canvas = canvas
view.zoom(zoom)
view.set_size_request(150, 120)
s = Gtk.ScrolledWindow()
s.set_policy(Gtk.PolicyType.ALWAYS, Gtk.PolicyType.ALWAYS)
s = gtk.ScrolledWindow()
s.add(view)
h.add(s)
w.show_all()
w.connect('destroy', Gtk.main_quit)
w.connect('destroy', gtk.main_quit)
def handle_changed(view, item, what):
print(what, 'changed: ', item)
print what, 'changed: ', item
view.connect('focus-changed', handle_changed, 'focus')
view.connect('hover-changed', handle_changed, 'hover')
view.connect('selection-changed', handle_changed, 'selection')
def create_canvas(c=None):
if not c:
c = Canvas()
b = MyBox()
b=MyBox()
b.min_width = 20
b.min_height = 30
print('box', b)
b.matrix = (1.0, 0.0, 0.0, 1, 20, 20)
print 'box', b
b.matrix=(1.0, 0.0, 0.0, 1, 20,20)
b.width = b.height = 40
c.add(b)
bb = Box()
print('box', bb)
bb.matrix = (1.0, 0.0, 0.0, 1, 10, 10)
bb=Box()
print 'box', bb
bb.matrix=(1.0, 0.0, 0.0, 1, 10,10)
c.add(bb, parent=b)
fl = FatLine()
@ -420,14 +403,14 @@ def create_canvas(c=None):
# AJM: extra boxes:
bb = Box()
print('rotated box', bb)
bb.matrix.rotate(math.pi / 1.567)
print 'rotated box', bb
bb.matrix.rotate(math.pi/1.567)
c.add(bb, parent=b)
# for i in xrange(10):
# bb=Box()
# print 'box', bb
# bb.matrix.rotate(math.pi/4.0 * i / 10.0)
# c.add(bb, parent=b)
# for i in xrange(10):
# bb=Box()
# print 'box', bb
# bb.matrix.rotate(math.pi/4.0 * i / 10.0)
# c.add(bb, parent=b)
b = PortoBox(60, 60)
b.min_width = 40
@ -436,11 +419,11 @@ def create_canvas(c=None):
c.add(b)
t = UnderlineText()
t.matrix.translate(70, 30)
t.matrix.translate(70,30)
c.add(t)
t = MyText('Single line')
t.matrix.translate(70, 70)
t.matrix.translate(70,70)
c.add(t)
l = MyLine()
@ -454,54 +437,52 @@ def create_canvas(c=None):
off_y = 0
for align_x in (-1, 0, 1):
for align_y in (-1, 0, 1):
t = MyText('Aligned text %d/%d' % (align_x, align_y),
align_x=align_x, align_y=align_y)
t=MyText('Aligned text %d/%d' % (align_x, align_y),
align_x=align_x, align_y=align_y)
t.matrix.translate(120, 200 + off_y)
off_y += 30
c.add(t)
t = MyText('Multiple\nlines', multiline=True)
t.matrix.translate(70, 100)
t=MyText('Multiple\nlines', multiline = True)
t.matrix.translate(70,100)
c.add(t)
return c
def main():
#
# State handling (a.k.a. undo handlers)
#
##
## State handling (a.k.a. undo handlers)
##
# First, activate the revert handler:
state.observers.add(state.revert_handler)
def print_handler(event):
print('event:', event)
print 'event:', event
c = Canvas()
c=Canvas()
create_window(c, 'View created before')
create_canvas(c)
# state.subscribers.add(print_handler)
#state.subscribers.add(print_handler)
#
# Start the main application
#
##
## Start the main application
##
create_window(c, 'View created after')
Gtk.main()
gtk.main()
if __name__ == '__main__':
import sys
if '-p' in sys.argv:
print('Profiling...')
print 'Profiling...'
import hotshot, hotshot.stats
prof = hotshot.Profile('demo-gaphas.prof')
prof.runcall(main)
prof.close()

View File

@ -1,6 +1,5 @@
#!/usr/bin/env python
from __future__ import absolute_import
from demo import *
@ -11,7 +10,7 @@ if __name__ == '__main__':
cProfile.run('main()', 'demo-gaphas.prof')
p = pstats.Stats('demo-gaphas.prof')
p.strip_dirs().sort_stats('time').print_stats(40)
except ImportError as ex:
except ImportError, ex:
import hotshot, hotshot.stats
import gc
prof = hotshot.Profile('demo-gaphas.prof')

View File

@ -1,23 +1,4 @@
#!/usr/bin/env python
# Copyright (C) 2006-2017 Arjan Molenaar <gaphor@gmail.com>
# Dan Yeaw <dan@yeaw.me>
#
# This file is part of Gaphas.
#
# This library is free software; you can redistribute it and/or modify it under
# the terms of the GNU Library General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for
# more details.
#
# You should have received a copy of the GNU Library General Public License
# along with this library; if not, see <http://www.gnu.org/licenses/>.
#!python
"""Bootstrap setuptools installation
If you want to use setuptools in your package's setup.py, just include this
@ -32,8 +13,6 @@ the appropriate options to ``use_setuptools()``.
This file can also be run as a script to install or upgrade setuptools.
"""
from __future__ import absolute_import
from __future__ import print_function
import sys
DEFAULT_VERSION = "0.6c11"
DEFAULT_URL = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3]
@ -91,10 +70,10 @@ def _validate_md5(egg_name, data):
if egg_name in md5_data:
digest = md5(data).hexdigest()
if digest != md5_data[egg_name]:
print((
print >>sys.stderr, (
"md5 validation of %s failed! (Possible download problem?)"
% egg_name
), file=sys.stderr)
)
sys.exit(2)
return data
@ -124,14 +103,14 @@ def use_setuptools(
return do_download()
try:
pkg_resources.require("setuptools>="+version); return
except pkg_resources.VersionConflict as e:
except pkg_resources.VersionConflict, e:
if was_imported:
print((
print >>sys.stderr, (
"The required version of setuptools (>=%s) is not available, and\n"
"can't be installed while this script is running. Please install\n"
" a more recent version first, using 'easy_install -U setuptools'."
"\n\n(Currently using %r)"
) % (version, e.args[0]), file=sys.stderr)
) % (version, e.args[0])
sys.exit(2)
else:
del pkg_resources, sys.modules['pkg_resources'] # reload ok
@ -237,10 +216,10 @@ def main(argv, version=DEFAULT_VERSION):
os.unlink(egg)
else:
if setuptools.__version__ == '0.0.1':
print((
print >>sys.stderr, (
"You have an obsolete version of setuptools installed. Please\n"
"remove it from your system entirely before rerunning this script."
), file=sys.stderr)
)
sys.exit(2)
req = "setuptools>="+version
@ -259,8 +238,8 @@ def main(argv, version=DEFAULT_VERSION):
from setuptools.command.easy_install import main
main(argv)
else:
print("Setuptools version",version,"or greater has been installed.")
print('(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)')
print "Setuptools version",version,"or greater has been installed."
print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)'
def update_md5(filenames):
"""Update our built-in md5 registry"""
@ -283,7 +262,7 @@ def update_md5(filenames):
match = re.search("\nmd5_data = {\n([^}]+)}", src)
if not match:
print("Internal error!", file=sys.stderr)
print >>sys.stderr, "Internal error!"
sys.exit(2)
src = src[:match.start(1)] + repl + src[match.end(1):]

View File

@ -1,23 +1,3 @@
#!/usr/bin/env python
# Copyright (C) 2006-2017 Arjan Molenaar <gaphor@gmail.com>
# Dan Yeaw <dan@yeaw.me>
#
# This file is part of Gaphas.
#
# This library is free software; you can redistribute it and/or modify it under
# the terms of the GNU Library General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for
# more details.
#
# You should have received a copy of the GNU Library General Public License
# along with this library; if not, see <http://www.gnu.org/licenses/>.
"""
Gaphas
======
@ -39,15 +19,13 @@ used in Gaphas instead of the multiplier (``*``). In both the ``Canvas`` and
is used.
"""
from __future__ import absolute_import
from gaphas.canvas import Canvas
from gaphas.connector import Handle
from gaphas.item import Item, Line, Element
from gaphas.view import View, GtkView
__version__ = "$Revision$"
# $HeadURL$
from canvas import Canvas
from connector import Handle
from item import Item, Line, Element
from view import View, GtkView
# vi:sw=4:et:ai

View File

@ -1,23 +1,3 @@
#!/usr/bin/env python
# Copyright (C) 2009-2017 Arjan Molenaar <gaphor@gmail.com>
# Dan Yeaw <dan@yeaw.me>
#
# This file is part of Gaphas.
#
# This library is free software; you can redistribute it and/or modify it under
# the terms of the GNU Library General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for
# more details.
#
# You should have received a copy of the GNU Library General Public License
# along with this library; if not, see <http://www.gnu.org/licenses/>.
"""
Defines aspects for Items. Aspects form intermediate items between tools
and items.
@ -29,11 +9,9 @@ The simplegeneric module is dispatching opnly based on the first argument.
For Gaphas that's enough.
"""
from gi.repository import Gdk
import gtk.gdk
from simplegeneric import generic
from gaphas.item import Element
from gaphas.item import Item, Element
class ItemFinder(object):
@ -155,10 +133,10 @@ HandleSelection = generic(ItemHandleSelection)
@HandleSelection.when_type(Element)
class ElementHandleSelection(ItemHandleSelection):
CURSORS = (
Gdk.Cursor(Gdk.CursorType.TOP_LEFT_CORNER),
Gdk.Cursor(Gdk.CursorType.TOP_RIGHT_CORNER),
Gdk.Cursor(Gdk.CursorType.BOTTOM_RIGHT_CORNER),
Gdk.Cursor(Gdk.CursorType.BOTTOM_LEFT_CORNER))
gtk.gdk.Cursor(gtk.gdk.TOP_LEFT_CORNER),
gtk.gdk.Cursor(gtk.gdk.TOP_RIGHT_CORNER),
gtk.gdk.Cursor(gtk.gdk.BOTTOM_RIGHT_CORNER),
gtk.gdk.Cursor(gtk.gdk.BOTTOM_LEFT_CORNER) )
def select(self):
index = self.item.handles().index(self.handle)
@ -167,10 +145,11 @@ class ElementHandleSelection(ItemHandleSelection):
def unselect(self):
from view import DEFAULT_CURSOR
cursor = Gdk.Cursor(DEFAULT_CURSOR)
cursor = gtk.gdk.Cursor(DEFAULT_CURSOR)
self.view.window.set_cursor(cursor)
class ItemHandleInMotion(object):
"""
Move a handle (role is applied to the handle)
@ -228,7 +207,7 @@ class ItemHandleInMotion(object):
return None
connectable, port, glue_pos = \
view.get_port_at_point(pos, distance=distance, exclude=(item,))
view.get_port_at_point(pos, distance=distance, exclude=(item,))
# check if item and found item can be connected on closest port
if port is not None:
@ -250,7 +229,8 @@ HandleInMotion = generic(ItemHandleInMotion)
class ItemConnector(object):
GLUE_DISTANCE = 10 # Glue distance in view points
GLUE_DISTANCE = 10 # Glue distance in view points
def __init__(self, item, handle):
self.item = item
@ -292,6 +272,7 @@ class ItemConnector(object):
self.connect_handle(sink)
def connect_handle(self, sink, callback=None):
"""
Create constraint between handle of a line and port of connectable
@ -310,7 +291,8 @@ class ItemConnector(object):
constraint = sink.port.constraint(canvas, item, handle, sink.item)
canvas.connect_item(item, handle, sink.item, sink.port,
constraint, callback=callback)
constraint, callback=callback)
def disconnect(self):
"""
@ -351,9 +333,9 @@ class ItemConnectionSink(object):
ConnectionSink = generic(ItemConnectionSink)
#
# Painter aspects
#
##
## Painter aspects
##
class ItemPaintFocused(object):
"""

View File

@ -1,25 +1,3 @@
#!/usr/bin/env python
# Copyright (C) 2006-2017 Antony N. Pavlov <antony@niisi.msk.ru>
# Arjan Molenaar <gaphor@gmail.com>
# Artur Wroblewski <wrobell@pld-linux.org>
# Dan Yeaw <dan@yeaw.me>
#
# This file is part of Gaphas.
#
# This library is free software; you can redistribute it and/or modify it under
# the terms of the GNU Library General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for
# more details.
#
# You should have received a copy of the GNU Library General Public License
# along with this library; if not, see <http://www.gnu.org/licenses/>.
"""
A Canvas owns a set of Items and acts as a container for both the items
and a constraint solver.
@ -49,20 +27,19 @@ To get connecting items (i.e. all lines connected to a class)::
"""
from __future__ import absolute_import
__version__ = "$Revision$"
# $HeadURL$
import logging
from collections import namedtuple
import logging
from cairo import Matrix
from six.moves import map
from six.moves import range
from gaphas import tree
from gaphas import solver
from gaphas import table
from gaphas import tree
from gaphas.decorators import nonrecursive, async, PRIORITY_HIGH_IDLE
from .state import observed, reversible_method, reversible_pair
from state import observed, reversible_method, reversible_pair
#
# Information about two connected items
@ -75,7 +52,7 @@ from .state import observed, reversible_method, reversible_pair
# - callback: optional disconnection callback
#
Connection = namedtuple('Connection',
'item handle connected port constraint callback')
'item handle connected port constraint callback')
class ConnectionError(Exception):
@ -103,7 +80,7 @@ class Context(object):
self.__dict__.update(**kwargs)
def __setattr__(self, key, value):
raise AttributeError('context is not writable')
raise AttributeError, 'context is not writable'
class Canvas(object):
@ -114,7 +91,7 @@ class Canvas(object):
def __init__(self):
self._tree = tree.Tree()
self._solver = solver.Solver()
self._connections = table.Table(Connection, list(range(4)))
self._connections = table.Table(Connection, range(4))
self._dirty_items = set()
self._dirty_matrix_items = set()
self._dirty_index = False
@ -123,6 +100,7 @@ class Canvas(object):
solver = property(lambda s: s._solver)
@observed
def add(self, item, parent=None, index=None):
"""
@ -147,6 +125,7 @@ class Canvas(object):
self.request_update(item)
@observed
def _remove(self, item):
"""
@ -159,6 +138,7 @@ class Canvas(object):
self._dirty_items.discard(item)
self._dirty_matrix_items.discard(item)
def remove(self, item):
"""
Remove item from the canvas.
@ -179,7 +159,8 @@ class Canvas(object):
reversible_pair(add, _remove,
bind1={'parent': lambda self, item: self.get_parent(item),
'index': lambda self, item: self._tree.get_siblings(item).index(item)})
'index': lambda self, item: self._tree.get_siblings(item).index(item) })
@observed
def reparent(self, item, parent, index=None):
@ -192,7 +173,8 @@ class Canvas(object):
reversible_method(reparent, reverse=reparent,
bind={'parent': lambda self, item: self.get_parent(item),
'index': lambda self, item: self._tree.get_siblings(item).index(item)})
'index': lambda self, item: self._tree.get_siblings(item).index(item) })
def get_all_items(self):
"""
@ -209,6 +191,7 @@ class Canvas(object):
"""
return self._tree.nodes
def get_root_items(self):
"""
Return the root items of the canvas.
@ -226,6 +209,7 @@ class Canvas(object):
"""
return self._tree.get_children(None)
def get_parent(self, item):
"""
See `tree.Tree.get_parent()`.
@ -242,6 +226,7 @@ class Canvas(object):
"""
return self._tree.get_parent(item)
def get_ancestors(self, item):
"""
See `tree.Tree.get_ancestors()`.
@ -263,6 +248,7 @@ class Canvas(object):
"""
return self._tree.get_ancestors(item)
def get_children(self, item):
"""
See `tree.Tree.get_children()`.
@ -284,6 +270,7 @@ class Canvas(object):
"""
return self._tree.get_children(item)
def get_all_children(self, item):
"""
See `tree.Tree.get_all_children()`.
@ -305,6 +292,7 @@ class Canvas(object):
"""
return self._tree.get_all_children(item)
@observed
def connect_item(self, item, handle, connected, port, constraint=None, callback=None):
"""
@ -340,6 +328,7 @@ class Canvas(object):
if constraint:
self._solver.add_constraint(constraint)
def disconnect_item(self, item, handle=None):
"""
Disconnect the connections of an item. If handle is not None, only the
@ -349,6 +338,7 @@ class Canvas(object):
for cinfo in list(self._connections.query(item=item, handle=handle)):
self._disconnect_item(*cinfo)
@observed
def _disconnect_item(self, item, handle, connected, port, constraint, callback):
"""
@ -365,6 +355,7 @@ class Canvas(object):
reversible_pair(connect_item, _disconnect_item)
def remove_connections_to_item(self, item):
"""
Remove all connections (handles connected to and constraints)
@ -380,6 +371,7 @@ class Canvas(object):
for cinfo in list(self._connections.query(connected=item)):
disconnect_item(*cinfo)
@observed
def reconnect_item(self, item, handle, constraint=None):
"""
@ -428,7 +420,7 @@ class Canvas(object):
# checks:
cinfo = self.get_connection(handle)
if not cinfo:
raise ValueError('No data available for item "%s" and handle "%s"' % (item, handle))
raise ValueError, 'No data available for item "%s" and handle "%s"' % (item, handle)
if cinfo.constraint:
self._solver.remove_constraint(cinfo.constraint)
@ -439,7 +431,8 @@ class Canvas(object):
self._solver.add_constraint(constraint)
reversible_method(reconnect_item, reverse=reconnect_item,
bind={'constraint': lambda self, item, handle: self.get_connection(handle).constraint})
bind={'constraint': lambda self, item, handle: self.get_connection(handle).constraint })
def get_connection(self, handle):
"""
@ -460,10 +453,11 @@ class Canvas(object):
>>> c.get_connection(ii.handles()[0]) # doctest: +ELLIPSIS
"""
try:
return next(self._connections.query(handle=handle))
except StopIteration as ex:
return self._connections.query(handle=handle).next()
except StopIteration, ex:
return None
def get_connections(self, item=None, handle=None, connected=None, port=None):
"""
Return an iterator of connection information.
@ -497,9 +491,10 @@ class Canvas(object):
[Connection(item=<gaphas.item.Line object at 0x...]
"""
return self._connections.query(item=item,
handle=handle,
connected=connected,
port=port)
handle=handle,
connected=connected,
port=port)
def sort(self, items, reverse=False):
"""
@ -523,6 +518,7 @@ class Canvas(object):
"""
return self._tree.sort(items, index_key='_canvas_index', reverse=reverse)
def get_matrix_i2c(self, item, calculate=False):
"""
Get the Item to Canvas matrix for ``item``.
@ -539,6 +535,7 @@ class Canvas(object):
self.update_matrix(item)
return item._matrix_i2c
def get_matrix_c2i(self, item, calculate=False):
"""
Get the Canvas to Item matrix for ``item``.
@ -557,10 +554,11 @@ class Canvas(object):
# Fall back to old behaviour
return i2c * c2i
@observed
def request_update(self, item, update=True, matrix=True):
"""
Set an update request for the item.
Set an update request for the item.
>>> c = Canvas()
>>> from gaphas import item
@ -583,12 +581,14 @@ class Canvas(object):
reversible_method(request_update, reverse=request_update)
def request_matrix_update(self, item):
"""
Schedule only the matrix to be updated.
"""
self.request_update(item, update=False, matrix=True)
def require_update(self):
"""
Returns ``True`` or ``False`` depending on if an update is needed.
@ -607,6 +607,7 @@ class Canvas(object):
"""
return bool(self._dirty_items)
@async(single=True, priority=PRIORITY_HIGH_IDLE)
def update(self):
"""
@ -615,17 +616,20 @@ class Canvas(object):
"""
self.update_now()
def _pre_update_items(self, items, cr):
context_map = dict()
c = Context(cairo=cr)
for item in items:
item.pre_update(c)
def _post_update_items(self, items, cr):
c = Context(cairo=cr)
for item in items:
item.post_update(c)
def _extend_dirty_items(self, dirty_items):
# item's can be marked dirty due to external constraints solving
if self._dirty_items:
@ -671,8 +675,7 @@ class Canvas(object):
self.update_constraints(dirty_matrix_items)
# no matrix can change during constraint solving
assert not self._dirty_matrix_items, 'No matrices may have been marked dirty (%s)' % (
self._dirty_matrix_items,)
assert not self._dirty_matrix_items, 'No matrices may have been marked dirty (%s)' % (self._dirty_matrix_items,)
# item's can be marked dirty due to external constraints solving
extend_dirty_items(dirty_items)
@ -696,14 +699,15 @@ class Canvas(object):
self._post_update_items(dirty_items, cr)
except Exception as e:
except Exception, e:
logging.error('Error while updating canvas', exc_info=e)
assert len(self._dirty_items) == 0 and len(self._dirty_matrix_items) == 0, \
'dirty: %s; matrix: %s' % (self._dirty_items, self._dirty_matrix_items)
'dirty: %s; matrix: %s' % (self._dirty_items, self._dirty_matrix_items)
self._update_views(dirty_items, dirty_matrix_items)
def update_matrices(self, items):
"""
Recalculate matrices of the items. Items' children matrices are
@ -727,6 +731,7 @@ class Canvas(object):
return changed
def update_matrix(self, item, parent=None):
"""
Update matrices of an item.
@ -750,6 +755,7 @@ class Canvas(object):
item._matrix_c2i = Matrix(*item._matrix_i2c)
item._matrix_c2i.invert()
def update_constraints(self, items):
"""
Update constraints. Also variables may be marked as dirty before the
@ -765,6 +771,7 @@ class Canvas(object):
# solve all constraints
self._solver.solve()
def _normalize(self, items):
"""
Update handle positions of items, so the first handle is always
@ -804,6 +811,7 @@ class Canvas(object):
return dirty_matrix_items
def update_index(self):
"""
Provide each item in the canvas with an index attribute. This makes
@ -811,6 +819,7 @@ class Canvas(object):
"""
self._tree.index_nodes('_canvas_index')
def register_view(self, view):
"""
Register a view on this canvas. This method is called when setting
@ -818,6 +827,7 @@ class Canvas(object):
"""
self._registered_views.add(view)
def unregister_view(self, view):
"""
Unregister a view on this canvas. This method is called when setting
@ -825,6 +835,7 @@ class Canvas(object):
"""
self._registered_views.discard(view)
def _update_views(self, dirty_items=(), dirty_matrix_items=(), removed_items=()):
"""
Send an update notification to all registered views.
@ -832,6 +843,7 @@ class Canvas(object):
for v in self._registered_views:
v.request_update(dirty_items, dirty_matrix_items, removed_items)
def _obtain_cairo_context(self):
"""
Try to obtain a Cairo context.
@ -855,6 +867,7 @@ class Canvas(object):
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, 0, 0)
return cairo.Context(surface)
def __getstate__(self):
"""
Persist canvas. Dirty item sets and views are not saved.
@ -867,6 +880,7 @@ class Canvas(object):
pass
return d
def __setstate__(self, state):
"""
Load persisted state.
@ -878,7 +892,8 @@ class Canvas(object):
self._dirty_matrix_items = set(self._tree.nodes)
self._dirty_index = True
self._registered_views = set()
# self.update()
#self.update()
def project(self, item, *points):
"""
@ -888,7 +903,6 @@ class Canvas(object):
returned. If there are more than one points, then tuple of
projected points is returned.
"""
def reg(cp):
item._canvas_projections.add(cp)
return cp
@ -907,7 +921,7 @@ class VariableProjection(solver.Projection):
The value has been set in the "other" coordinate system. A callback is
executed when the value changes.
It's a simple Variable-like class, following the Projection protocol:
>>> def notify_me(val):
@ -989,9 +1003,9 @@ class CanvasProjection(object):
self._px, self._py = item.canvas.get_matrix_i2c(item).transform_point(x, y)
return self._px, self._py
pos = property(lambda self: list(map(VariableProjection,
self._point, self._get_value(),
(self._on_change_x, self._on_change_y))))
pos = property(lambda self: map(VariableProjection,
self._point, self._get_value(),
(self._on_change_x, self._on_change_y)))
def __getitem__(self, key):
# Note: we can not use bound methods as callbacks, since that will
@ -1007,7 +1021,7 @@ __test__ = {
'Canvas.add': Canvas.add,
'Canvas.remove': Canvas.remove,
'Canvas.request_update': Canvas.request_update,
}
}
# vim:sw=4:et:ai

View File

@ -1,34 +1,11 @@
#!/usr/bin/env python
# Copyright (C) 2008-2017 Arjan Molenaar <gaphor@gmail.com>
# Artur Wroblewski <wrobell@pld-linux.org>
# Dan Yeaw <dan@yeaw.me>
#
# This file is part of Gaphas.
#
# This library is free software; you can redistribute it and/or modify it under
# the terms of the GNU Library General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for
# more details.
#
# You should have received a copy of the GNU Library General Public License
# along with this library; if not, see <http://www.gnu.org/licenses/>.
"""
Basic connectors such as Ports and Handles.
"""
from __future__ import absolute_import
__version__ = "$Revision: 2341 $"
# $HeadURL: https://svn.devjavu.com/gaphor/gaphas/trunk/gaphas/item.py $
from gaphas.solver import solvable, NORMAL
from gaphas.solver import solvable, WEAK, NORMAL, STRONG, VERY_STRONG
from gaphas.state import observed, reversible_property
from gaphas.geometry import distance_line_point, distance_point_point
from gaphas.constraint import LineConstraint, PositionConstraint
@ -86,7 +63,6 @@ class Position(object):
def __str__(self):
return '<%s object on (%g, %g)>' % (self.__class__.__name__, float(self.x), float(self.y))
__repr__ = __str__
def __getitem__(self, index):
@ -122,6 +98,7 @@ class Handle(object):
self._movable = movable
self._visible = True
def _set_pos(self, pos):
"""
Shortcut for ``handle.pos.pos = pos``
@ -157,27 +134,30 @@ class Handle(object):
y = property(deprecated(_get_y), deprecated(_set_y))
@observed
def _set_connectable(self, connectable):
self._connectable = connectable
connectable = reversible_property(lambda s: s._connectable, _set_connectable)
@observed
def _set_movable(self, movable):
self._movable = movable
movable = reversible_property(lambda s: s._movable, _set_movable)
@observed
def _set_visible(self, visible):
self._visible = visible
visible = reversible_property(lambda s: s._visible, _set_visible)
def __str__(self):
return '<%s object on (%g, %g)>' % (self.__class__.__name__, float(self._pos.x), float(self._pos.y))
__repr__ = __str__
@ -191,18 +171,21 @@ class Port(object):
self._connectable = True
@observed
def _set_connectable(self, connectable):
self._connectable = connectable
connectable = reversible_property(lambda s: s._connectable, _set_connectable)
def glue(self, pos):
"""
Get glue point on the port and distance to the port.
"""
raise NotImplemented('Glue method not implemented')
def constraint(self, canvas, item, handle, glue_item):
"""
Create connection constraint between item's handle and glue item.
@ -221,6 +204,7 @@ class LinePort(Port):
self.start = start
self.end = end
def glue(self, pos):
"""
Get glue point on the port and distance to the port.
@ -235,6 +219,7 @@ class LinePort(Port):
d, pl = distance_line_point(self.start, self.end, pos)
return pl, d
def constraint(self, canvas, item, handle, glue_item):
"""
Create connection line constraint between item's handle and the
@ -254,6 +239,7 @@ class PointPort(Port):
super(PointPort, self).__init__()
self.point = point
def glue(self, pos):
"""
Get glue point on the port and distance to the port.
@ -266,6 +252,7 @@ class PointPort(Port):
d = distance_point_point(self.point, pos)
return self.point, d
def constraint(self, canvas, item, handle, glue_item):
"""
Return connection position constraint between item's handle and the
@ -274,6 +261,7 @@ class PointPort(Port):
origin = canvas.project(glue_item, self.point)
point = canvas.project(item, handle.pos)
c = PositionConstraint(origin, point)
return c # PositionConstraint(origin, point)
return c #PositionConstraint(origin, point)
# vim: sw=4:et:ai

View File

@ -1,24 +1,3 @@
#!/usr/bin/env python
# Copyright (C) 2006-2017 Arjan Molenaar <gaphor@gmail.com>
# Artur Wroblewski <wrobell@pld-linux.org>
# Dan Yeaw <dan@yeaw.me>
#
# This file is part of Gaphas.
#
# This library is free software; you can redistribute it and/or modify it under
# the terms of the GNU Library General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for
# more details.
#
# You should have received a copy of the GNU Library General Public License
# along with this library; if not, see <http://www.gnu.org/licenses/>.
"""
This module contains several flavors of constraint classes.
Each has a method `Constraint.solve_for(name)` and a method
@ -49,12 +28,11 @@ implement `Constraint.solve_for(Variable)` method to update a variable with
appropriate value.
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import operator
import math
from gaphas.solver import Projection
from solver import Projection
__version__ = "$Revision$"
# $HeadURL$
@ -63,7 +41,6 @@ __version__ = "$Revision$"
# is simple abs(x - y) > EPSILON enough for canvas needs?
EPSILON = 1e-6
def _update(variable, value):
if abs(variable.value - value) > EPSILON:
variable.value = value
@ -96,6 +73,7 @@ class Constraint(object):
# Used by the Solver for efficiency
self._solver_has_projections = False
def create_weakest_list(self):
"""
Create list of weakest variables.
@ -104,6 +82,7 @@ class Constraint(object):
strength = min(v.strength for v in self._variables)
self._weakest = [v for v in self._variables if v.strength == strength]
def variables(self):
"""
Return an iterator which iterates over the variables that are
@ -111,6 +90,7 @@ class Constraint(object):
"""
return self._variables
def weakest(self):
"""
Return the weakest variable. The weakest variable should be always
@ -118,6 +98,7 @@ class Constraint(object):
"""
return self._weakest[0]
def mark_dirty(self, v):
"""
Mark variable v dirty and if possible move it to the end of
@ -158,6 +139,7 @@ class Constraint(object):
raise NotImplemented
class EqualsConstraint(Constraint):
"""
Constraint, which ensures that two arguments ``a`` and ``b`` are equal:
@ -183,12 +165,16 @@ class EqualsConstraint(Constraint):
self.b = b
self.delta = delta
def solve_for(self, var):
assert var in (self.a, self.b, self.delta)
_update(*((var is self.a) and (self.a, self.b.value - self.delta) or (var is self.b) and
(self.b, self.a.value + self.delta) or
(self.delta, self.b.value - self.a.value)))
_update(*((var is self.a) and \
(self.a, self.b.value - self.delta) or \
(var is self.b) and \
(self.b, self.a.value + self.delta) or \
(self.delta, self.b.value - self.a.value)))
class CenterConstraint(Constraint):
@ -219,6 +205,7 @@ class CenterConstraint(Constraint):
self.b = b
self.center = center
def solve_for(self, var):
assert var in (self.a, self.b, self.center)
@ -226,6 +213,7 @@ class CenterConstraint(Constraint):
_update(self.center, v)
class LessThanConstraint(Constraint):
"""
Ensure ``smaller`` is less than ``bigger``. The variable that is passed
@ -258,6 +246,7 @@ class LessThanConstraint(Constraint):
self.bigger = bigger
self.delta = delta
def solve_for(self, var):
if self.smaller.value > self.bigger.value - self.delta:
if var is self.smaller:
@ -268,10 +257,11 @@ class LessThanConstraint(Constraint):
self.delta.value = self.bigger.value - self.smaller.value
# Constants for the EquationConstraint
ITERLIMIT = 1000 # iteration limit
# Constants for the EquationConstraint
ITERLIMIT = 1000 # iteration limit
class EquationConstraint(Constraint):
"""
Equation solver using attributes and introspection.
@ -295,21 +285,23 @@ class EquationConstraint(Constraint):
"""
def __init__(self, f, **args):
super(EquationConstraint, self).__init__(*list(args.values()))
super(EquationConstraint, self).__init__(*args.values())
self._f = f
self._args = {}
# see important note on order of operations in __setattr__ below.
for arg in f.__code__.co_varnames[0:f.__code__.co_argcount]:
for arg in f.func_code.co_varnames[0:f.func_code.co_argcount]:
self._args[arg] = None
self._set(**args)
def __repr__(self):
argstring = ', '.join(['%s=%s' % (arg, str(value)) for (arg, value) in
self._args.items()])
self._args.items()])
if argstring:
return 'EquationConstraint(%s, %s)' % (self._f.__code__.co_name, argstring)
return 'EquationConstraint(%s, %s)' % (self._f.func_code.co_name, argstring)
else:
return 'EquationConstraint(%s)' % self._f.__code__.co_name
return 'EquationConstraint(%s)' % self._f.func_code.co_name
def __getattr__(self, name):
"""
@ -318,6 +310,7 @@ class EquationConstraint(Constraint):
self._args[name]
return self.solve_for(name)
def __setattr__(self, name, value):
"""
Sets function argument values.
@ -326,16 +319,17 @@ class EquationConstraint(Constraint):
# be added to self.__dict__. This is a good thing as it throws
# an exception if you try to assign to an arg which is inappropriate
# for the function in the solver.
if '_args' in self.__dict__:
if self.__dict__.has_key('_args'):
if name in self._args:
self._args[name] = value
elif name in self.__dict__:
self.__dict__[name] = value
else:
raise KeyError(name)
raise KeyError, name
else:
object.__setattr__(self, name, value)
def _set(self, **args):
"""
Sets values of function arguments.
@ -344,6 +338,7 @@ class EquationConstraint(Constraint):
self._args[arg] # raise exception if arg not in _args
setattr(self, arg, args[arg])
def solve_for(self, var):
"""
Solve this constraint for the variable named 'arg' in the
@ -357,12 +352,13 @@ class EquationConstraint(Constraint):
if var.value != v:
var.value = v
def _solve_for(self, arg, args):
"""
Newton's method solver
"""
# args = self._args
close_runs = 10 # after getting close, do more passes
#args = self._args
close_runs = 10 # after getting close, do more passes
if args[arg]:
x0 = args[arg]
else:
@ -370,38 +366,36 @@ class EquationConstraint(Constraint):
if x0 == 0:
x1 = 1
else:
x1 = x0 * 1.1
x1 = x0*1.1
def f(x):
"""function to solve"""
args[arg] = x
return self._f(**args)
fx0 = f(x0)
n = 0
while 1: # Newton's method loop here
while 1: # Newton's method loop here
fx1 = f(x1)
if fx1 == 0 or x1 == x0: # managed to nail it exactly
break
if abs(fx1 - fx0) < EPSILON: # very close
if abs(fx1-fx0) < EPSILON: # very close
close_flag = True
if close_runs == 0: # been close several times
if close_runs == 0: # been close several times
break
else:
close_runs -= 1 # try some more
close_runs -= 1 # try some more
else:
close_flag = False
if n > ITERLIMIT:
print("Failed to converge; exceeded iteration limit")
print "Failed to converge; exceeded iteration limit"
break
slope = (fx1 - fx0) / (x1 - x0)
if slope == 0:
if close_flag: # we're close but have zero slope, finish
break
else:
print('Zero slope and not close enough to solution')
print 'Zero slope and not close enough to solution'
break
x2 = x0 - fx0 / slope # New 'x1'
x2 = x0 - fx0 / slope # New 'x1'
fx0 = fx1
x0 = x1
x1 = x2
@ -409,6 +403,7 @@ class EquationConstraint(Constraint):
return x1
class BalanceConstraint(Constraint):
"""
Ensure that a variable ``v`` is between values specified by ``band``
@ -447,6 +442,7 @@ class BalanceConstraint(Constraint):
if self.balance is None:
self.update_balance()
def update_balance(self):
b1, b2 = self.band
w = b2 - b1
@ -455,6 +451,7 @@ class BalanceConstraint(Constraint):
else:
self.balance = 0
def solve_for(self, var):
b1, b2 = self.band
w = b2.value - b1.value
@ -462,6 +459,7 @@ class BalanceConstraint(Constraint):
_update(var, value)
class LineConstraint(Constraint):
"""
Ensure a point is kept on a line.
@ -478,6 +476,7 @@ class LineConstraint(Constraint):
self._point = point
self.update_ratio()
def update_ratio(self):
"""
>>> from gaphas.solver import Variable
@ -508,13 +507,15 @@ class LineConstraint(Constraint):
except ZeroDivisionError:
self.ratio_y = 0.0
def solve_for(self, var=None):
self._solve()
def _solve(self):
"""
Solve the equation for the connected_handle.
>>> from gaphas.solver import Variable
>>> line = (Variable(0), Variable(0)), (Variable(30), Variable(20))
>>> point = (Variable(15), Variable(4))
@ -540,6 +541,7 @@ class LineConstraint(Constraint):
_update(py, y)
class PositionConstraint(Constraint):
"""
Ensure that point is always in origin position.
@ -551,11 +553,12 @@ class PositionConstraint(Constraint):
def __init__(self, origin, point):
super(PositionConstraint, self).__init__(origin[0], origin[1],
point[0], point[1])
point[0], point[1])
self._origin = origin
self._point = point
def solve_for(self, var=None):
"""
Ensure that point's coordinates are the same as coordinates of the
@ -566,6 +569,7 @@ class PositionConstraint(Constraint):
_update(self._point[1], y)
class LineAlignConstraint(Constraint):
"""
Ensure a point is kept on a line in position specified by align and padding
@ -601,6 +605,7 @@ class LineAlignConstraint(Constraint):
self._align = align
self._delta = delta
def solve_for(self, var=None):
sx, sy = self._line[0]
ex, ey = self._line[1]
@ -613,4 +618,5 @@ class LineAlignConstraint(Constraint):
_update(px, x)
_update(py, y)
# vim:sw=4:et:ai

View File

@ -1,38 +1,16 @@
#!/usr/bin/env python
# Copyright (C) 2006-2017 Adrian Boguszewski <adrbogus1@student.pg.gda.pl>
# Arjan Molenaar <gaphor@gmail.com>
# Artur Wroblewski <wrobell@pld-linux.org>
# Dan Yeaw <dan@yeaw.me>
#
# This file is part of Gaphas.
#
# This library is free software; you can redistribute it and/or modify it under
# the terms of the GNU Library General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for
# more details.
#
# You should have received a copy of the GNU Library General Public License
# along with this library; if not, see <http://www.gnu.org/licenses/>.
"""
Custom decorators.
"""
from __future__ import absolute_import
from __future__ import print_function
__version__ = "$Revision$"
# $HeadURL$
import threading
from gi.repository import GObject
from gi.repository.GLib import PRIORITY_HIGH, PRIORITY_HIGH_IDLE, PRIORITY_DEFAULT, \
import gobject
from gobject import PRIORITY_HIGH, PRIORITY_HIGH_IDLE, PRIORITY_DEFAULT, \
PRIORITY_DEFAULT_IDLE, PRIORITY_LOW
DEBUG_ASYNC = False
@ -56,28 +34,28 @@ class async(object):
'Hi'
Simple method:
>>> class A(object):
... @async(single=False, priority=GObject.PRIORITY_HIGH)
... @async(single=False, priority=gobject.PRIORITY_HIGH)
... def a(self):
... print 'idle-a', GObject.main_depth()
... print 'idle-a', gobject.main_depth()
Methods can also set single mode to True (the method is only scheduled one).
>>> class B(object):
... @async(single=True)
... def b(self):
... print 'idle-b', GObject.main_depth()
... print 'idle-b', gobject.main_depth()
Also a timeout property can be provided:
>>> class C(object):
... @async(timeout=50)
... def c1(self):
... print 'idle-c1', GObject.main_depth()
... print 'idle-c1', gobject.main_depth()
... @async(single=True, timeout=60)
... def c2(self):
... print 'idle-c2', GObject.main_depth()
... print 'idle-c2', gobject.main_depth()
This is a helper function used to test classes A and B from within the GTK+
main loop:
@ -98,11 +76,11 @@ class async(object):
... a.a()
... b.b()
... print 'after'
... GObject.timeout_add(100, Gtk.main_quit)
>>> GObject.timeout_add(1, delayed) > 0 # timeout id may vary
... gobject.timeout_add(100, gtk.main_quit)
>>> gobject.timeout_add(1, delayed) > 0 # timeout id may vary
True
>>> from gi.repository import Gtk
>>> Gtk.main()
>>> import gtk
>>> gtk.main()
before
after
idle-a 1
@ -117,7 +95,7 @@ class async(object):
executed once.
"""
def __init__(self, single=False, timeout=0, priority=PRIORITY_DEFAULT):
def __init__(self, single=False, timeout=0, priority=gobject.PRIORITY_DEFAULT):
self.single = single
self.timeout = timeout
self.priority = priority
@ -125,9 +103,9 @@ class async(object):
def source(self, func):
timeout = self.timeout
if timeout > 0:
s = GObject.Timeout(timeout)
s = gobject.Timeout(timeout)
else:
s = GObject.Idle()
s = gobject.Idle()
s.set_callback(func)
s.priority = self.priority
return s
@ -139,12 +117,11 @@ class async(object):
def wrapper(*args, **kwargs):
global getattr, setattr, delattr
# execute directly if we're not in the main loop.
if GObject.main_depth() == 0:
if gobject.main_depth() == 0:
return func(*args, **kwargs)
elif not self.single:
def async_wrapper(*x):
if DEBUG_ASYNC:
print('async:', func, args, kwargs)
def async_wrapper():
if DEBUG_ASYNC: print 'async:', func, args, kwargs
func(*args, **kwargs)
source(async_wrapper).attach()
else:
@ -153,10 +130,9 @@ class async(object):
try:
if getattr(holder, async_id):
return
except AttributeError as e:
def async_wrapper(*x):
if DEBUG_ASYNC:
print('async:', func, args, kwargs)
except AttributeError, e:
def async_wrapper():
if DEBUG_ASYNC: print 'async:', func, args, kwargs
try:
func(*args, **kwargs)
finally:
@ -182,7 +158,6 @@ def nonrecursive(func):
1
"""
m = threading.Lock()
def wrapper(*args, **kwargs):
"""
Decorate function with a mutex that prohibits recursice execution.

View File

@ -1,39 +1,16 @@
#!/usr/bin/env python
# Copyright (C) 2006-2017 Arjan Molenaar <gaphor@gmail.com>
# Artur Wroblewski <wrobell@pld-linux.org>
# Dan Yeaw <dan@yeaw.me>
#
# This file is part of Gaphas.
#
# This library is free software; you can redistribute it and/or modify it under
# the terms of the GNU Library General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for
# more details.
#
# You should have received a copy of the GNU Library General Public License
# along with this library; if not, see <http://www.gnu.org/licenses/>.
"""
Simple example items.
These items are used in various tests.
"""
from __future__ import absolute_import
__version__ = "$Revision$"
# $HeadURL$
from gaphas.item import Element, Item, NW, NE, SW, SE
from gaphas.item import Element, Item, NW, NE,SW, SE
from gaphas.connector import Handle, PointPort, LinePort, Position
from gaphas.solver import WEAK
from .util import text_align, text_multiline, path_ellipse
from gaphas.solver import solvable, WEAK
import tool
from util import text_align, text_multiline, path_ellipse
class Box(Element):
""" A Box has 4 handles (for a start):
@ -49,18 +26,18 @@ class Box(Element):
nw = self._handles[NW].pos
c.rectangle(nw.x, nw.y, self.width, self.height)
if context.hovered:
c.set_source_rgba(.8, .8, 1, .8)
c.set_source_rgba(.8,.8,1, .8)
else:
c.set_source_rgba(1, 1, 1, .8)
c.set_source_rgba(1,1,1, .8)
c.fill_preserve()
c.set_source_rgb(0, 0, 0.8)
c.set_source_rgb(0,0,0.8)
c.stroke()
class PortoBox(Box):
"""
Box item with few falvours of port(o)s.
Box item with few flavours of port(o)s.
Default box ports are disabled. Three, non-default connectable ports
are created (represented by ``x`` on the picture).
@ -76,7 +53,6 @@ class PortoBox(Box):
SW +--------+ SE
x
"""
def __init__(self, width=10, height=10):
super(PortoBox, self).__init__(width, height)
@ -112,6 +88,7 @@ class PortoBox(Box):
self._lport = LinePort(nw.pos, se.pos)
self._ports.append(self._lport)
def draw(self, context):
super(PortoBox, self).draw(context)
c = context.cairo
@ -123,12 +100,12 @@ class PortoBox(Box):
# draw movable port
x, y = self._hm.pos
c.rectangle(x - 20, y - 5, 20, 10)
c.rectangle(x - 1, y - 1, 2, 2)
c.rectangle(x - 20 , y - 5, 20, 10)
c.rectangle(x - 1 , y - 1, 2, 2)
# draw static port
x, y = self._sport.point
c.rectangle(x - 2, y - 2, 4, 4)
c.rectangle(x - 2 , y - 2, 4, 4)
c.fill_preserve()
@ -138,10 +115,12 @@ class PortoBox(Box):
c.move_to(x1, y1)
c.line_to(x2, y2)
c.set_source_rgb(0, 0, 0.8)
c.set_source_rgb(0,0,0.8)
c.stroke()
class Text(Item):
"""
Simple item showing some text on the canvas.
@ -156,7 +135,7 @@ class Text(Item):
self.align_y = align_y
def draw(self, context):
# print 'Text.draw', self
#print 'Text.draw', self
cr = context.cairo
if self.multiline:
text_multiline(cr, 0, 0, self.text)
@ -175,7 +154,6 @@ class FatLine(Item):
todo: rectangle port instead of line port would be nicer
"""
def __init__(self):
super(FatLine, self).__init__()
self._handles.extend((Handle(), Handle()))
@ -187,16 +165,20 @@ class FatLine(Item):
self.constraint(vertical=(h1.pos, h2.pos))
self.constraint(above=(h1.pos, h2.pos), delta=20)
def _set_height(self, height):
h1, h2 = self._handles
h2.pos.y = height
def _get_height(self):
h1, h2 = self._handles
return h2.pos.y
height = property(_get_height, _set_height)
def draw(self, context):
cr = context.cairo
cr.set_line_width(10)
@ -206,16 +188,19 @@ class FatLine(Item):
cr.stroke()
class Circle(Item):
def __init__(self):
super(Circle, self).__init__()
self._handles.extend((Handle(), Handle()))
def _set_radius(self, r):
h1, h2 = self._handles
h2.pos.x = r
h2.pos.y = r
def _get_radius(self):
h1, h2 = self._handles
p1, p2 = h1.pos, h2.pos
@ -223,14 +208,18 @@ class Circle(Item):
radius = property(_get_radius, _set_radius)
def setup_canvas(self):
super(Circle, self).setup_canvas()
h1, h2 = self._handles
h1.movable = False
def draw(self, context):
cr = context.cairo
path_ellipse(cr, 0, 0, 2 * self.radius, 2 * self.radius)
cr.stroke()
# vim: sw=4:et:ai

View File

@ -1,12 +1,10 @@
#!/usr/bin/env python
"""
Cairo context using Steve Hanov's freehand drawing code.
# Crazyline. By Steve Hanov, 2008
# Released to the public domain.
# The idea is to draw a curve, setting two control points at random
# The idea is to draw a curve, setting two control points at random
# close to each side of the line. The longer the line, the sloppier
# it's drawn.
@ -14,15 +12,13 @@ See: http://stevehanov.ca/blog/index.php?id=33 and
http://stevehanov.ca/blog/index.php?id=93
"""
from __future__ import absolute_import
from math import sqrt
from random import Random
from gaphas.painter import Context
from painter import Context
class FreeHandCairoContext(object):
KAPPA = 0.5522847498
def __init__(self, cr, sloppiness=0.5):
@ -37,7 +33,7 @@ class FreeHandCairoContext(object):
* Drunk: 2.0
"""
self.cr = cr
self.sloppiness = sloppiness # In range 0.0 .. 2.0
self.sloppiness = sloppiness # In range 0.0 .. 2.0
def __getattr__(self, key):
return getattr(self.cr, key)
@ -48,22 +44,21 @@ class FreeHandCairoContext(object):
from_x, from_y = cr.get_current_point()
# calculate the length of the line.
length = sqrt((x - from_x) * (x - from_x) + (y - from_y) * (y - from_y))
length = sqrt( (x-from_x)*(x-from_x) + (y-from_y)*(y-from_y))
# This offset determines how sloppy the line is drawn. It depends on
# the length, but maxes out at 20.
offset = length / 10 * sloppiness
if offset > 20:
offset = 20
offset = length/10 * sloppiness
if offset > 20: offset = 20
dev_x, dev_y = cr.user_to_device(x, y)
rand = Random((from_x, from_y, dev_x, dev_y, length, offset)).random
# Overshoot the destination a little, as one might if drawing with a pen.
to_x = x + sloppiness * rand() * offset / 4
to_y = y + sloppiness * rand() * offset / 4
to_x = x + sloppiness * rand() * offset/4
to_y = y + sloppiness * rand() * offset/4
# t1 and t2 are coordinates of a line shifted under or to the right of
# t1 and t2 are coordinates of a line shifted under or to the right of
# our original.
t1_x = from_x + offset
t1_y = from_y + offset
@ -72,10 +67,10 @@ class FreeHandCairoContext(object):
# create a control point at random along our shifted line.
r = rand()
control1_x = t1_x + r * (t2_x - t1_x)
control1_y = t1_y + r * (t2_y - t1_y)
control1_x = t1_x + r * (t2_x-t1_x)
control1_y = t1_y + r * (t2_y-t1_y)
# now make t1 and t2 the coordinates of our line shifted above
# now make t1 and t2 the coordinates of our line shifted above
# and to the left of the original.
t1_x = from_x - offset
@ -85,8 +80,8 @@ class FreeHandCairoContext(object):
# create a second control point at random along the shifted line.
r = rand()
control2_x = t1_x + r * (t2_x - t1_x)
control2_y = t1_y + r * (t2_y - t1_y)
control2_x = t1_x + r * (t2_x-t1_x)
control2_y = t1_y + r * (t2_y-t1_y)
# draw the line!
cr.curve_to(control1_x, control1_y, control2_x, control2_y, to_x, to_y)
@ -104,30 +99,31 @@ class FreeHandCairoContext(object):
rand = Random((from_x, from_y, dev_x, dev_y, x1, y1, x2, y2, x3, y3)).random
r = rand()
c1_x = from_x + r * (x1 - from_x)
c1_y = from_y + r * (y1 - from_y)
c1_x = from_x + r * (x1-from_x)
c1_y = from_y + r * (y1-from_y)
r = rand()
c2_x = x3 + r * (x2 - x3)
c2_y = y3 + r * (y2 - y3)
c2_x = x3 + r * (x2-x3)
c2_y = y3 + r * (y2-y3)
cr.curve_to(c1_x, c1_y, c2_x, c2_y, x3, y3)
def rel_curve_to(self, dx1, dy1, dx2, dy2, dx3, dy3):
cr = self.cr
from_x, from_y = cr.get_current_point()
self.curve_to(from_x + dx1, from_y + dy1, from_x + dx2, from_y + dy2, from_x + dx3, from_y + dy3)
self.curve_to(from_x+dx1, from_y+dy1, from_x+dx2, from_y+dy2, from_x+dx3, from_y+dy3)
def corner_to(self, cx, cy, x, y):
cr = self.cr
from_x, from_y = cr.get_current_point()
# calculate radius of the circle.
radius1 = Math.sqrt((cx - from_x) * (cx - from_x) +
(cy - from_y) * (cy - from_y))
radius1 = Math.sqrt( (cx-from_x)*(cx-from_x) +
(cy-from_y)*(cy-from_y));
radius2 = Math.sqrt((cx - x) * (cx - x) +
(cy - y) * (cy - y))
radius2 = Math.sqrt( (cx-x)*(cx-x) +
(cy-y)*(cy-y));
dev_x, dev_y = cr.user_to_device(x, y)
rand = Random((cx, cy, dev_x, dev_y, radius1, radius2)).random
@ -156,6 +152,7 @@ class FreeHandCairoContext(object):
class FreeHandPainter(object):
def __init__(self, subpainter, sloppiness=1.0, view=None):
self.subpainter = subpainter
self.view = view
@ -166,8 +163,8 @@ class FreeHandPainter(object):
self.subpainter.set_view(view)
def paint(self, context):
subcontext = Context(cairo=FreeHandCairoContext(context.cairo, self.sloppiness), items=context.items,
area=context.area)
subcontext = Context(cairo=FreeHandCairoContext(context.cairo, self.sloppiness), items=context.items, area=context.area)
self.subpainter.paint(subcontext)
# vi:sw=4:et:ai

View File

@ -1,24 +1,3 @@
#!/usr/bin/env python
# Copyright (C) 2006-2017 Arjan Molenaar <gaphor@gmail.com>
# Artur Wroblewski <wrobell@pld-linux.org>
# Dan Yeaw <dan@yeaw.me>
#
# This file is part of Gaphas.
#
# This library is free software; you can redistribute it and/or modify it under
# the terms of the GNU Library General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for
# more details.
#
# You should have received a copy of the GNU Library General Public License
# along with this library; if not, see <http://www.gnu.org/licenses/>.
"""
Geometry functions.
@ -29,13 +8,11 @@ A point is represented as a tuple `(x, y)`.
"""
from __future__ import absolute_import
from math import sqrt
__version__ = "$Revision$"
# $HeadURL$
from math import sqrt
class Rectangle(object):
"""
@ -84,8 +61,7 @@ class Rectangle(object):
"""
"""
width = x1 - self.x
if width < 0:
width = 0
if width < 0: width = 0
self.width = width
x1 = property(lambda s: s.x + s.width, _set_x1)
@ -94,8 +70,7 @@ class Rectangle(object):
"""
"""
height = y1 - self.y
if height < 0:
height = 0
if height < 0: height = 0
self.height = height
y1 = property(lambda s: s.y + s.height, _set_y1)
@ -147,10 +122,10 @@ class Rectangle(object):
def __eq__(self, other):
return (type(self) is type(other)) \
and self.x == other.x \
and self.y == other.y \
and self.width == other.width \
and self.height == self.height
and self.x == other.x \
and self.y == other.y \
and self.width == other.width \
and self.height == self.height
def __add__(self, obj):
"""
@ -182,7 +157,7 @@ class Rectangle(object):
try:
x, y, width, height = obj
except ValueError:
raise TypeError("Can only add Rectangle or tuple (x, y, width, height), not %s." % repr(obj))
raise TypeError, "Can only add Rectangle or tuple (x, y, width, height), not %s." % repr(obj)
x1, y1 = x + width, y + height
if self:
ox1, oy1 = self.x + self.width, self.y + self.height
@ -222,7 +197,7 @@ class Rectangle(object):
try:
x, y, width, height = obj
except ValueError:
raise TypeError("Can only substract Rectangle or tuple (x, y, width, height), not %s." % repr(obj))
raise TypeError, "Can only substract Rectangle or tuple (x, y, width, height), not %s." % repr(obj)
x1, y1 = x + width, y + height
if self:
@ -272,10 +247,9 @@ class Rectangle(object):
x, y = obj
x1, y1 = obj
except ValueError:
raise TypeError(
"Should compare to Rectangle, tuple (x, y, width, height) or point (x, y), not %s." % repr(obj)
)
return x >= self.x and x1 <= self.x1 and y >= self.y and y1 <= self.y1
raise TypeError, "Should compare to Rectangle, tuple (x, y, width, height) or point (x, y), not %s." % repr(obj)
return x >= self.x and x1 <= self.x1 and \
y >= self.y and y1 <= self.y1
def distance_point_point(point1, point2=(0., 0.)):
@ -287,7 +261,7 @@ def distance_point_point(point1, point2=(0., 0.)):
"""
dx = point1[0] - point2[0]
dy = point1[1] - point2[1]
return sqrt(dx * dx + dy * dy)
return sqrt(dx*dx + dy*dy)
def distance_point_point_fast(point1, point2=(0., 0.)):
@ -337,7 +311,7 @@ def point_on_rectangle(rect, point, border=False):
Return the point on which ``point`` can be projecten on the rectangle.
``border = True`` will make sure the point is bound to the border of
the reactangle. Otherwise, if the point is in the rectangle, it's okay.
>>> point_on_rectangle(Rectangle(0, 0, 10, 10), (11, -1))
(10, 0)
>>> point_on_rectangle((0, 0, 10, 10), (5, 12))
@ -384,7 +358,7 @@ def point_on_rectangle(rect, point, border=False):
if x_inside and y_inside:
# Find point on side closest to the point
if min(abs(rx - px), abs(rx + rw - px)) > \
min(abs(ry - py), abs(ry + rh - py)):
min(abs(ry - py), abs(ry + rh - py)):
if py < ry + rh / 2.:
py = ry
else:
@ -401,7 +375,7 @@ def point_on_rectangle(rect, point, border=False):
def distance_line_point(line_start, line_end, point):
"""
Calculate the distance of a ``point`` from a line. The line is marked
by begin and end point ``line_start`` and ``line_end``.
by begin and end point ``line_start`` and ``line_end``.
A tuple is returned containing the distance and point on the line.
@ -442,9 +416,8 @@ def distance_line_point(line_start, line_end, point):
# Projection is on the line. multiply the line_end with the projlen
# factor to obtain the point on the line.
proj = line_end[0] * projlen, line_end[1] * projlen
return distance_point_point((proj[0] - point[0], proj[1] - point[1])), (
line_start[0] + proj[0], line_start[1] + proj[1]
)
return distance_point_point((proj[0] - point[0], proj[1] - point[1])),\
(line_start[0] + proj[0], line_start[1] + proj[1])
def intersect_line_line(line1_start, line1_end, line2_start, line2_end):
@ -514,9 +487,9 @@ def intersect_line_line(line1_start, line1_end, line2_start, line2_end):
x3, y3 = line2_start
x4, y4 = line2_end
# long a1, a2, b1, b2, c1, c2; /* Coefficients of line eqns. */
# long r1, r2, r3, r4; /* 'Sign' values */
# long denom, offset, num; /* Intermediate values */
#long a1, a2, b1, b2, c1, c2; /* Coefficients of line eqns. */
#long r1, r2, r3, r4; /* 'Sign' values */
#long denom, offset, num; /* Intermediate values */
# Compute a1, b1, c1, where line joining points 1 and 2
# is "a1 x + b1 y + c1 = 0".
@ -534,7 +507,7 @@ def intersect_line_line(line1_start, line1_end, line2_start, line2_end):
# same side of line 1, the line segments do not intersect.
if r3 and r4 and (r3 * r4) >= 0:
return None # ( DONT_INTERSECT )
return None # ( DONT_INTERSECT )
# Compute a2, b2, c2
@ -551,14 +524,14 @@ def intersect_line_line(line1_start, line1_end, line2_start, line2_end):
# on same side of second line segment, the line segments do
# not intersect.
if r1 and r2 and (r1 * r2) >= 0: # SAME_SIGNS( r1, r2 ))
return None # ( DONT_INTERSECT )
if r1 and r2 and (r1 * r2) >= 0: #SAME_SIGNS( r1, r2 ))
return None # ( DONT_INTERSECT )
# Line segments intersect: compute intersection point.
# Line segments intersect: compute intersection point.
denom = a1 * b2 - a2 * b1
if not denom:
return None # ( COLLINEAR )
return None # ( COLLINEAR )
offset = abs(denom) / 2
# The denom/2 is to get rounding instead of truncating. It
@ -566,10 +539,10 @@ def intersect_line_line(line1_start, line1_end, line2_start, line2_end):
# sign of the numerator.
num = b1 * c2 - b2 * c1
x = ((num < 0) and (num - offset) or (num + offset)) / denom
x = ( (num < 0) and (num - offset) or (num + offset) ) / denom
num = a2 * c1 - a1 * c2
y = ((num < 0) and (num - offset) or (num + offset)) / denom
y = ( (num < 0) and (num - offset) or (num + offset) ) / denom
return x, y
@ -609,10 +582,11 @@ def rectangle_clip(recta, rectb):
bx, by, bw, bh = rectb
x = max(ax, bx)
y = max(ay, by)
w = min(ax + aw, bx + bw) - x
h = min(ay + ah, by + bh) - y
w = min(ax +aw, bx + bw) - x
h = min(ay +ah, by + bh) - y
if w < 0 or h < 0:
return None
return (x, y, w, h)
# vim:sw=4:et:ai

View File

@ -1,37 +1,12 @@
#!/usr/bin/env python
# Copyright (C) 2010-2017 Arjan Molenaar <gaphor@gmail.com>
# Dan Yeaw <dan@yeaw.me>
#
# This file is part of Gaphas.
#
# This library is free software; you can redistribute it and/or modify it under
# the terms of the GNU Library General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for
# more details.
#
# You should have received a copy of the GNU Library General Public License
# along with this library; if not, see <http://www.gnu.org/licenses/>.
"""
Module implements guides when moving items and handles around.
"""
from __future__ import absolute_import
from functools import reduce
from simplegeneric import generic
from six.moves import map
from gaphas.aspect import InMotion, HandleInMotion, PaintFocused
from gaphas.aspect import ItemInMotion, ItemHandleInMotion, ItemPaintFocused
from gaphas.item import Item, Element, Line
from gaphas.connector import Handle
from gaphas.item import Item, Element, Line, SE
class ItemGuide(object):
@ -60,13 +35,14 @@ Guide = generic(ItemGuide)
@Guide.when_type(Element)
class ElementGuide(ItemGuide):
def horizontal(self):
y = self.item.height
return 0, y / 2, y
return (0, y/2, y)
def vertical(self):
x = self.item.width
return 0, x / 2, x
return (0, x/2, x)
@Guide.when_type(Line)
@ -101,6 +77,7 @@ class LineGuide(ItemGuide):
class Guides(object):
def __init__(self, v, h):
self.v = v
self.h = h
@ -126,9 +103,9 @@ class GuideMixin(object):
margin = self.MARGIN
items = []
for x in item_vedges:
items.append(view.get_items_in_rectangle((x - margin, 0, margin * 2, height)))
items.append(view.get_items_in_rectangle((x - margin, 0, margin*2, height)))
try:
guides = list(map(Guide, reduce(set.union, list(map(set, items))) - excluded_items))
guides = map(Guide, reduce(set.union, map(set, items)) - excluded_items)
except TypeError:
guides = []
@ -139,6 +116,7 @@ class GuideMixin(object):
dx, edges_x = self.find_closest(item_vedges, vedges)
return dx, edges_x
def find_horizontal_guides(self, item_hedges, pdy, width, excluded_items):
view = self.view
item = self.item
@ -146,9 +124,9 @@ class GuideMixin(object):
margin = self.MARGIN
items = []
for y in item_hedges:
items.append(view.get_items_in_rectangle((0, y - margin, width, margin * 2)))
items.append(view.get_items_in_rectangle((0, y - margin, width, margin*2)))
try:
guides = list(map(Guide, reduce(set.union, list(map(set, items))) - excluded_items))
guides = map(Guide, reduce(set.union, map(set, items)) - excluded_items)
except TypeError:
guides = []
@ -161,6 +139,7 @@ class GuideMixin(object):
dy, edges_y = self.find_closest(item_hedges, hedges)
return dy, edges_y
def get_excluded_items(self):
"""
Get a set of items excluded from guide calculation.
@ -173,13 +152,15 @@ class GuideMixin(object):
excluded_items.update(view.selected_items)
return excluded_items
def get_view_dimensions(self):
try:
allocation = self.view.get_allocation()
except AttributeError as e:
allocation = self.view.allocation
except AttributeError, e:
return 0, 0
return allocation.width, allocation.height
def queue_draw_guides(self):
view = self.view
try:
@ -190,9 +171,10 @@ class GuideMixin(object):
w, h = self.get_view_dimensions()
for x in guides.vertical():
view.queue_draw_area(x - 1, 0, x + 2, h)
view.queue_draw_area(x-1, 0, x+2, h)
for y in guides.horizontal():
view.queue_draw_area(0, y - 1, w, y + 2)
view.queue_draw_area(0, y-1, w, y+2)
def find_closest(self, item_edges, edges):
delta = 0
@ -252,6 +234,7 @@ class GuidedItemInMotion(GuideMixin, ItemInMotion):
return sink
def stop_move(self):
self.queue_draw_guides()
try:
@ -263,6 +246,7 @@ class GuidedItemInMotion(GuideMixin, ItemInMotion):
@HandleInMotion.when_type(Item)
class GuidedItemHandleInMotion(GuideMixin, ItemHandleInMotion):
def move(self, pos):
sink = super(GuidedItemHandleInMotion, self).move(pos)
@ -275,20 +259,29 @@ class GuidedItemHandleInMotion(GuideMixin, ItemHandleInMotion):
v2i = view.get_matrix_v2i(item)
excluded_items = self.get_excluded_items()
w, h = self.get_view_dimensions()
dx, edges_x = self.find_vertical_guides((x,), 0, h, excluded_items)
dy, edges_y = self.find_horizontal_guides((y,), 0, w, excluded_items)
newpos = x + dx, y + dy
x, y = v2i.transform_point(*newpos)
self.handle.pos = (x, y)
#super(GuidedItemHandleInMotion, self).move(newpos)
self.queue_draw_guides()
view.guides = Guides(edges_x, edges_y)
self.queue_draw_guides()
item.request_update()
def stop_move(self):
self.queue_draw_guides()
try:
@ -300,6 +293,7 @@ class GuidedItemHandleInMotion(GuideMixin, ItemHandleInMotion):
@PaintFocused.when_type(Item)
class GuidePainter(ItemPaintFocused):
def paint(self, context):
try:
guides = self.view.guides
@ -308,7 +302,7 @@ class GuidePainter(ItemPaintFocused):
cr = context.cairo
view = self.view
allocation = view.get_allocation()
allocation = view.allocation
w, h = allocation.width, allocation.height
cr.save()

View File

@ -1,51 +1,24 @@
#!/usr/bin/env python
# Copyright (C) 2006-2017 Arjan Molenaar <gaphor@gmail.com>
# Artur Wroblewski <wrobell@pld-linux.org>
# Dan Yeaw <dan@yeaw.me>
#
# This file is part of Gaphas.
#
# This library is free software; you can redistribute it and/or modify it under
# the terms of the GNU Library General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for
# more details.
#
# You should have received a copy of the GNU Library General Public License
# along with this library; if not, see <http://www.gnu.org/licenses/>.
"""
Basic items.
"""
from __future__ import absolute_import
__version__ = "$Revision$"
# $HeadURL$
from functools import reduce
from math import atan2
from weakref import WeakKeyDictionary
from six.moves import map
from six.moves import range
from six.moves import zip
try:
# python 3.0 (better be prepared)
from weakref import WeakSet
except ImportError:
from gaphas.weakset import WeakSet
from gaphas.matrix import Matrix
from gaphas.geometry import distance_line_point, distance_rectangle_point
from gaphas.connector import Handle, LinePort
from gaphas.solver import solvable, WEAK, VERY_STRONG, REQUIRED
from gaphas.constraint import EqualsConstraint, LessThanConstraint, LineConstraint, LineAlignConstraint
from gaphas.state import observed, reversible_method, reversible_pair, reversible_property
from weakset import WeakSet
from matrix import Matrix
from geometry import distance_line_point, distance_rectangle_point
from connector import Handle, LinePort
from solver import solvable, WEAK, NORMAL, STRONG, VERY_STRONG, REQUIRED
from constraint import EqualsConstraint, LessThanConstraint, LineConstraint, LineAlignConstraint
from state import observed, reversible_method, reversible_pair, reversible_property
class Item(object):
"""
@ -101,10 +74,10 @@ class Item(object):
self.setup_canvas()
canvas = reversible_property(lambda s: s._canvas, _set_canvas,
doc="Canvas owning this item")
doc="Canvas owning this item")
constraints = property(lambda s: s._constraints,
doc="Item constraints")
doc="Item constraints")
def setup_canvas(self):
"""
@ -115,6 +88,7 @@ class Item(object):
for c in self._constraints:
add(c)
def teardown_canvas(self):
"""
Called when the canvas is unset for the item.
@ -126,6 +100,7 @@ class Item(object):
for c in self._constraints:
remove(c)
@observed
def _set_matrix(self, matrix):
"""
@ -137,10 +112,12 @@ class Item(object):
matrix = reversible_property(lambda s: s._matrix, _set_matrix)
def request_update(self, update=True, matrix=True):
if self._canvas:
self._canvas.request_update(self, update=update, matrix=matrix)
def pre_update(self, context):
"""
Perform any changes before item update here, for example:
@ -154,6 +131,7 @@ class Item(object):
"""
pass
def post_update(self, context):
"""
Method called after item update.
@ -168,11 +146,12 @@ class Item(object):
"""
pass
def normalize(self):
"""
Update handle positions of the item, so the first handle is always
located at (0, 0).
Note that, since this method basically does some housekeeping during
the update phase, there's no need to keep track of the changes.
@ -187,7 +166,7 @@ class Item(object):
updated = False
handles = self._handles
if handles:
x, y = list(map(float, handles[0].pos))
x, y = map(float, handles[0].pos)
if x:
self.matrix.translate(x, 0)
updated = True
@ -200,6 +179,7 @@ class Item(object):
h.pos.y -= y
return updated
def draw(self, context):
"""
Render the item to a canvas view.
@ -212,18 +192,21 @@ class Item(object):
"""
pass
def handles(self):
"""
Return a list of handles owned by the item.
"""
return self._handles
def ports(self):
"""
Return list of ports.
"""
return self._ports
def point(self, pos):
"""
Get the distance from a point (``x``, ``y``) to the item.
@ -231,14 +214,15 @@ class Item(object):
"""
pass
def constraint(self,
horizontal=None,
vertical=None,
left_of=None,
above=None,
line=None,
delta=0.0,
align=None):
horizontal=None,
vertical=None,
left_of=None,
above=None,
line=None,
delta=0.0,
align=None):
"""
Utility (factory) method to create item's internal constraint between
two positions or between a position and a line.
@ -264,7 +248,7 @@ class Item(object):
line=(p, l)
Keep position ``p`` on line ``l``.
"""
cc = None # created constraint
cc = None # created constraint
if horizontal:
p1, p2 = horizontal
cc = EqualsConstraint(p1[1], p2[1], delta)
@ -289,6 +273,7 @@ class Item(object):
self._constraints.append(cc)
return cc
def __getstate__(self):
"""
Persist all, but calculated values (``_matrix_?2?``).
@ -302,6 +287,7 @@ class Item(object):
d['_canvas_projections'] = tuple(self._canvas_projections)
return d
def __setstate__(self, state):
"""
Set state. No ``__init__()`` is called.
@ -314,11 +300,10 @@ class Item(object):
self._canvas_projections = WeakSet(state['_canvas_projections'])
[NW,
NE,
SE,
SW] = range(4)
[ NW,
NE,
SE,
SW ] = xrange(4)
class Element(Item):
"""
@ -334,7 +319,7 @@ class Element(Item):
def __init__(self, width=10, height=10):
super(Element, self).__init__()
self._handles = [h(strength=VERY_STRONG) for h in [Handle] * 4]
self._handles = [ h(strength=VERY_STRONG) for h in [Handle]*4 ]
handles = self._handles
h_nw = handles[NW]
@ -367,7 +352,8 @@ class Element(Item):
self.height = height
# TODO: constraints that calculate width and height based on handle pos
# self.constraints.append(EqualsConstraint(p1[1], p2[1], delta))
#self.constraints.append(EqualsConstraint(p1[1], p2[1], delta))
def setup_canvas(self):
super(Element, self).setup_canvas()
@ -390,6 +376,7 @@ class Element(Item):
h = self._handles
h[SE].pos.x = h[NW].pos.x + width
def _get_width(self):
"""
Width of the box, calculated as the distance from the left and
@ -426,33 +413,6 @@ class Element(Item):
height = property(_get_height, _set_height)
# @observed
# def _set_min_width(self, min_width):
# """
# Set minimal width.
# """
# if min_width < 0:
# raise ValueError, 'Minimal width cannot be less than 0'
#
# self._c_min_w.delta = min_width
# if self.canvas:
# self.canvas.solver.request_resolve_constraint(self._c_min_w)
# min_width = reversible_property(lambda s: s._c_min_w.delta, _set_min_width)
# @observed
# def _set_min_height(self, min_height):
# """
# Set minimal height.
# """
# if min_height < 0:
# raise ValueError, 'Minimal height cannot be less than 0'
#
# self._c_min_h.delta = min_height
# if self.canvas:
# self.canvas.solver.request_resolve_constraint(self._c_min_h)
#
# min_height = reversible_property(lambda s: s._c_min_h.delta, _set_min_height)
def normalize(self):
"""
@ -461,7 +421,7 @@ class Element(Item):
updated = False
handles = self._handles
handles = (handles[NW], handles[SE])
x, y = list(map(float, handles[0].pos))
x, y = map(float, handles[0].pos)
if x:
self.matrix.translate(x, 0)
updated = True
@ -484,7 +444,7 @@ class Element(Item):
"""
h = self._handles
pnw, pse = h[NW].pos, h[SE].pos
return distance_rectangle_point(list(map(float, (pnw.x, pnw.y, pse.x, pse.y))), pos)
return distance_rectangle_point(map(float, (pnw.x, pnw.y, pse.x, pse.y)), pos)
class Line(Item):
@ -537,7 +497,7 @@ class Line(Item):
observed, so the undo system will update the contents properly
"""
if not self.canvas:
self._orthogonal_constraints = orthogonal and [None] or []
self._orthogonal_constraints = orthogonal and [ None ] or []
return
for c in self._orthogonal_constraints:
@ -548,16 +508,16 @@ class Line(Item):
return
h = self._handles
# if len(h) < 3:
#if len(h) < 3:
# self.split_segment(0)
eq = EqualsConstraint # lambda a, b: a - b
eq = EqualsConstraint #lambda a, b: a - b
add = self.canvas.solver.add_constraint
cons = []
rest = self._horizontal and 1 or 0
for pos, (h0, h1) in enumerate(zip(h, h[1:])):
p0 = h0.pos
p1 = h1.pos
if pos % 2 == rest: # odd
if pos % 2 == rest: # odd
cons.append(add(eq(a=p0.x, b=p1.x)))
else:
cons.append(add(eq(a=p0.y, b=p1.y)))
@ -583,7 +543,7 @@ class Line(Item):
False
"""
if orthogonal and len(self.handles()) < 3:
raise ValueError("Can't set orthogonal line with less than 3 handles")
raise ValueError, "Can't set orthogonal line with less than 3 handles"
self._update_orthogonal_constraints(orthogonal)
orthogonal = reversible_property(lambda s: bool(s._orthogonal_constraints), _set_orthogonal)
@ -593,7 +553,7 @@ class Line(Item):
self._horizontal = horizontal
reversible_method(_inner_set_horizontal, _inner_set_horizontal,
{'horizontal': lambda horizontal: not horizontal})
{'horizontal': lambda horizontal: not horizontal })
def _set_horizontal(self, horizontal):
"""
@ -632,8 +592,8 @@ class Line(Item):
def _reversible_remove_handle(self, handle):
self._handles.remove(handle)
reversible_pair(_reversible_insert_handle, _reversible_remove_handle,
bind1={'index': lambda self, handle: self._handles.index(handle)})
reversible_pair(_reversible_insert_handle, _reversible_remove_handle, \
bind1={'index': lambda self, handle: self._handles.index(handle)})
@observed
def _reversible_insert_port(self, index, port):
@ -643,15 +603,18 @@ class Line(Item):
def _reversible_remove_port(self, port):
self._ports.remove(port)
reversible_pair(_reversible_insert_port, _reversible_remove_port,
bind1={'index': lambda self, port: self._ports.index(port)})
reversible_pair(_reversible_insert_port, _reversible_remove_port, \
bind1={'index': lambda self, port: self._ports.index(port)})
def _create_handle(self, pos, strength=WEAK):
return Handle(pos, strength=strength)
def _create_port(self, p1, p2):
return LinePort(p1, p2)
def _update_ports(self):
"""
Update line ports. This destroys all previously created ports and
@ -663,6 +626,7 @@ class Line(Item):
for h1, h2 in zip(handles[:-1], handles[1:]):
self._ports.append(self._create_port(h1.pos, h2.pos))
def opposite(self, handle):
"""
Given the handle of one end of the line, return the other end.
@ -689,7 +653,7 @@ class Line(Item):
def closest_segment(self, pos):
"""
Obtain a tuple (distance, point_on_line, segment).
Distance is the distance from point to the closest line segment
Distance is the distance from point to the closest line segment
Point_on_line is the reflection of the point on the line.
Segment is the line segment closest to (x, y)
@ -698,12 +662,12 @@ class Line(Item):
(0.7071067811865476, (4.5, 4.5), 0)
"""
h = self._handles
hpos = list(map(getattr, h, ['pos'] * len(h)))
hpos = map(getattr, h, ['pos'] * len(h))
# create a list of (distance, point_on_line) tuples:
distances = list(map(distance_line_point, hpos[:-1], hpos[1:], [pos] * (len(hpos) - 1)))
distances, pols = list(zip(*distances))
return reduce(min, list(zip(distances, pols, list(range(len(distances))))))
distances = map(distance_line_point, hpos[:-1], hpos[1:], [pos] * (len(hpos) - 1))
distances, pols = zip(*distances)
return reduce(min, zip(distances, pols, range(len(distances))))
def point(self, pos):
"""
@ -732,12 +696,12 @@ class Line(Item):
"""
context.cairo.line_to(0, 0)
def draw(self, context):
"""
Draw the line itself.
See Item.draw(context).
"""
def draw_line_end(pos, angle, draw):
cr = context.cairo
cr.save()
@ -756,18 +720,19 @@ class Line(Item):
draw_line_end(self._handles[-1].pos, self._tail_angle, self.draw_tail)
cr.stroke()
# debug code to draw line ports
# cr.set_line_width(1)
# cr.set_source_rgb(1.0, 0.0, 0.0)
# for p in self.ports():
# cr.move_to(*p.start)
# cr.line_to(*p.end)
# cr.stroke()
### debug code to draw line ports
### cr.set_line_width(1)
### cr.set_source_rgb(1.0, 0.0, 0.0)
### for p in self.ports():
### cr.move_to(*p.start)
### cr.line_to(*p.end)
### cr.stroke()
__test__ = {
'Line._set_orthogonal': Line._set_orthogonal,
}
}
# vim: sw=4:et:ai

View File

@ -1,23 +1,3 @@
#!/usr/bin/env python
# Copyright (C) 2007-2017 Arjan Molenaar <gaphor@gmail.com>
# Dan Yeaw <dan@yeaw.me>
#
# This file is part of Gaphas.
#
# This library is free software; you can redistribute it and/or modify it under
# the terms of the GNU Library General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for
# more details.
#
# You should have received a copy of the GNU Library General Public License
# along with this library; if not, see <http://www.gnu.org/licenses/>.
"""
Some Gaphor specific updates to the canvas. This is done by setting the
correct properties on gaphas' modules.
@ -28,15 +8,12 @@ Small utility class wrapping cairo.Matrix. The `Matrix` class
adds state preservation capabilities.
"""
from __future__ import absolute_import
import cairo
from gaphas.state import observed, reversible_method
__version__ = "$Revision$"
# $HeadURL$
import cairo
from state import observed, reversible_method
class Matrix(object):
"""
@ -77,11 +54,11 @@ class Matrix(object):
reversible_method(invert, invert)
reversible_method(rotate, rotate,
{'radians': lambda radians: -radians})
{ 'radians': lambda radians: -radians })
reversible_method(scale, scale,
{'sx': lambda sx: 1 / sx, 'sy': lambda sy: 1 / sy})
{ 'sx': lambda sx: 1/sx, 'sy': lambda sy: 1/sy })
reversible_method(translate, translate,
{'tx': lambda tx: -tx, 'ty': lambda ty: -ty})
{ 'tx': lambda tx: -tx, 'ty': lambda ty: -ty })
def transform_distance(self, dx, dy):
self._matrix.transform_distance(dx, dy)

View File

@ -1,25 +1,3 @@
#!/usr/bin/env python
# Copyright (C) 2006-2017 Antony N. Pavlov <antony@niisi.msk.ru>
# Arjan Molenaar <gaphor@gmail.com>
# Artur Wroblewski <wrobell@pld-linux.org>
# Dan Yeaw <dan@yeaw.me>
#
# This file is part of Gaphas.
#
# This library is free software; you can redistribute it and/or modify it under
# the terms of the GNU Library General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for
# more details.
#
# You should have received a copy of the GNU Library General Public License
# along with this library; if not, see <http://www.gnu.org/licenses/>.
"""
The painter module provides different painters for parts of the canvas.
@ -29,24 +7,23 @@ Each painter takes care of a layer in the canvas (such as grid, items
and handles).
"""
from __future__ import absolute_import
from cairo import ANTIALIAS_NONE, LINE_JOIN_ROUND
from gaphas.aspect import PaintFocused
from gaphas.canvas import Context
from gaphas.geometry import Rectangle
__version__ = "$Revision$"
# $HeadURL$
from cairo import Matrix, ANTIALIAS_NONE, LINE_JOIN_ROUND
from gaphas.canvas import Context
from gaphas.geometry import Rectangle
from gaphas.item import Line
from gaphas.aspect import PaintFocused
DEBUG_DRAW_BOUNDING_BOX = False
# The tolerance for Cairo. Bigger values increase speed and reduce accuracy
# (default: 0.1)
TOLERANCE = 0.8
class Painter(object):
"""
Painter interface.
@ -116,13 +93,16 @@ class DrawContext(Context):
class ItemPainter(Painter):
draw_all = False
def _draw_item(self, item, cairo, area=None):
view = self.view
cairo.save()
try:
cairo.set_matrix(view.matrix)
cairo.transform(view.canvas.get_matrix_i2c(item))
item.draw(DrawContext(painter=self,
cairo=cairo,
_area=area,
@ -150,7 +130,7 @@ class ItemPainter(Painter):
try:
b = view.get_item_bounding_box(item)
except KeyError:
pass # No bounding box right now..
pass # No bounding box right now..
else:
cairo.save()
cairo.identity_matrix()
@ -164,11 +144,7 @@ class ItemPainter(Painter):
cairo = context.cairo
cairo.set_tolerance(TOLERANCE)
cairo.set_line_join(LINE_JOIN_ROUND)
cairo.save()
cairo.transform(self.view.matrix)
self._draw_items(context.items, cairo, context.area)
cairo.restore()
class CairoBoundingBoxContext(object):
@ -180,7 +156,7 @@ class CairoBoundingBoxContext(object):
def __init__(self, cairo):
self._cairo = cairo
self._bounds = None # a Rectangle object
self._bounds = None # a Rectangle object
def __getattr__(self, key):
return getattr(self._cairo, key)
@ -211,9 +187,9 @@ class CairoBoundingBoxContext(object):
cr.restore()
if b and line_width:
# Do this after the restore(), so we can get the proper width.
lw = cr.get_line_width() / 2
lw = cr.get_line_width()/2
d = cr.user_to_device_distance(lw, lw)
b.expand(d[0] + d[1])
b.expand(d[0]+d[1])
self._update_bounds(b)
return b
@ -259,8 +235,8 @@ class CairoBoundingBoxContext(object):
if not b:
x, y = cr.get_current_point()
e = cr.text_extents(utf8)
x0, y0 = cr.user_to_device(x + e[0], y + e[1])
x1, y1 = cr.user_to_device(x + e[0] + e[2], y + e[1] + e[3])
x0, y0 = cr.user_to_device(x+e[0], y+e[1])
x1, y1 = cr.user_to_device(x+e[0]+e[2], y+e[1]+e[3])
b = Rectangle(x0, y0, x1=x1, y1=y1)
self._update_bounds(b)
cr.show_text(utf8)
@ -289,6 +265,7 @@ class BoundingBoxPainter(ItemPainter):
bounds.expand(1)
view.set_item_bounding_box(item, bounds)
def _draw_items(self, items, cairo, area=None):
"""
Draw the items.
@ -308,7 +285,7 @@ class HandlePainter(Painter):
def _draw_handles(self, item, cairo, opacity=None, inner=False):
"""
Draw handles for an item.
The handles are drawn in non-antialiased mode for clearity.
The handles are drawn in non-antialiased mode for clarity.
"""
view = self.view
cairo.save()
@ -317,7 +294,6 @@ class HandlePainter(Painter):
opacity = (item is view.focused_item) and .7 or .4
cairo.set_line_width(1)
cairo.set_antialias(ANTIALIAS_NONE)
get_connection = view.canvas.get_connection
for h in item.handles():
@ -334,8 +310,8 @@ class HandlePainter(Painter):
else:
r, g, b = 0, 0, 1
# cairo.identity_matrix()
cairo.save()
cairo.identity_matrix()
cairo.set_antialias(ANTIALIAS_NONE)
cairo.translate(*i2v.transform_point(*h.pos))
cairo.rectangle(-4, -4, 8, 8)
if inner:
@ -347,9 +323,8 @@ class HandlePainter(Painter):
cairo.line_to(2, 3)
cairo.move_to(2, -2)
cairo.line_to(-2, 3)
cairo.set_source_rgba(r / 4., g / 4., b / 4., opacity * 1.3)
cairo.set_source_rgba(r/4., g/4., b/4., opacity*1.3)
cairo.stroke()
cairo.restore()
cairo.restore()
def paint(self, context):
@ -379,10 +354,10 @@ class ToolPainter(Painter):
cairo = context.cairo
if view.tool:
cairo.save()
cairo.identity_matrix()
view.tool.draw(context)
cairo.restore()
class FocusedItemPainter(Painter):
"""
This painter allows for drawing on top off all other layers for the
@ -406,4 +381,5 @@ def DefaultPainter(view=None):
append(FocusedItemPainter()). \
append(ToolPainter())
# vim: sw=4:et:ai

View File

@ -1,58 +1,35 @@
#!/usr/bin/env python
# Copyright (C) 2008-2017 Arjan Molenaar <gaphor@gmail.com>
# Dan Yeaw <dan@yeaw.me>
#
# This file is part of Gaphas.
#
# This library is free software; you can redistribute it and/or modify it under
# the terms of the GNU Library General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for
# more details.
#
# You should have received a copy of the GNU Library General Public License
# along with this library; if not, see <http://www.gnu.org/licenses/>.
"""
Some extra picklers needed to gracefully dump and load a canvas.
"""
from __future__ import absolute_import
import types
import cairo
import copyreg
def construct_instancemethod(funcname, self, clazz):
func = getattr(clazz, funcname)
return types.MethodType(func, self, clazz)
def reduce_instancemethod(im):
return construct_instancemethod, (im.__func__.__name__, im.__self__, im.__self__.__class__)
copyreg.pickle(types.MethodType, reduce_instancemethod, construct_instancemethod)
# Allow cairo.Matrix to be pickled:
def construct_cairo_matrix(*args):
return cairo.Matrix(*args)
def reduce_cairo_matrix(m):
return construct_cairo_matrix, tuple(m)
copyreg.pickle(cairo.Matrix, reduce_cairo_matrix, construct_cairo_matrix)
# vim:sw=4:et:ai
"""
Some extra picklers needed to gracefully dump and load a canvas.
"""
import copy_reg
# Allow instancemethod to be pickled:
import new
def construct_instancemethod(funcname, self, clazz):
func = getattr(clazz, funcname)
return new.instancemethod(func, self, clazz)
def reduce_instancemethod(im):
return construct_instancemethod, (im.im_func.__name__, im.im_self, im.im_class)
copy_reg.pickle(new.instancemethod, reduce_instancemethod, construct_instancemethod)
# Allow cairo.Matrix to be pickled:
import cairo
def construct_cairo_matrix(*args):
return cairo.Matrix(*args)
def reduce_cairo_matrix(m):
return construct_cairo_matrix, tuple(m)
copy_reg.pickle(cairo.Matrix, reduce_cairo_matrix, construct_cairo_matrix)
# vim:sw=4:et:ai

View File

@ -1,24 +1,3 @@
#!/usr/bin/env python
# Copyright (C) 2007-2017 Arjan Molenaar <gaphor@gmail.com>
# Artur Wroblewski <wrobell@pld-linux.org>
# Dan Yeaw <dan@yeaw.me>
#
# This file is part of Gaphas.
#
# This library is free software; you can redistribute it and/or modify it under
# the terms of the GNU Library General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for
# more details.
#
# You should have received a copy of the GNU Library General Public License
# along with this library; if not, see <http://www.gnu.org/licenses/>.
"""
Quadtree
========
@ -39,20 +18,12 @@ common features:
(From Wikipedia, the free encyclopedia)
"""
from __future__ import absolute_import
from __future__ import print_function
import operator
import six
from six.moves import map
from six.moves import zip
from gaphas.geometry import rectangle_contains, rectangle_intersects, rectangle_clip
__version__ = "$Revision$"
# $HeadURL$
import operator
from geometry import rectangle_contains, rectangle_intersects, rectangle_clip
class Quadtree(object):
"""
@ -114,10 +85,10 @@ class Quadtree(object):
def __init__(self, bounds=(0, 0, 0, 0), capacity=10):
"""
Create a new Quadtree instance.
Bounds is the boundries of the quadtree. this is fixed and do not
change depending on the contents.
Capacity defines the number of elements in one tree bucket (default: 10)
"""
self._capacity = capacity
@ -126,8 +97,10 @@ class Quadtree(object):
# Easy lookup item->(bounds, data, clipped bounds) mapping
self._ids = dict()
bounds = property(lambda s: s._bucket.bounds)
def resize(self, bounds):
"""
Resize the tree.
@ -136,6 +109,7 @@ class Quadtree(object):
self._bucket = QuadtreeBucket(bounds, self._capacity)
self.rebuild()
def get_soft_bounds(self):
"""
Calculate the size of all items in the tree. This size may be beyond
@ -156,18 +130,19 @@ class Quadtree(object):
>>> qtree.bounds
(0, 0, 0, 0)
"""
x_y_w_h = list(zip(*list(map(operator.getitem, six.itervalues(self._ids), [0] * len(self._ids)))))
x_y_w_h = zip(*map(operator.getitem, self._ids.itervalues(), [0] * len(self._ids)))
if not x_y_w_h:
return 0, 0, 0, 0
x0 = min(x_y_w_h[0])
y0 = min(x_y_w_h[1])
add = operator.add
x1 = max(list(map(add, x_y_w_h[0], x_y_w_h[2])))
y1 = max(list(map(add, x_y_w_h[1], x_y_w_h[3])))
return x0, y0, x1 - x0, y1 - y0
x1 = max(map(add, x_y_w_h[0], x_y_w_h[2]))
y1 = max(map(add, x_y_w_h[1], x_y_w_h[3]))
return (x0, y0, x1 - x0, y1 - y0)
soft_bounds = property(get_soft_bounds)
def add(self, item, bounds, data=None):
"""
Add an item to the tree.
@ -198,6 +173,7 @@ class Quadtree(object):
self._bucket.find_bucket(clipped_bounds).add(item, clipped_bounds)
self._ids[item] = (bounds, data, clipped_bounds)
def remove(self, item):
"""
Remove an item from the tree.
@ -207,6 +183,7 @@ class Quadtree(object):
if clipped_bounds:
self._bucket.find_bucket(clipped_bounds).remove(item)
def clear(self):
"""
Remove all items from the tree.
@ -214,6 +191,7 @@ class Quadtree(object):
self._bucket.clear()
self._ids.clear()
def rebuild(self):
"""
Rebuild the tree structure.
@ -221,24 +199,27 @@ class Quadtree(object):
# Clean bucket and items:
self._bucket.clear()
for item, (bounds, data, _) in six.iteritems(dict(self._ids)):
for item, (bounds, data, _) in dict(self._ids).iteritems():
clipped_bounds = rectangle_clip(bounds, self._bucket.bounds)
if clipped_bounds:
self._bucket.find_bucket(clipped_bounds).add(item, clipped_bounds)
self._ids[item] = (bounds, data, clipped_bounds)
def get_bounds(self, item):
"""
Return the bounding box for the given item.
"""
return self._ids[item][0]
def get_data(self, item):
"""
Return the data for the given item, None if no data was provided.
"""
return self._ids[item][1]
def get_clipped_bounds(self, item):
"""
Return the bounding box for the given item. The bounding box is clipped
@ -247,6 +228,7 @@ class Quadtree(object):
"""
return self._ids[item][2]
def find_inside(self, rect):
"""
Find all items in the given rectangle (x, y, with, height).
@ -254,6 +236,7 @@ class Quadtree(object):
"""
return set(self._bucket.find(rect, method=rectangle_contains))
def find_intersect(self, rect):
"""
Find all items that intersect with the given rectangle
@ -262,18 +245,21 @@ class Quadtree(object):
"""
return set(self._bucket.find(rect, method=rectangle_intersects))
def __len__(self):
"""
Return number of items in tree.
"""
return len(self._ids)
def __contains__(self, item):
"""
Check if an item is in tree.
"""
return item in self._ids
def dump(self):
"""
Print structure to stdout.
@ -296,6 +282,7 @@ class QuadtreeBucket(object):
self.items = {}
self._buckets = []
def add(self, item, bounds):
"""
Add an item to the quadtree.
@ -313,7 +300,7 @@ class QuadtreeBucket(object):
QuadtreeBucket((x, cy, rw, rh), self.capacity),
QuadtreeBucket((cx, cy, rw, rh), self.capacity)]
# Add items to subnodes
items = list(self.items.items())
items = self.items.items()
self.items.clear()
for i, b in items:
self.find_bucket(b).add(i, b)
@ -321,6 +308,7 @@ class QuadtreeBucket(object):
else:
self.items[item] = bounds
def remove(self, item):
"""
Remove an item from the quadtree bucket.
@ -328,6 +316,7 @@ class QuadtreeBucket(object):
"""
del self.items[item]
def update(self, item, new_bounds):
"""
Update the position of an item within the current bucket.
@ -338,6 +327,7 @@ class QuadtreeBucket(object):
self.remove(item)
self.find_bucket(new_bounds).add(item, new_bounds)
def find_bucket(self, bounds):
"""
Find the bucket that holds a bounding box.
@ -362,6 +352,7 @@ class QuadtreeBucket(object):
return self._buckets[index].find_bucket(bounds)
return self
def find(self, rect, method):
"""
Find all items in the given rectangle (x, y, with, height).
@ -370,13 +361,14 @@ class QuadtreeBucket(object):
Returns an iterator.
"""
if rectangle_intersects(rect, self.bounds):
for item, bounds in six.iteritems(self.items):
for item, bounds in self.items.iteritems():
if method(bounds, rect):
yield item
for bucket in self._buckets:
for item in bucket.find(rect, method=method):
yield item
def clear(self):
"""
Clear the bucket, including sub-buckets.
@ -384,12 +376,14 @@ class QuadtreeBucket(object):
del self._buckets[:]
self.items.clear()
def dump(self, indent=''):
print(indent, self, self.bounds)
indent += ' '
for item, bounds in sorted(six.iteritems(self.items)):
print(indent, item, bounds)
for bucket in self._buckets:
bucket.dump(indent)
print indent, self, self.bounds
indent += ' '
for item, bounds in sorted(self.items.iteritems()):
print indent, item, bounds
for bucket in self._buckets:
bucket.dump(indent)
# vim:sw=4:et:ai

View File

@ -1,48 +1,26 @@
#!/usr/bin/env python
# Copyright (C) 2010-2017 Arjan Molenaar <gaphor@gmail.com>
# Dan Yeaw <dan@yeaw.me>
#
# This file is part of Gaphas.
#
# This library is free software; you can redistribute it and/or modify it under
# the terms of the GNU Library General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for
# more details.
#
# You should have received a copy of the GNU Library General Public License
# along with this library; if not, see <http://www.gnu.org/licenses/>.
"""
Allow for easily adding segments to lines.
"""
from __future__ import absolute_import
from cairo import Matrix, ANTIALIAS_NONE
from simplegeneric import generic
from six.moves import zip
from gaphas.aspect import ConnectionSink
from gaphas.aspect import HandleFinder, HandleSelection, PaintFocused
from gaphas.aspect import ItemHandleFinder, ItemHandleSelection, ItemPaintFocused
from gaphas.geometry import distance_point_point_fast, distance_line_point
from gaphas.item import Line
from gaphas.aspect import HandleFinder, HandleSelection, PaintFocused
from gaphas.aspect import ConnectionSink
from gaphas.aspect import ItemHandleFinder, ItemHandleSelection, ItemPaintFocused
@generic
class Segment(object):
def __init__(self, item, view):
raise TypeError
@Segment.when_type(Line)
class LineSegment(object):
def __init__(self, item, view):
self.item = item
self.view = view
@ -54,7 +32,7 @@ class LineSegment(object):
for h1, h2 in zip(handles, handles[1:]):
xp = (h1.pos.x + h2.pos.x) / 2
yp = (h1.pos.y + h2.pos.y) / 2
if distance_point_point_fast((x, y), (xp, yp)) <= 4:
if distance_point_point_fast((x,y), (xp, yp)) <= 4:
segment = handles.index(h1)
handles, ports = self.split_segment(segment)
return handles and handles[0]
@ -72,7 +50,7 @@ class LineSegment(object):
segment
Segment number to split (starting from zero).
count
Amount of new segments to be created (minimum 2).
Amount of new segments to be created (minimum 2).
"""
item = self.item
if segment < 0 or segment >= len(item.ports()):
@ -108,6 +86,7 @@ class LineSegment(object):
ports = item.ports()[segment:segment + count - 1]
return handles, ports
def merge_segment(self, segment, count=2):
"""
Merge two (or more) item segments.
@ -119,7 +98,7 @@ class LineSegment(object):
segment
Segment number to start merging from (starting from zero).
count
Amount of segments to be merged (minimum 2).
Amount of segments to be merged (minimum 2).
"""
item = self.item
if len(item.ports()) < 2:
@ -151,6 +130,7 @@ class LineSegment(object):
return deleted_handles, deleted_ports
def _recreate_constraints(self):
"""
Create connection constraints between connecting lines and an item.
@ -160,10 +140,9 @@ class LineSegment(object):
Connected item.
"""
connected = self.item
def find_port(line, handle, item):
# port = None
# max_dist = sys.maxint
#port = None
#max_dist = sys.maxint
canvas = item.canvas
ix, iy = canvas.get_matrix_i2i(line, item).transform_point(*handle.pos)
@ -233,7 +212,7 @@ class SegmentHandleSelection(ItemHandleSelection):
# cannot merge starting from last segment
if segment == len(item.ports()) - 1:
segment = - 1
segment =- 1
assert segment >= 0 and segment < len(item.ports()) - 1
before = handles[handle_index - 1]
@ -248,10 +227,11 @@ class SegmentHandleSelection(ItemHandleSelection):
item.request_update()
@PaintFocused.when_type(Line)
class LineSegmentPainter(ItemPaintFocused):
"""
This painter draws pseudo-hanldes on gaphas.item.Line objects. Each
This painter draws pseudo-handles on gaphas.item.Line objects. Each
line can be split by dragging those points, which will result in
a new handle.
@ -283,4 +263,6 @@ class LineSegmentPainter(ItemPaintFocused):
cr.stroke()
cr.restore()
# vim:sw=4:et:ai

View File

@ -1,24 +1,3 @@
#!/usr/bin/env python
# Copyright (C) 2006-2017 Arjan Molenaar <gaphor@gmail.com>
# Artur Wroblewski <wrobell@pld-linux.org>
# Dan Yeaw <dan@yeaw.me>
#
# This file is part of Gaphas.
#
# This library is free software; you can redistribute it and/or modify it under
# the terms of the GNU Library General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for
# more details.
#
# You should have received a copy of the GNU Library General Public License
# along with this library; if not, see <http://www.gnu.org/licenses/>.
"""
Constraint solver allows to define constraint between two or more different
variables and keep this constraint always true when one or more of the
@ -52,14 +31,14 @@ constraint is being asked to solve itself (`constraint.Constraint.solve_for()`
method) changing appropriate variables to make the constraint valid again.
"""
from __future__ import absolute_import
from __future__ import division
from gaphas.state import observed, reversible_pair, reversible_property
__version__ = "$Revision$"
# $HeadURL$
from operator import isCallable
from state import observed, reversible_pair, reversible_property
# epsilon for float comparison
# is simple abs(x - y) > EPSILON enough for canvas needs?
EPSILON = 1e-6
@ -76,9 +55,9 @@ REQUIRED = 100
class Variable(object):
"""
Representation of a variable in the constraint solver.
Each Variable has a @value and a @strength. In a constraint the
Each Variable has a @value and a @strength. Ina constraint the
weakest variables are changed.
You can even do some calculating with it. The Variable always represents
a float variable.
"""
@ -91,9 +70,6 @@ class Variable(object):
self._solver = None
self._constraints = set()
def __hash__(self):
return hash((self._value, self._strength))
@observed
def _set_strength(self, strength):
self._strength = strength
@ -106,7 +82,7 @@ class Variable(object):
"""
Mark the variable dirty in both the constraint solver and attached
constraints.
Variables are marked dirty also during constraints solving to
solve all dependent constraints, i.e. two equals constraints
between 3 variables.
@ -121,7 +97,7 @@ class Variable(object):
def set_value(self, value):
oldval = self._value
if abs(oldval - value) > EPSILON:
# print id(self), oldval, value
#print id(self), oldval, value
self._value = float(value)
self.dirty()
@ -129,7 +105,6 @@ class Variable(object):
def __str__(self):
return 'Variable(%g, %d)' % (self._value, self._strength)
__repr__ = __str__
def __float__(self):
@ -386,7 +361,6 @@ class Projection(object):
def __str__(self):
return '%s(%s)' % (self.__class__.__name__, self.variable())
__repr__ = __str__
@ -404,6 +378,7 @@ class Solver(object):
constraints = property(lambda s: s._constraints)
def request_resolve(self, variable, projections_only=False):
"""
Mark a variable as "dirty". This means it it solved the next time
@ -449,8 +424,8 @@ class Solver(object):
c.mark_dirty(variable)
self._marked_cons.append(c)
if self._marked_cons.count(c) > 100:
raise JuggleError('Variable juggling detected, constraint %s resolved %d times out of %d' % (
c, self._marked_cons.count(c), len(self._marked_cons)))
raise JuggleError, 'Variable juggling detected, constraint %s resolved %d times out of %d' % (c, self._marked_cons.count(c), len(self._marked_cons))
@observed
def add_constraint(self, constraint):
@ -485,7 +460,7 @@ class Solver(object):
constraint._solver_has_projections = True
v._constraints.add(constraint)
v._solver = self
# print 'added constraint', constraint
#print 'added constraint', constraint
return constraint
@observed
@ -520,12 +495,14 @@ class Solver(object):
reversible_pair(add_constraint, remove_constraint)
def request_resolve_constraint(self, c):
"""
Request resolving a constraint.
"""
self._marked_cons.append(c)
def constraints_with_variable(self, *variables):
"""
Return an iterator of constraints that work with variable.
@ -590,12 +567,13 @@ class Solver(object):
else:
found = False
if not found:
break # quit for loop, variable not in constraint
break # quit for loop, variable not in constraint
else:
# All iteration have completed succesfully,
# so all variables are in the constraint
yield c
def solve(self):
"""
Example:
@ -661,9 +639,9 @@ class solvable(object):
>>> a._v_x
Variable(12, 20)
>>> a.x = 3
>>> a.x
>>> a.x
Variable(3, 20)
>>> a.y
>>> a.y
Variable(0, 30)
"""
@ -702,7 +680,7 @@ class JuggleError(AssertionError):
__test__ = {
'Solver.add_constraint': Solver.add_constraint,
'Solver.remove_constraint': Solver.remove_constraint,
}
}
# vim:sw=4:et:ai

View File

@ -1,24 +1,3 @@
#!/usr/bin/env python
# Copyright (C) 2007-2017 Arjan Molenaar <gaphor@gmail.com>
# Artur Wroblewski <wrobell@pld-linux.org>
# Dan Yeaw <dan@yeaw.me>
#
# This file is part of Gaphas.
#
# This library is free software; you can redistribute it and/or modify it under
# the terms of the GNU Library General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for
# more details.
#
# You should have received a copy of the GNU Library General Public License
# along with this library; if not, see <http://www.gnu.org/licenses/>.
"""
This module is the central point where Gaphas' classes report their state
changes.
@ -39,18 +18,12 @@ For this to work the revert_handler has to be added to the observers set::
"""
from __future__ import absolute_import
import inspect
import types, inspect
import threading
import types
import six
from decorator import decorator
from six.moves import zip
# This string is added to each docstring in order to denote is's observed
# OBSERVED_DOCSTRING = \
#OBSERVED_DOCSTRING = \
# '\n\n This method is @observed. See gaphas.state for extra info.\n'
# Tell @observed to dispatch invokation messages by default
@ -59,19 +32,19 @@ from six.moves import zip
DISPATCH_BY_DEFAULT = True
# Add/remove methods from this subscribers list.
# Subscribers should have signature method(event) where event is a
# Subscribers should have signature method(event) where event is a
# Event has the form: (func, keywords)
# Since most events originate from methods, it's save to call
# saveapply(func, keywords) for those functions
subscribers = set()
# Subscribe to low-level change events:
observers = set()
# Perform locking (should be per thread?).
mutex = threading.Lock()
def observed(func):
"""
Simple observer, dispatches events to functions registered in the observers
@ -84,7 +57,6 @@ def observed(func):
Also note that the events are dispatched *before* the function is invoked.
This is an important feature, esp. for the reverter code.
"""
def wrapper(func, *args, **kwargs):
o = func.__observer__
acquired = mutex.acquire(False)
@ -95,7 +67,6 @@ def observed(func):
finally:
if acquired:
mutex.release()
dec = decorator(wrapper)(func)
func.__observer__ = dec
@ -124,8 +95,7 @@ def dispatch(event, queue):
>>> observers.remove(handler)
>>> callme()
"""
for s in queue:
s(event)
for s in queue: s(event)
_reverse = dict()
@ -163,10 +133,10 @@ def reversible_pair(func1, func2, bind1={}, bind2={}):
def reversible_property(fget=None, fset=None, fdel=None, doc=None, bind={}):
"""
Replacement for the property descriptor. In addition to creating a
property instance, the property is registered as reversible and
property instance, the property is registered as reversible and
reverse events can be send out when changes occur.
Cave eat: we can't handle both fset and fdel in the proper way. Therefore
Caveat: we can't handle both fset and fdel in the proper way. Therefore
fdel should somehow invoke fset. (persinally, I hardly use fdel)
See revert_handler() for doctesting.
@ -182,8 +152,8 @@ def reversible_property(fget=None, fset=None, fdel=None, doc=None, bind={}):
argself, argvalue = argnames
func = getfunction(fset)
b = {argvalue: eval("lambda %(self)s: fget(%(self)s)" % {'self': argself},
{'fget': fget})}
b = { argvalue: eval("lambda %(self)s: fget(%(self)s)" % {'self': argself },
{'fget': fget}) }
b.update(bind)
_reverse[func] = (func, spec, b)
@ -198,7 +168,7 @@ def revert_handler(event):
First thing to do is to actually enable the revert_handler:
>>> observers.add(revert_handler)
First let's define our simple list:
>>> class SList(object):
@ -255,21 +225,18 @@ def revert_handler(event):
return
kw = dict(kwargs)
kw.update(dict(list(zip(spec[0], args))))
for arg, binding in six.iteritems(bind):
kw.update(dict(zip(spec[0], args)))
for arg, binding in bind.iteritems():
kw[arg] = saveapply(binding, kw)
argnames = list(revspec[0])
if spec[1]:
argnames.append(revspec[1])
if spec[2]:
argnames.append(revspec[2])
if spec[1]: argnames.append(revspec[1])
if spec[2]: argnames.append(revspec[2])
kwargs = {}
for arg in argnames:
kwargs[arg] = kw.get(arg)
dispatch((reverse, kwargs), queue=subscribers)
def saveapply(func, kw):
"""
Do apply a set of keywords to a method or function.
@ -278,10 +245,8 @@ def saveapply(func, kw):
"""
spec = inspect.getargspec(func)
argnames = list(spec[0])
if spec[1]:
argnames.append(spec[1])
if spec[2]:
argnames.append(spec[2])
if spec[1]: argnames.append(spec[1])
if spec[2]: argnames.append(spec[2])
kwargs = {}
for arg in argnames:
kwargs[arg] = kw.get(arg)
@ -292,8 +257,9 @@ def getfunction(func):
"""
Return the function associated with a class method.
"""
if isinstance(func, types.MethodType):
return func.__func__
if isinstance(func, types.UnboundMethodType):
return func.im_func
return func
# vim:sw=4:et:ai

View File

@ -1,36 +1,8 @@
#!/usr/bin/env python
# Copyright (C) 2009-2017 Arjan Molenaar <gaphor@gmail.com>
# Artur Wroblewski <wrobell@pld-linux.org>
# Dan Yeaw <dan@yeaw.me>
#
# This file is part of Gaphas.
#
# This library is free software; you can redistribute it and/or modify it under
# the terms of the GNU Library General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for
# more details.
#
# You should have received a copy of the GNU Library General Public License
# along with this library; if not, see <http://www.gnu.org/licenses/>.
"""
Table is a storage class that can be used to store information, like one
would in a database table, with indexes on the desired "columns."
"""
from __future__ import absolute_import
from functools import reduce
from six.moves import zip
class Table(object):
"""
A Table structure with indexing. Optimized for lookups.
@ -55,8 +27,10 @@ class Table(object):
index[n] = dict()
self._index = index
columns = property(lambda s: s._type)
def insert(self, *values):
"""
Add a set of values to the store.
@ -76,9 +50,7 @@ class Table(object):
ValueError: Number of arguments doesn't match the number of columns (2 != 3)
"""
if len(values) != len(self._type._fields):
raise ValueError("Number of arguments doesn't match the number of columns (%d != %d)" % (
len(values), len(self._type._fields))
)
raise ValueError, "Number of arguments doesn't match the number of columns (%d != %d)" % (len(values), len(self._type._fields))
# Add value to index entries
index = self._index
data = self._type._make(values)
@ -89,6 +61,7 @@ class Table(object):
else:
index[n][v] = set([data])
def delete(self, *_row, **kv):
"""
Remove value from the table. Either a complete set may be given or
@ -130,10 +103,10 @@ class Table(object):
"""
fields = self._type._fields
if _row and kv:
raise ValueError("Should either provide a row or a query statement, not both")
raise ValueError, "Should either provide a row or a query statement, not both"
if _row:
assert len(_row) == len(fields)
kv = dict(list(zip(self._indexes, _row)))
kv = dict(zip(self._indexes, _row))
rows = list(self.query(**kv))
@ -146,6 +119,7 @@ class Table(object):
if len(index[n][v]) == 0:
del index[n][v]
def query(self, **kv):
"""
Get rows (tuples) for each key defined. An iterator is returned.
@ -195,8 +169,9 @@ class Table(object):
rows = (index[n][v] for n, v in items)
try:
r = iter(reduce(set.intersection, rows))
except TypeError as ex:
except TypeError, ex:
pass
return r
# vi:sw=4:et:ai

View File

@ -1,18 +0,0 @@
#!/usr/bin/env python
# Copyright (C) 2007 Artur Wroblewski <wrobell@pld-linux.org>
#
# This file is part of Gaphas.
#
# This library is free software; you can redistribute it and/or modify it under
# the terms of the GNU Library General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for
# more details.
#
# You should have received a copy of the GNU Library General Public License
# along with this library; if not, see <http://www.gnu.org/licenses/>.

View File

@ -1,37 +1,14 @@
#!/usr/bin/env python
# Copyright (C) 2009-2017 Arjan Molenaar <gaphor@gmail.com>
# Dan Yeaw <dan@yeaw.me>
#
# This file is part of Gaphas.
#
# This library is free software; you can redistribute it and/or modify it under
# the terms of the GNU Library General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for
# more details.
#
# You should have received a copy of the GNU Library General Public License
# along with this library; if not, see <http://www.gnu.org/licenses/>.
"""
Generic gaphas item tests.
"""
from __future__ import absolute_import
import unittest
from gaphas.item import Item
from gaphas.aspect import *
from gaphas.canvas import Canvas
from gaphas.item import Item
from gaphas.view import View
class AspectTestCase(unittest.TestCase):
"""
Test aspects for items.
@ -41,6 +18,7 @@ class AspectTestCase(unittest.TestCase):
self.canvas = Canvas()
self.view = View(self.canvas)
def test_selection_select(self):
"""
Test the Selection role methods
@ -57,6 +35,7 @@ class AspectTestCase(unittest.TestCase):
assert item not in view.selected_items
assert None is view.focused_item
def test_selection_move(self):
"""
Test the Selection role methods
@ -70,4 +49,5 @@ class AspectTestCase(unittest.TestCase):
inmotion.move((12, 26))
self.assertEquals((1, 0, 0, 1, 12, 26), tuple(item.matrix))
# vim:sw=4:et:ai

View File

@ -1,34 +1,10 @@
#!/usr/bin/env python
# Copyright (C) 2007-2017 Arjan Molenaar <gaphor@gmail.com>
# Artur Wroblewski <wrobell@pld-linux.org>
# Dan Yeaw <dan@yeaw.me>
#
# This file is part of Gaphas.
#
# This library is free software; you can redistribute it and/or modify it under
# the terms of the GNU Library General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for
# more details.
#
# You should have received a copy of the GNU Library General Public License
# along with this library; if not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import
import unittest
import cairo
from gaphas.canvas import Canvas, ConnectionError
from gaphas.examples import Box
from gaphas.item import Line
from gaphas.item import Line, Handle
from gaphas.constraint import BalanceConstraint, EqualsConstraint
import cairo
class MatricesTestCase(unittest.TestCase):
def test_update_matrices(self):
@ -55,7 +31,6 @@ class MatricesTestCase(unittest.TestCase):
c.add(b2, b1)
c.reparent(b2, None)
# fixme: what about multiple constraints for a handle?
# what about 1d projection?
@ -64,6 +39,7 @@ def count(i):
class CanvasTestCase(unittest.TestCase):
def test_connect_item(self):
b1 = Box()
b2 = Box()
@ -81,12 +57,12 @@ class CanvasTestCase(unittest.TestCase):
assert count(c.get_connections(handle=l.handles()[0])) == 1
# Same item, different port
# c.connect_item(l, l.handles()[0], b1, b1.ports()[-1])
# assert count(c.get_connections(handle=l.handles()[0])) == 1
#c.connect_item(l, l.handles()[0], b1, b1.ports()[-1])
#assert count(c.get_connections(handle=l.handles()[0])) == 1
# Different item
# c.connect_item(l, l.handles()[0], b2, b2.ports()[0])
# assert count(c.get_connections(handle=l.handles()[0])) == 1
#c.connect_item(l, l.handles()[0], b2, b2.ports()[0])
#assert count(c.get_connections(handle=l.handles()[0])) == 1
def test_disconnect_item_with_callback(self):
b1 = Box()
@ -98,7 +74,6 @@ class CanvasTestCase(unittest.TestCase):
c.add(l)
events = []
def callback():
events.append('called')
@ -109,6 +84,7 @@ class CanvasTestCase(unittest.TestCase):
assert count(c.get_connections(handle=l.handles()[0])) == 0
assert events == ['called']
def test_disconnect_item_with_constraint(self):
b1 = Box()
b2 = Box()
@ -131,6 +107,7 @@ class CanvasTestCase(unittest.TestCase):
assert len(c.solver.constraints) == 4
def test_disconnect_item_by_deleting_element(self):
b1 = Box()
b2 = Box()
@ -141,7 +118,6 @@ class CanvasTestCase(unittest.TestCase):
c.add(l)
events = []
def callback():
events.append('called')
@ -153,6 +129,7 @@ class CanvasTestCase(unittest.TestCase):
assert count(c.get_connections(handle=l.handles()[0])) == 0
assert events == ['called']
def test_disconnect_item_with_constraint_by_deleting_element(self):
b1 = Box()
b2 = Box()
@ -178,6 +155,7 @@ class CanvasTestCase(unittest.TestCase):
class ConstraintProjectionTestCase(unittest.TestCase):
def test_line_projection(self):
"""Test projection with line's handle on element's side"""
line = Line()
@ -199,31 +177,31 @@ class ConstraintProjectionTestCase(unittest.TestCase):
# move line's second handle on box side
h2.x, h2.y = 5, -20
# bc = BalanceConstraint(band=(h_sw.x, h_se.x), v=h2.x, balance=0.25)
# canvas.projector(bc, x={h_sw.x: box, h_se.x: box, h2.x: line})
# canvas._solver.add_constraint(bc)
#
# eq = EqualsConstraint(a=h_se.y, b=h2.y)
# canvas.projector(eq, y={h_se.y: box, h2.y: line})
# canvas._solver.add_constraint(eq)
#
# box.request_update()
# line.request_update()
#
# canvas.update()
#
# box.width = 60
# box.height = 30
#
# canvas.update()
#
# # expect h2.x to be moved due to balance constraint
# self.assertEquals(10, h2.x)
# self.assertEquals(-10, h2.y)
### bc = BalanceConstraint(band=(h_sw.x, h_se.x), v=h2.x, balance=0.25)
### canvas.projector(bc, x={h_sw.x: box, h_se.x: box, h2.x: line})
### canvas._solver.add_constraint(bc)
###
### eq = EqualsConstraint(a=h_se.y, b=h2.y)
### canvas.projector(eq, y={h_se.y: box, h2.y: line})
### canvas._solver.add_constraint(eq)
###
### box.request_update()
### line.request_update()
###
### canvas.update()
###
### box.width = 60
### box.height = 30
###
### canvas.update()
###
### # expect h2.x to be moved due to balance constraint
### self.assertEquals(10, h2.x)
### self.assertEquals(-10, h2.y)
class CanvasConstraintTestCase(unittest.TestCase):
def test_remove_connected_item(self):
"""Test adding canvas constraint"""
canvas = Canvas()
@ -233,6 +211,7 @@ class CanvasConstraintTestCase(unittest.TestCase):
l1 = Line()
canvas.add(l1)
b1 = Box()
canvas.add(b1)
@ -257,6 +236,7 @@ class CanvasConstraintTestCase(unittest.TestCase):
assert canvas.get_connection(l1.handles()[1])
self.assertEquals(number_cons2 + 2, len(canvas.solver.constraints))
canvas.remove(b1)
@ -264,112 +244,112 @@ class CanvasConstraintTestCase(unittest.TestCase):
# Expecting a class + line connected at one end only
self.assertEquals(number_cons1 + 1, len(canvas.solver.constraints))
# def test_adding_constraint(self):
# """Test adding canvas constraint"""
# canvas = Canvas()
#
# l1 = Line()
# canvas.add(l1)
#
# h1, h2 = l1.handles()
# h = Handle()
#
# eq1 = EqualsConstraint(h1.x, h.x)
# canvas.add_canvas_constraint(l1, h1, eq1)
# self.assertTrue(l1 in cons)
# self.assertTrue(h1 in cons[l1])
# self.assertTrue(eq1 in cons[l1][h1])
#
# l2 = Line()
# canvas.add(l2)
#
# h1, h2 = l2.handles()
# h = Handle()
#
# eq2 = EqualsConstraint(h1.x, h.x)
# canvas.add_canvas_constraint(l2, h1, eq2)
# self.assertTrue(l2 in cons)
# self.assertTrue(h1 in cons[l2])
# self.assertTrue(eq2 in cons[l2][h1])
#
#
# def test_adding_constraint_ex(self):
# """Test adding canvas constraint for non-existing item"""
# canvas = Canvas()
# l1 = Line()
# h1, h2 = l1.handles()
# h = Handle()
#
# eq = EqualsConstraint(h1.x, h.x)
# self.assertRaises(ValueError, canvas.add_canvas_constraint, l1, h1, eq)
#
#
# def test_removing_constraint(self):
# """Test removing canvas constraint"""
# canvas = Canvas()
# cons = canvas._canvas_constraints
#
# l1 = Line()
# canvas.add(l1)
#
# h1, h2 = l1.handles()
# h = Handle()
#
# eq1 = EqualsConstraint(h1.x, h.x)
# canvas.add_canvas_constraint(l1, h1, eq1)
#
# # test preconditions
# assert l1 in cons
# assert h1 in cons[l1]
# assert eq1 in cons[l1][h1]
#
# canvas.remove_canvas_constraint(l1, h1, eq1)
# self.assertTrue(l1 in cons)
# self.assertTrue(h1 in cons[l1])
# self.assertFalse(eq1 in cons[l1][h1])
#
# eq1 = EqualsConstraint(h1.x, h.x)
# eq2 = EqualsConstraint(h1.y, h.y)
# canvas.add_canvas_constraint(l1, h1, eq1)
# canvas.add_canvas_constraint(l1, h1, eq2)
#
# # test preconditions
# assert l1 in cons
# assert h1 in cons[l1]
# assert eq1 in cons[l1][h1]
# assert eq2 in cons[l1][h1]
#
# canvas.remove_canvas_constraint(l1, h1)
#
# self.assertTrue(l1 in cons)
# self.assertTrue(h1 in cons[l1])
# self.assertFalse(eq1 in cons[l1][h1])
# self.assertFalse(eq2 in cons[l1][h1])
#
#
# def test_fetching_constraints(self):
# """Test fetching canvas constraints"""
# canvas = Canvas()
# cons = canvas._canvas_constraints
#
# l1 = Line()
# canvas.add(l1)
#
# h1, h2 = l1.handles()
# h = Handle()
#
# eq1 = EqualsConstraint(h1.x, h.x)
# eq2 = EqualsConstraint(h1.y, h.y)
# canvas.add_canvas_constraint(l1, h1, eq1)
# canvas.add_canvas_constraint(l1, h1, eq2)
#
# # test preconditions
# assert l1 in cons
# assert h1 in cons[l1]
# assert eq1 in cons[l1][h1]
# assert eq2 in cons[l1][h1]
#
# self.assertTrue(eq1 in canvas.canvas_constraints(l1))
# self.assertTrue(eq2 in canvas.canvas_constraints(l1))
### def test_adding_constraint(self):
### """Test adding canvas constraint"""
### canvas = Canvas()
###
### l1 = Line()
### canvas.add(l1)
###
### h1, h2 = l1.handles()
### h = Handle()
###
### eq1 = EqualsConstraint(h1.x, h.x)
### canvas.add_canvas_constraint(l1, h1, eq1)
### self.assertTrue(l1 in cons)
### self.assertTrue(h1 in cons[l1])
### self.assertTrue(eq1 in cons[l1][h1])
###
### l2 = Line()
### canvas.add(l2)
###
### h1, h2 = l2.handles()
### h = Handle()
###
### eq2 = EqualsConstraint(h1.x, h.x)
### canvas.add_canvas_constraint(l2, h1, eq2)
### self.assertTrue(l2 in cons)
### self.assertTrue(h1 in cons[l2])
### self.assertTrue(eq2 in cons[l2][h1])
###
###
### def test_adding_constraint_ex(self):
### """Test adding canvas constraint for non-existing item"""
### canvas = Canvas()
### l1 = Line()
### h1, h2 = l1.handles()
### h = Handle()
###
### eq = EqualsConstraint(h1.x, h.x)
### self.assertRaises(ValueError, canvas.add_canvas_constraint, l1, h1, eq)
###
###
### def test_removing_constraint(self):
### """Test removing canvas constraint"""
### canvas = Canvas()
### cons = canvas._canvas_constraints
###
### l1 = Line()
### canvas.add(l1)
###
### h1, h2 = l1.handles()
### h = Handle()
###
### eq1 = EqualsConstraint(h1.x, h.x)
### canvas.add_canvas_constraint(l1, h1, eq1)
###
### # test preconditions
### assert l1 in cons
### assert h1 in cons[l1]
### assert eq1 in cons[l1][h1]
###
### canvas.remove_canvas_constraint(l1, h1, eq1)
### self.assertTrue(l1 in cons)
### self.assertTrue(h1 in cons[l1])
### self.assertFalse(eq1 in cons[l1][h1])
###
### eq1 = EqualsConstraint(h1.x, h.x)
### eq2 = EqualsConstraint(h1.y, h.y)
### canvas.add_canvas_constraint(l1, h1, eq1)
### canvas.add_canvas_constraint(l1, h1, eq2)
###
### # test preconditions
### assert l1 in cons
### assert h1 in cons[l1]
### assert eq1 in cons[l1][h1]
### assert eq2 in cons[l1][h1]
###
### canvas.remove_canvas_constraint(l1, h1)
###
### self.assertTrue(l1 in cons)
### self.assertTrue(h1 in cons[l1])
### self.assertFalse(eq1 in cons[l1][h1])
### self.assertFalse(eq2 in cons[l1][h1])
###
###
### def test_fetching_constraints(self):
### """Test fetching canvas constraints"""
### canvas = Canvas()
### cons = canvas._canvas_constraints
###
### l1 = Line()
### canvas.add(l1)
###
### h1, h2 = l1.handles()
### h = Handle()
###
### eq1 = EqualsConstraint(h1.x, h.x)
### eq2 = EqualsConstraint(h1.y, h.y)
### canvas.add_canvas_constraint(l1, h1, eq1)
### canvas.add_canvas_constraint(l1, h1, eq2)
###
### # test preconditions
### assert l1 in cons
### assert h1 in cons[l1]
### assert eq1 in cons[l1][h1]
### assert eq2 in cons[l1][h1]
###
### self.assertTrue(eq1 in canvas.canvas_constraints(l1))
### self.assertTrue(eq2 in canvas.canvas_constraints(l1))
# vim:sw=4:et:ai

View File

@ -1,32 +1,10 @@
#!/usr/bin/env python
# Copyright (C) 2009-2017 Arjan Molenaar <gaphor@gmail.com>
# Dan Yeaw <dan@yeaw.me>
#
# This file is part of Gaphas.
#
# This library is free software; you can redistribute it and/or modify it under
# the terms of the GNU Library General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for
# more details.
#
# You should have received a copy of the GNU Library General Public License
# along with this library; if not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import
import unittest
from gaphas.connector import Position, Handle
from gaphas.solver import Variable
class PositionTestCase(unittest.TestCase):
def test_position(self):
pos = Position((0, 0))
self.assertEquals(0, pos.x)
@ -38,7 +16,7 @@ class PositionTestCase(unittest.TestCase):
self.assertEquals(2, pos.y)
def test_set_xy(self):
pos = Position((1, 2))
pos = Position((1,2))
x = Variable()
y = Variable()
assert x is not pos.x
@ -49,8 +27,8 @@ class PositionTestCase(unittest.TestCase):
assert x is pos.x
assert y is pos.y
class HandleTestCase(unittest.TestCase):
def test_handle_x_y(self):
h = Handle()
self.assertEquals(0.0, h.x)

View File

@ -1,30 +1,7 @@
#!/usr/bin/env python
# Copyright (C) 2008-2017 Arjan Molenaar <gaphor@gmail.com>
# Dan Yeaw <dan@yeaw.me>
#
# This file is part of Gaphas.
#
# This library is free software; you can redistribute it and/or modify it under
# the terms of the GNU Library General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for
# more details.
#
# You should have received a copy of the GNU Library General Public License
# along with this library; if not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import
import unittest
from gaphas.constraint import PositionConstraint, LineAlignConstraint
from gaphas.solver import Variable
from gaphas.constraint import PositionConstraint, LineAlignConstraint
class PositionTestCase(unittest.TestCase):
def test_pos_constraint(self):
@ -53,11 +30,11 @@ class PositionTestCase(unittest.TestCase):
self.assertEquals(14, y2)
class LineAlignConstraintTestCase(unittest.TestCase):
"""
Line align constraint test case.
"""
def test_delta(self):
"""Test line align delta
"""
@ -69,11 +46,12 @@ class LineAlignConstraintTestCase(unittest.TestCase):
self.assertAlmostEqual(12.77, point[1].value, 2)
line[1][0].value = 40
line[1][1].value = 30
line[1][1].value = 30
lc.solve_for()
self.assertAlmostEqual(24.00, point[0].value, 2)
self.assertAlmostEqual(18.00, point[1].value, 2)
def test_delta_below_zero(self):
"""Test line align with delta below zero
"""
@ -85,7 +63,7 @@ class LineAlignConstraintTestCase(unittest.TestCase):
self.assertAlmostEqual(7.23, point[1].value, 2)
line[1][0].value = 40
line[1][1].value = 30
line[1][1].value = 30
lc.solve_for()
self.assertAlmostEqual(16.0, point[0].value, 2)
self.assertAlmostEqual(12.00, point[1].value, 2)

View File

@ -1,37 +1,14 @@
#!/usr/bin/env python
# Copyright (C) 2007-2017 Arjan Molenaar <gaphor@gmail.com>
# Artur Wroblewski <wrobell@pld-linux.org>
# Dan Yeaw <dan@yeaw.me>
#
# This file is part of Gaphas.
#
# This library is free software; you can redistribute it and/or modify it under
# the terms of the GNU Library General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for
# more details.
#
# You should have received a copy of the GNU Library General Public License
# along with this library; if not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import
import unittest
from os import getenv
from six.moves import range
import unittest
from gaphas.canvas import Canvas
from gaphas.examples import Box
from gaphas.item import NW, NE, SE, SW
class ElementTestCase(unittest.TestCase):
def test_creation_with_size(self):
"""
Test if initial size holds when added to a canvas.
@ -51,6 +28,7 @@ class ElementTestCase(unittest.TestCase):
assert box.handles()[SE].pos.x == 150, box.handles()[SE].pos.x
assert box.handles()[SE].pos.y == 153, box.handles()[SE].pos.y
def test_resize_se(self):
"""
Test resizing of element by dragging it SE handle.
@ -68,8 +46,7 @@ class ElementTestCase(unittest.TestCase):
assert h_se is handles[SE]
# to see how many solver was called:
# GAPHAS_TEST_COUNT=3 nosetests -s --with-prof --profile-restrict=gaphas
# gaphas/tests/test_element.py | grep -e '\<solve\>' -e dirty
# GAPHAS_TEST_COUNT=3 nosetests -s --with-prof --profile-restrict=gaphas gaphas/tests/test_element.py | grep -e '\<solve\>' -e dirty
count = getenv('GAPHAS_TEST_COUNT')
if count:
@ -78,17 +55,18 @@ class ElementTestCase(unittest.TestCase):
count = 1
for i in range(count):
h_se.pos.x += 100 # h.se.{x,y} = 10, now
h_se.pos.x += 100 # h.se.{x,y} = 10, now
h_se.pos.y += 100
box.request_update()
canvas.update()
self.assertEquals(110 * count, h_se.pos.x) # h_se changed above, should remain the same
self.assertEquals(110 * count, h_se.pos.x) # h_se changed above, should remain the same
self.assertEquals(110 * count, float(h_se.pos.y))
self.assertEquals(110 * count, float(h_ne.pos.x))
self.assertEquals(110 * count, float(h_sw.pos.y))
def test_minimal_se(self):
"""
Test resizing of element by dragging it SE handle.
@ -105,14 +83,14 @@ class ElementTestCase(unittest.TestCase):
assert h_sw is handles[SW]
assert h_se is handles[SE]
h_se.pos.x -= 20 # h.se.{x,y} == -10
h_se.pos.x -= 20 # h.se.{x,y} == -10
h_se.pos.y -= 20
assert h_se.pos.x == h_se.pos.y == -10
box.request_update()
canvas.update()
self.assertEquals(10, h_se.pos.x) # h_se changed above, should be 10
self.assertEquals(10, h_se.pos.x) # h_se changed above, should be 10
self.assertEquals(10, h_se.pos.y)
self.assertEquals(10, h_ne.pos.x)

View File

@ -1,43 +1,20 @@
#!/usr/bin/env python
# Copyright (C) 2010-2017 Arjan Molenaar <gaphor@gmail.com>
# Dan Yeaw <dan@yeaw.me>
#
# This file is part of Gaphas.
#
# This library is free software; you can redistribute it and/or modify it under
# the terms of the GNU Library General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for
# more details.
#
# You should have received a copy of the GNU Library General Public License
# along with this library; if not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import
from __future__ import print_function
import unittest
from gaphas.freehand import FreeHandCairoContext
import cairo
from gaphas.freehand import FreeHandCairoContext
class PseudoFile(object):
def __init__(self):
self.data = ''
def write(self, data):
print(data)
print data
self.data = self.data + data
class FreeHandCairoContextTest(unittest.TestCase):
def setUp(self):
pass
@ -64,7 +41,7 @@ class FreeHandCairoContextTest(unittest.TestCase):
DRAWING_LINES_OUTPUT = """<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100pt" height="100pt"viewBox="0 0 100 100" version="1.1">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100pt" height="100pt" viewBox="0 0 100 100" version="1.1">
<g id="surface0">
<path style="fill:none;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke:rgb(0%,0%,0%);stroke-opacity:1;stroke-miterlimit:10;" d="M 20 20 C 23.324219 50.054688 17.195312 33.457031 20.722656 80.585938 C 38.78125 83.566406 20.984375 77.625 80.652344 80.652344 C 83.65625 70.992188 77.578125 60.988281 80.507812 20.019531 "/>
</g>

View File

@ -1,42 +1,18 @@
#!/usr/bin/env python
# Copyright (C) 2010-2017 Arjan Molenaar <gaphor@gmail.com>
# Dan Yeaw <dan@yeaw.me>
#
# This file is part of Gaphas.
#
# This library is free software; you can redistribute it and/or modify it under
# the terms of the GNU Library General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for
# more details.
#
# You should have received a copy of the GNU Library General Public License
# along with this library; if not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import
from __future__ import print_function
import unittest
from gi.repository import Gtk
from six.moves import range
from gaphas.canvas import Canvas
import gtk
from gaphas.guide import *
from gaphas.item import Element, Line
from gaphas.canvas import Canvas
from gaphas.view import GtkView
from gaphas.item import Element, Line
class GuideTestCase(unittest.TestCase):
def setUp(self):
self.canvas = Canvas()
self.view = GtkView(self.canvas)
self.window = Gtk.Window()
self.window = gtk.Window()
self.window.add(self.view)
self.window.show_all()
@ -109,6 +85,7 @@ class GuideTestCase(unittest.TestCase):
self.assertEquals(0.0, guides[0])
self.assertEquals(20.0, guides[1])
def test_guide_item_in_motion(self):
e1 = Element()
e2 = Element()
@ -135,13 +112,13 @@ class GuideTestCase(unittest.TestCase):
# Moved back to guided lines:
for d in range(0, 3):
print('move to', d)
print 'move to', d
guider.move((d, d))
self.assertEquals(0, e3.matrix[4])
self.assertEquals(0, e3.matrix[5])
for d in range(3, 5):
print('move to', d)
print 'move to', d
guider.move((d, d))
self.assertEquals(5, e3.matrix[4])
self.assertEquals(5, e3.matrix[5])
@ -150,6 +127,7 @@ class GuideTestCase(unittest.TestCase):
self.assertEquals(20, e3.matrix[4])
self.assertEquals(20, e3.matrix[5])
def test_guide_item_in_motion_2(self):
e1 = Element()
e2 = Element()
@ -176,13 +154,13 @@ class GuideTestCase(unittest.TestCase):
# Moved back to guided lines:
for y in range(4, 6):
print('move to', y)
print 'move to', y
guider.move((3, y))
self.assertEquals(0, e3.matrix[4])
self.assertEquals(0, e3.matrix[5])
for y in range(6, 9):
print('move to', y)
print 'move to', y
guider.move((3, y))
self.assertEquals(0, e3.matrix[4])
self.assertEquals(5, e3.matrix[5])
@ -192,4 +170,5 @@ class GuideTestCase(unittest.TestCase):
self.assertEquals(17, e3.matrix[4])
self.assertEquals(20, e3.matrix[5])
# vim:sw=4:et:ai

View File

@ -1,44 +1,19 @@
#!/usr/bin/env python
# Copyright (C) 2008-2017 Arjan Molenaar <gaphor@gmail.com>
# Artur Wroblewski <wrobell@pld-linux.org>
# Dan Yeaw <dan@yeaw.me>
#
# This file is part of Gaphas.
#
# This library is free software; you can redistribute it and/or modify it under
# the terms of the GNU Library General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for
# more details.
#
# You should have received a copy of the GNU Library General Public License
# along with this library; if not, see <http://www.gnu.org/licenses/>.
"""
Generic gaphas item tests.
"""
from __future__ import absolute_import
import unittest
from gaphas.constraint import LineConstraint, \
EqualsConstraint, LessThanConstraint
from gaphas.item import Item
from gaphas.constraint import LineAlignConstraint, LineConstraint, \
EqualsConstraint, LessThanConstraint
from gaphas.solver import Variable
class ItemConstraintTestCase(unittest.TestCase):
"""
Item constraint creation tests. The test check functionality of
`Item.constraint` method, not constraints themselves.
"""
def test_line_constraint(self):
"""
Test line creation constraint.
@ -54,6 +29,7 @@ class ItemConstraintTestCase(unittest.TestCase):
self.assertEquals((1, 2), c._point)
self.assertEquals(((3, 4), (5, 6)), c._line)
def test_horizontal_constraint(self):
"""
Test horizontal constraint creation.
@ -70,6 +46,7 @@ class ItemConstraintTestCase(unittest.TestCase):
self.assertEquals(2, c.a)
self.assertEquals(4, c.b)
def test_vertical_constraint(self):
"""
Test vertical constraint creation.
@ -86,6 +63,7 @@ class ItemConstraintTestCase(unittest.TestCase):
self.assertEquals(1, c.a)
self.assertEquals(3, c.b)
def test_left_of_constraint(self):
"""
Test "less than" constraint (horizontal) creation.
@ -101,6 +79,7 @@ class ItemConstraintTestCase(unittest.TestCase):
self.assertEquals(1, c.smaller)
self.assertEquals(3, c.bigger)
def test_above_constraint(self):
"""
Test "less than" constraint (vertical) creation.

View File

@ -1,130 +1,111 @@
#!/usr/bin/env python
# Copyright (C) 2008-2017 Arjan Molenaar <gaphor@gmail.com>
# Dan Yeaw <dan@yeaw.me>
#
# This file is part of Gaphas.
#
# This library is free software; you can redistribute it and/or modify it under
# the terms of the GNU Library General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for
# more details.
#
# You should have received a copy of the GNU Library General Public License
# along with this library; if not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import
import unittest
from gaphas import state
from gaphas.canvas import Canvas
from gaphas.item import Line
from gaphas.segment import Segment
undo_list = []
redo_list = []
def undo_handler(event):
undo_list.append(event)
def undo():
apply_me = list(undo_list)
del undo_list[:]
apply_me.reverse()
for e in apply_me:
state.saveapply(*e)
redo_list[:] = undo_list[:]
del undo_list[:]
class TestCaseBase(unittest.TestCase):
"""
Abstract test case class with undo support.
"""
def setUp(self):
state.observers.add(state.revert_handler)
state.subscribers.add(undo_handler)
def tearDown(self):
state.observers.remove(state.revert_handler)
state.subscribers.remove(undo_handler)
class LineTestCase(TestCaseBase):
"""
Basic line item tests.
"""
def test_initial_ports(self):
"""Test initial ports amount
"""
line = Line()
self.assertEquals(1, len(line.ports()))
def test_orthogonal_horizontal_undo(self):
"""Test orthogonal line constraints bug (#107)
"""
canvas = Canvas()
line = Line()
canvas.add(line)
assert not line.horizontal
assert len(canvas.solver._constraints) == 0
segment = Segment(line, None)
segment.split_segment(0)
line.orthogonal = True
self.assertEquals(2, len(canvas.solver._constraints))
after_ortho = set(canvas.solver._constraints)
del undo_list[:]
line.horizontal = True
self.assertEquals(2, len(canvas.solver._constraints))
undo()
self.assertFalse(line.horizontal)
self.assertEquals(2, len(canvas.solver._constraints))
line.horizontal = True
self.assertTrue(line.horizontal)
self.assertEquals(2, len(canvas.solver._constraints))
def test_orthogonal_line_undo(self):
"""Test orthogonal line undo
"""
canvas = Canvas()
line = Line()
canvas.add(line)
segment = Segment(line, None)
segment.split_segment(0)
# start with no orthogonal constraints
assert len(canvas.solver._constraints) == 0
line.orthogonal = True
# check orthogonal constraints
self.assertEquals(2, len(canvas.solver._constraints))
self.assertEquals(3, len(line.handles()))
undo()
self.assertFalse(line.orthogonal)
self.assertEquals(0, len(canvas.solver._constraints))
self.assertEquals(2, len(line.handles()))
# vim:sw=4:et
import unittest
from gaphas.item import Line
from gaphas.canvas import Canvas
from gaphas import state
from gaphas.segment import Segment
undo_list = []
redo_list = []
def undo_handler(event):
undo_list.append(event)
def undo():
apply_me = list(undo_list)
del undo_list[:]
apply_me.reverse()
for e in apply_me:
state.saveapply(*e)
redo_list[:] = undo_list[:]
del undo_list[:]
class TestCaseBase(unittest.TestCase):
"""
Abstract test case class with undo support.
"""
def setUp(self):
state.observers.add(state.revert_handler)
state.subscribers.add(undo_handler)
def tearDown(self):
state.observers.remove(state.revert_handler)
state.subscribers.remove(undo_handler)
class LineTestCase(TestCaseBase):
"""
Basic line item tests.
"""
def test_initial_ports(self):
"""Test initial ports amount
"""
line = Line()
self.assertEquals(1, len(line.ports()))
def test_orthogonal_horizontal_undo(self):
"""Test orthogonal line constraints bug (#107)
"""
canvas = Canvas()
line = Line()
canvas.add(line)
assert not line.horizontal
assert len(canvas.solver._constraints) == 0
segment = Segment(line, None)
segment.split_segment(0)
line.orthogonal = True
self.assertEquals(2, len(canvas.solver._constraints))
after_ortho = set(canvas.solver._constraints)
del undo_list[:]
line.horizontal = True
self.assertEquals(2, len(canvas.solver._constraints))
undo()
self.assertFalse(line.horizontal)
self.assertEquals(2, len(canvas.solver._constraints))
line.horizontal = True
self.assertTrue(line.horizontal)
self.assertEquals(2, len(canvas.solver._constraints))
def test_orthogonal_line_undo(self):
"""Test orthogonal line undo
"""
canvas = Canvas()
line = Line()
canvas.add(line)
segment = Segment(line, None)
segment.split_segment(0)
# start with no orthogonal constraints
assert len(canvas.solver._constraints) == 0
line.orthogonal = True
# check orthogonal constraints
self.assertEquals(2, len(canvas.solver._constraints))
self.assertEquals(3, len(line.handles()))
undo()
self.assertFalse(line.orthogonal)
self.assertEquals(0, len(canvas.solver._constraints))
self.assertEquals(2, len(line.handles()))
# vim:sw=4:et

View File

@ -1,46 +1,23 @@
#!/usr/bin/env python
# Copyright (C) 2008-2017 Arjan Molenaar <gaphor@gmail.com>
# Dan Yeaw <dan@yeaw.me>
#
# This file is part of Gaphas.
#
# This library is free software; you can redistribute it and/or modify it under
# the terms of the GNU Library General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for
# more details.
#
# You should have received a copy of the GNU Library General Public License
# along with this library; if not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import
from __future__ import print_function
import pickle
import unittest
import pickle
from gaphas.canvas import Canvas
from gaphas.examples import Box
from gaphas.item import Element, Line
from gaphas.item import Item, Element, Line
from gaphas.view import View, GtkView
# Ensure extra pickle reducers/reconstructors are loaded:
import gaphas.picklers
class MyPickler(pickle.Pickler):
def save(self, obj):
print('saving obj', obj, type(obj))
print 'saving obj', obj, type(obj)
try:
return pickle.Pickler.save(self, obj)
except pickle.PicklingError as e:
print('Error while pickling', obj, self.dispatch.get(type(obj)))
except pickle.PicklingError, e:
print 'Error while pickling', obj, self.dispatch.get(type(obj))
raise e
@ -49,11 +26,9 @@ class my_disconnect(object):
Disconnect object should be located at top-level, so the pickle code
can find it.
"""
def __call__(self):
pass
def create_canvas():
canvas = Canvas()
box = Box()
@ -63,6 +38,7 @@ def create_canvas():
box2 = Box()
canvas.add(box2, parent=box)
line = Line()
line.handles()[0].visible = False
line.handles()[0].connected_to = box
@ -77,6 +53,7 @@ def create_canvas():
class PickleTestCase(unittest.TestCase):
def test_pickle_element(self):
item = Element()
@ -86,6 +63,7 @@ class PickleTestCase(unittest.TestCase):
assert i2
assert len(i2.handles()) == 4
def test_pickle_line(self):
item = Line()
@ -95,6 +73,7 @@ class PickleTestCase(unittest.TestCase):
assert i2
assert len(i2.handles()) == 2
def test_pickle(self):
canvas = create_canvas()
@ -105,6 +84,7 @@ class PickleTestCase(unittest.TestCase):
assert type(canvas._tree.nodes[1]) is Box
assert type(canvas._tree.nodes[2]) is Line
def test_pickle_connect(self):
"""
Persist a connection.
@ -115,6 +95,7 @@ class PickleTestCase(unittest.TestCase):
box2 = Box()
canvas.add(box2, parent=box)
line = Line()
line.handles()[0].visible = False
line.handles()[0].connected_to = box
@ -142,6 +123,7 @@ class PickleTestCase(unittest.TestCase):
assert callable(h.disconnect)
assert h.disconnect() is None, h.disconnect()
def test_pickle_with_view(self):
canvas = create_canvas()
@ -159,6 +141,7 @@ class PickleTestCase(unittest.TestCase):
surface.flush()
surface.finish()
def test_pickle_with_gtk_view(self):
canvas = create_canvas()
@ -166,8 +149,8 @@ class PickleTestCase(unittest.TestCase):
c2 = pickle.loads(pickled)
from gi.repository import Gtk
win = Gtk.Window()
import gtk
win = gtk.Window()
view = GtkView(canvas=c2)
win.add(view)
@ -185,24 +168,24 @@ class PickleTestCase(unittest.TestCase):
view = GtkView(canvas=canvas)
# from gaphas.tool import ConnectHandleTool
# handle_tool = ConnectHandleTool()
# handle_tool.connect(view, line, line.handles()[0], (40, 0))
# assert line.handles()[0].connected_to is box, line.handles()[0].connected_to
# assert line.handles()[0].connection_data
# assert line.handles()[0].disconnect
# assert isinstance(line.handles()[0].disconnect, object), line.handles()[0].disconnect
# from gaphas.tool import ConnectHandleTool
# handle_tool = ConnectHandleTool()
# handle_tool.connect(view, line, line.handles()[0], (40, 0))
# assert line.handles()[0].connected_to is box, line.handles()[0].connected_to
# assert line.handles()[0].connection_data
# assert line.handles()[0].disconnect
# assert isinstance(line.handles()[0].disconnect, object), line.handles()[0].disconnect
import io
f = io.BytesIO()
import StringIO
f = StringIO.StringIO()
pickler = MyPickler(f)
pickler.dump(canvas)
pickled = f.getvalue()
c2 = pickle.loads(pickled)
from gi.repository import Gtk
win = Gtk.Window()
import gtk
win = gtk.Window()
view = GtkView(canvas=c2)
win.add(view)
view.show()
@ -219,8 +202,8 @@ class PickleTestCase(unittest.TestCase):
c2 = pickle.loads(pickled)
from gi.repository import Gtk
win = Gtk.Window()
import gtk
win = gtk.Window()
view = GtkView(canvas=c2)
win.add(view)
@ -229,7 +212,6 @@ class PickleTestCase(unittest.TestCase):
view.update()
if __name__ == '__main__':
unittest.main()

View File

@ -1,34 +1,9 @@
#!/usr/bin/env python
# Copyright (C) 2007-2017 Arjan Molenaar <gaphor@gmail.com>
# Dan Yeaw <dan@yeaw.me>
#
# This file is part of Gaphas.
#
# This library is free software; you can redistribute it and/or modify it under
# the terms of the GNU Library General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for
# more details.
#
# You should have received a copy of the GNU Library General Public License
# along with this library; if not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import
import unittest
import six
from six.moves import range
from gaphas.quadtree import Quadtree
class QuadtreeTestCase(unittest.TestCase):
def test_lookups(self):
qtree = Quadtree((0, 0, 100, 100))
for i in range(100, 10):
@ -37,8 +12,8 @@ class QuadtreeTestCase(unittest.TestCase):
for i in range(100, 10):
for j in range(100, 10):
assert qtree.find_intersect((i + 1, j + 1, 1, 1)) == ['%dx%d' % (i, j)], \
qtree.find_intersect((i + 1, j + 1, 1, 1))
assert qtree.find_intersect((i+1, j+1, 1, 1)) == ['%dx%d' % (i, j)], \
qtree.find_intersect((i+1, j+1, 1, 1))
def test_with_rectangles(self):
from gaphas.geometry import Rectangle
@ -51,8 +26,9 @@ class QuadtreeTestCase(unittest.TestCase):
for i in range(100, 10):
for j in range(100, 10):
assert qtree.find_intersect((i + 1, j + 1, 1, 1)) == ['%dx%d' % (i, j)], \
qtree.find_intersect((i + 1, j + 1, 1, 1))
assert qtree.find_intersect((i+1, j+1, 1, 1)) == ['%dx%d' % (i, j)], \
qtree.find_intersect((i+1, j+1, 1, 1))
def test_moving_items(self):
qtree = Quadtree((0, 0, 100, 100), capacity=10)
@ -72,7 +48,7 @@ class QuadtreeTestCase(unittest.TestCase):
assert len(qtree._bucket.items) == 0, qtree._bucket.items
for i in range(4):
assert len(qtree._bucket._buckets[i].items) == 9
for item, bounds in six.iteritems(qtree._bucket._buckets[i].items):
for item, bounds in qtree._bucket._buckets[i].items.iteritems():
assert qtree._bucket.find_bucket(bounds) is qtree._bucket._buckets[i]
for j in range(4):
assert len(qtree._bucket._buckets[i]._buckets[j].items) == 4
@ -82,19 +58,20 @@ class QuadtreeTestCase(unittest.TestCase):
qtree.add('0x0', (20, 20, 10, 10))
assert len(qtree._bucket.items) == 0
assert len(qtree._bucket._buckets[0]._buckets[0].items) == 3, \
qtree._bucket._buckets[0]._buckets[0].items
qtree._bucket._buckets[0]._buckets[0].items
assert len(qtree._bucket._buckets[0].items) == 10, \
qtree._bucket._buckets[0].items
qtree._bucket._buckets[0].items
# Now move item '0x0' to the second quadrant (70, 20)
qtree.add('0x0', (70, 20, 10, 10))
assert len(qtree._bucket.items) == 0
assert len(qtree._bucket._buckets[0]._buckets[0].items) == 3, \
qtree._bucket._buckets[0]._buckets[0].items
qtree._bucket._buckets[0]._buckets[0].items
assert len(qtree._bucket._buckets[0].items) == 9, \
qtree._bucket._buckets[0].items
qtree._bucket._buckets[0].items
assert len(qtree._bucket._buckets[1].items) == 10, \
qtree._bucket._buckets[1].items
qtree._bucket._buckets[1].items
def test_get_data(self):
"""
@ -103,11 +80,11 @@ class QuadtreeTestCase(unittest.TestCase):
qtree = Quadtree((0, 0, 100, 100))
for i in range(0, 100, 10):
for j in range(0, 100, 10):
qtree.add("%dx%d" % (i, j), (i, j, 10, 10), i + j)
qtree.add("%dx%d" % (i, j), (i, j, 10, 10), i+j)
for i in range(0, 100, 10):
for j in range(0, 100, 10):
assert i + j == qtree.get_data("%dx%d" % (i, j))
assert i+j == qtree.get_data("%dx%d" % (i, j))
def test_clipped_bounds(self):
qtree = Quadtree((0, 0, 100, 100), capacity=10)

View File

@ -1,38 +1,14 @@
#!/usr/bin/env python
# Copyright (C) 2009-2017 Arjan Molenaar <gaphor@gmail.com>
# Dan Yeaw <dan@yeaw.me>
#
# This file is part of Gaphas.
#
# This library is free software; you can redistribute it and/or modify it under
# the terms of the GNU Library General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for
# more details.
#
# You should have received a copy of the GNU Library General Public License
# along with this library; if not, see <http://www.gnu.org/licenses/>.
"""
Generic gaphas item tests.
"""
from __future__ import absolute_import
from __future__ import print_function
import unittest
from gaphas import state
from gaphas.canvas import Canvas
from gaphas.item import Item
from gaphas.item import Item, Line
from gaphas.segment import *
from gaphas.tests.test_tool import simple_canvas
from gaphas.canvas import Canvas
from gaphas.view import View
from gaphas import state
class SegmentTestCase(unittest.TestCase):
@ -51,9 +27,9 @@ class SegmentTestCase(unittest.TestCase):
item = Item()
try:
s = Segment(item, self.view)
print(item, 'segment aspect:', s)
except TypeError as e:
print('TypeError', e)
print item, 'segment aspect:', s
except TypeError, e:
print 'TypeError', e
else:
assert False, 'Should not be reached'
@ -69,6 +45,9 @@ class SegmentTestCase(unittest.TestCase):
self.assertEquals(3, len(line.handles()))
from gaphas.tests.test_tool import simple_canvas
undo_list = []
redo_list = []
@ -91,7 +70,6 @@ class TestCaseBase(unittest.TestCase):
"""
Abstract test case class with undo support.
"""
def setUp(self):
state.observers.add(state.revert_handler)
state.subscribers.add(undo_handler)
@ -102,11 +80,12 @@ class TestCaseBase(unittest.TestCase):
state.subscribers.remove(undo_handler)
class LineSplitTestCase(TestCaseBase):
"""
Tests for line splitting.
"""
def test_split_single(self):
"""Test single line splitting
"""
@ -142,6 +121,7 @@ class LineSplitTestCase(TestCaseBase):
self.assertEquals(h2.pos, p2.start)
self.assertEquals(h3.pos, p2.end)
def test_split_multiple(self):
"""Test multiple line splitting
"""
@ -186,6 +166,7 @@ class LineSplitTestCase(TestCaseBase):
self.assertEquals(h4.pos, p4.start)
self.assertEquals(h5.pos, p4.end)
def test_ports_after_split(self):
"""Test ports removal after split
"""
@ -207,6 +188,7 @@ class LineSplitTestCase(TestCaseBase):
self.assertFalse(old_ports[0] in self.line.ports())
self.assertEquals(old_ports[1], self.line.ports()[2])
def test_constraints_after_split(self):
"""Test if constraints are recreated after line split
"""
@ -228,6 +210,7 @@ class LineSplitTestCase(TestCaseBase):
self.assertEquals(h1.pos, cinfo.constraint._line[0]._point)
self.assertEquals(h2.pos, cinfo.constraint._line[1]._point)
def test_split_undo(self):
"""Test line splitting undo
"""
@ -248,6 +231,7 @@ class LineSplitTestCase(TestCaseBase):
self.assertEquals(2, len(self.line.handles()))
self.assertEquals(1, len(self.line.ports()))
def test_orthogonal_line_split(self):
"""Test orthogonal line splitting
"""
@ -271,6 +255,7 @@ class LineSplitTestCase(TestCaseBase):
self.assertEquals(4, len(self.line.handles()))
self.assertEquals(3, len(self.line.ports()))
def test_params_errors(self):
"""Test parameter error exceptions
"""
@ -290,6 +275,7 @@ class LineSplitTestCase(TestCaseBase):
self.assertRaises(ValueError, segment.split_segment, 0, 1)
class LineMergeTestCase(TestCaseBase):
"""
Tests for line merging.
@ -330,6 +316,7 @@ class LineMergeTestCase(TestCaseBase):
self.assertEquals((0, 0), port.start.pos)
self.assertEquals((20, 0), port.end.pos)
def test_constraints_after_merge(self):
"""Test if constraints are recreated after line merge
"""
@ -339,11 +326,11 @@ class LineMergeTestCase(TestCaseBase):
self.canvas.add(line2)
head = line2.handles()[0]
# conn = Connector(line2, head)
# sink = conn.glue((25, 25))
# assert sink is not None
#conn = Connector(line2, head)
#sink = conn.glue((25, 25))
#assert sink is not None
# conn.connect(sink)
#conn.connect(sink)
self.tool.connect(line2, head, (25, 25))
cinfo = self.canvas.get_connection(head)
@ -364,6 +351,7 @@ class LineMergeTestCase(TestCaseBase):
self.assertEquals(cinfo.constraint._line[1]._point, h2.pos)
self.assertFalse(c1 == cinfo.constraint)
def test_merge_multiple(self):
"""Test multiple line merge
"""
@ -375,7 +363,7 @@ class LineMergeTestCase(TestCaseBase):
assert len(self.line.handles()) == 4
assert len(self.line.ports()) == 3
print(self.line.handles())
print self.line.handles()
handles, ports = segment.merge_segment(0, count=3)
self.assertEquals(2, len(handles))
self.assertEquals(3, len(ports))
@ -390,6 +378,7 @@ class LineMergeTestCase(TestCaseBase):
self.assertEquals((0, 0), port.start.pos)
self.assertEquals((20, 16), port.end.pos)
def test_merge_undo(self):
"""Test line merging undo
"""
@ -415,6 +404,7 @@ class LineMergeTestCase(TestCaseBase):
self.assertEquals(3, len(self.line.handles()))
self.assertEquals(2, len(self.line.ports()))
def test_orthogonal_line_merge(self):
"""Test orthogonal line merging
"""
@ -439,6 +429,7 @@ class LineMergeTestCase(TestCaseBase):
self.assertEquals(3, len(self.line.handles()))
self.assertEquals(2, len(self.line.ports()))
def test_params_errors(self):
"""Test parameter error exceptions
"""
@ -488,14 +479,17 @@ class LineMergeTestCase(TestCaseBase):
class SegmentHandlesTest(unittest.TestCase):
def setUp(self):
self.canvas = Canvas()
self.line = Line()
self.canvas.add(self.line)
self.view = View(self.canvas)
def testHandleFinder(self):
finder = HandleFinder(self.line, self.view)
assert type(finder) is SegmentHandleFinder, type(finder)
# vim:sw=4:et:ai

View File

@ -1,37 +1,14 @@
#!/usr/bin/env python
# Copyright (C) 2007-2017 Arjan Molenaar <gaphor@gmail.com>
# Artur Wroblewski <wrobell@pld-linux.org>
# Dan Yeaw <dan@yeaw.me>
#
# This file is part of Gaphas.
#
# This library is free software; you can redistribute it and/or modify it under
# the terms of the GNU Library General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for
# more details.
#
# You should have received a copy of the GNU Library General Public License
# along with this library; if not, see <http://www.gnu.org/licenses/>.
"""
Unit tests for Gaphas' solver.
"""
from __future__ import absolute_import
from __future__ import print_function
import unittest
from timeit import Timer
from gaphas.solver import Solver, Variable
from gaphas.constraint import EquationConstraint, EqualsConstraint, \
LessThanConstraint
from gaphas.solver import Solver, Variable
SETUP = """
from gaphas.solver import Solver, Variable
@ -46,12 +23,10 @@ solver.add_constraint(c_eq)
REPEAT = 30
NUMBER = 1000
class WeakestVariableTestCase(unittest.TestCase):
"""
Test weakest variable calculation.
"""
def test_weakest_list(self):
"""Test weakest list"""
solver = Solver()
@ -66,6 +41,7 @@ class WeakestVariableTestCase(unittest.TestCase):
self.assertTrue(b in c_eq._weakest)
self.assertTrue(c in c_eq._weakest)
def test_weakest_list_order(self):
"""Test weakest list order"""
solver = Solver()
@ -79,7 +55,7 @@ class WeakestVariableTestCase(unittest.TestCase):
weakest = [el for el in c_eq._weakest]
a.value = 4
self.assertEqual(c_eq._weakest, weakest) # does not change if non-weak variable changed
self.assertEqual(c_eq._weakest, weakest) # does not change if non-weak variable changed
b.value = 5
self.assertEqual(c_eq.weakest(), c)
@ -95,6 +71,7 @@ class WeakestVariableTestCase(unittest.TestCase):
b.value = 6
self.assertEqual(c_eq.weakest(), c)
def test_strength_change(self):
"""Test strength change"""
solver = Solver()
@ -109,11 +86,11 @@ class WeakestVariableTestCase(unittest.TestCase):
self.assertEqual(c_eq._weakest, [b])
class SizeTestCase(unittest.TestCase):
"""
Test size related constraints, i.e. minimal size.
"""
def test_min_size(self):
"""Test minimal size constraint"""
solver = Solver()
@ -159,11 +136,11 @@ class SizeTestCase(unittest.TestCase):
self.assertEquals(10, v3)
class SolverSpeedTestCase(unittest.TestCase):
"""
Solver speed tests.
"""
def _test_speed_run_weakest(self):
"""
Speed test for weakest variable.
@ -175,7 +152,7 @@ c_eq.weakest()""").repeat(repeat=REPEAT, number=NUMBER)
# Print the average of the best 10 runs:
results.sort()
print('[Avg: %gms]' % ((sum(results[:10]) / 10) * 1000))
print '[Avg: %gms]' % ((sum(results[:10]) / 10) * 1000)
if __name__ == '__main__':

View File

@ -1,54 +1,24 @@
#!/usr/bin/env python
# Copyright (C) 2009-2017 Artur Wroblewski <wrobell@pld-linux.org>
# Dan Yeaw <dan@yeaw.me>
#
# This file is part of Gaphas.
#
# This library is free software; you can redistribute it and/or modify it under
# the terms of the GNU Library General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for
# more details.
#
# You should have received a copy of the GNU Library General Public License
# along with this library; if not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import
import unittest
from gaphas.state import reversible_pair, observed, _reverse
class SList(object):
def __init__(self):
self.l = list()
def add(self, node, before=None):
if before:
self.l.insert(self.l.index(before), node)
else:
self.l.append(node)
if before: self.l.insert(self.l.index(before), node)
else: self.l.append(node)
add = observed(add)
@observed
def remove(self, node):
self.l.remove(self.l.index(node))
class StateTestCase(unittest.TestCase):
def test_adding_pair(self):
"""Test adding reversible pair
"""
reversible_pair(SList.add, SList.remove,
bind1={'before': lambda self, node: self.l[self.l.index(node) + 1]}
)
reversible_pair(SList.add, SList.remove, \
bind1={'before': lambda self, node: self.l[self.l.index(node)+1] })
self.assertTrue(SList.add in _reverse)
self.assertTrue(SList.remove in _reverse)
self.assertTrue(SList.add.im_func in _reverse)
self.assertTrue(SList.remove.im_func in _reverse)

View File

@ -1,39 +1,20 @@
#!/usr/bin/env python
# Copyright (C) 2008-2017 Arjan Molenaar <gaphor@gmail.com>
# Artur Wroblewski <wrobell@pld-linux.org>
# Dan Yeaw <dan@yeaw.me>
#
# This file is part of Gaphas.
#
# This library is free software; you can redistribute it and/or modify it under
# the terms of the GNU Library General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for
# more details.
#
# You should have received a copy of the GNU Library General Public License
# along with this library; if not, see <http://www.gnu.org/licenses/>.
"""
Test all the tools provided by gaphas.
"""
from __future__ import absolute_import
import unittest
from gaphas.canvas import Canvas
from gaphas.canvas import Context
from gaphas.constraint import LineConstraint
from gaphas.examples import Box
from gaphas.item import Line
from gaphas.tool import ConnectHandleTool
from gaphas.view import GtkView
from gaphas.canvas import Canvas
from gaphas.examples import Box
from gaphas.item import Item, Element, Line
from gaphas.view import View, GtkView
from gaphas.constraint import LineConstraint
from gaphas.canvas import Context
from gaphas import state
from gaphas.aspect import Connector, ConnectionSink
Event = Context
@ -68,8 +49,8 @@ def simple_canvas(self):
self.canvas.update_now()
self.view = GtkView()
self.view.canvas = self.canvas
from gi.repository import Gtk
win = Gtk.Window()
import gtk
win = gtk.Window()
win.add(self.view)
self.view.show()
self.view.update()
@ -78,6 +59,7 @@ def simple_canvas(self):
self.tool = ConnectHandleTool(self.view)
class ConnectHandleToolGlueTestCase(unittest.TestCase):
"""
Test handle connection tool glue method.
@ -86,6 +68,7 @@ class ConnectHandleToolGlueTestCase(unittest.TestCase):
def setUp(self):
simple_canvas(self)
def test_item_and_port_glue(self):
"""Test glue operation to an item and its ports"""
@ -111,47 +94,49 @@ class ConnectHandleToolGlueTestCase(unittest.TestCase):
self.assertEquals(sink.item, self.box1)
self.assertEquals(ports[3], sink.port)
def test_failed_glue(self):
"""Test glue from too far distance"""
sink = self.tool.glue(self.line, self.head, (90, 50))
self.assertTrue(sink is None)
# def test_glue_call_can_glue_once(self):
# """Test if glue method calls can glue once only
#
# Box has 4 ports. Every port is examined once per
# ConnectHandleTool.glue method call. The purpose of this test is to
# assure that ConnectHandleTool.can_glue is called once (for the
# found port), it cannot be called four times (once for every port).
# """
#
# # count ConnectHandleTool.can_glue calls
# class Tool(ConnectHandleTool):
# def __init__(self, *args):
# super(Tool, self).__init__(*args)
# self._calls = 0
#
# def can_glue(self, *args):
# self._calls += 1
# return True
#
# tool = Tool(self.view)
# item, port = tool.glue(self.line, self.head, (120, 50))
# assert item and port
# self.assertEquals(1, tool._calls)
# def test_glue_call_can_glue_once(self):
# """Test if glue method calls can glue once only
#
# Box has 4 ports. Every port is examined once per
# ConnectHandleTool.glue method call. The purpose of this test is to
# assure that ConnectHandleTool.can_glue is called once (for the
# found port), it cannot be called four times (once for every port).
# """
#
# # count ConnectHandleTool.can_glue calls
# class Tool(ConnectHandleTool):
# def __init__(self, *args):
# super(Tool, self).__init__(*args)
# self._calls = 0
#
# def can_glue(self, *args):
# self._calls += 1
# return True
#
# tool = Tool(self.view)
# item, port = tool.glue(self.line, self.head, (120, 50))
# assert item and port
# self.assertEquals(1, tool._calls)
# def test_glue_cannot_glue(self):
# """Test if glue method respects ConnectHandleTool.can_glue method"""
#
# class Tool(ConnectHandleTool):
# def can_glue(self, *args):
# return False
#
# tool = Tool(self.view)
# item, port = tool.glue(self.line, self.head, (120, 50))
# self.assertTrue(item is None, item)
# self.assertTrue(port is None, port)
# def test_glue_cannot_glue(self):
# """Test if glue method respects ConnectHandleTool.can_glue method"""
#
# class Tool(ConnectHandleTool):
# def can_glue(self, *args):
# return False
#
# tool = Tool(self.view)
# item, port = tool.glue(self.line, self.head, (120, 50))
# self.assertTrue(item is None, item)
# self.assertTrue(port is None, port)
def test_glue_no_port_no_can_glue(self):
@ -172,16 +157,20 @@ class ConnectHandleToolGlueTestCase(unittest.TestCase):
self.assertEquals(0, tool._calls)
class ConnectHandleToolConnectTestCase(unittest.TestCase):
def setUp(self):
simple_canvas(self)
def _get_line(self):
line = Line()
head = line.handles()[0]
self.canvas.add(line)
return line, head
def test_connect(self):
"""Test connection to an item"""
line, head = self._get_line()
@ -190,7 +179,7 @@ class ConnectHandleToolConnectTestCase(unittest.TestCase):
self.assertTrue(cinfo is not None)
self.assertEquals(self.box1, cinfo.connected)
self.assertTrue(cinfo.port is self.box1.ports()[0],
'port %s' % cinfo.port)
'port %s' % cinfo.port)
self.assertTrue(isinstance(cinfo.constraint, LineConstraint))
# No default callback defined:
self.assertTrue(cinfo.callback is None)
@ -201,6 +190,7 @@ class ConnectHandleToolConnectTestCase(unittest.TestCase):
self.assertTrue(cinfo is not cinfo2, cinfo2)
self.assertTrue(cinfo2 is None, cinfo2)
def test_reconnect_another(self):
"""Test reconnection to another item"""
line, head = self._get_line()
@ -227,6 +217,7 @@ class ConnectHandleToolConnectTestCase(unittest.TestCase):
self.assertNotEqual(item, cinfo.connected)
self.assertNotEqual(constraint, cinfo.constraint)
def test_reconnect_same(self):
"""Test reconnection to same item"""
line, head = self._get_line()
@ -249,6 +240,7 @@ class ConnectHandleToolConnectTestCase(unittest.TestCase):
self.assertEqual(self.box1.ports()[0], cinfo.port)
self.assertNotEqual(constraint, cinfo.constraint)
def xtest_find_port(self):
"""Test finding a port
"""
@ -271,4 +263,5 @@ class ConnectHandleToolConnectTestCase(unittest.TestCase):
port = self.tool.find_port(line, head, self.box1)
self.assertEquals(p4, port)
# vim: sw=4:et:ai

View File

@ -1,31 +1,10 @@
#!/usr/bin/env python
# Copyright (C) 2007-2017 Arjan Molenaar <gaphor@gmail.com>
# Dan Yeaw <dan@yeaw.me>
#
# This file is part of Gaphas.
#
# This library is free software; you can redistribute it and/or modify it under
# the terms of the GNU Library General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for
# more details.
#
# You should have received a copy of the GNU Library General Public License
# along with this library; if not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import
import unittest
from gaphas.tree import Tree
class TreeTestCase(unittest.TestCase):
def test_add(self):
"""
Test creating node trees.
@ -95,6 +74,7 @@ class TreeTestCase(unittest.TestCase):
assert tree.nodes == [n1, n3, n5, n4, n2], tree.nodes
assert tree.get_children(n3) == [n5, n4], tree.get_children(n3)
def test_remove(self):
"""
Test removal of nodes.
@ -115,7 +95,7 @@ class TreeTestCase(unittest.TestCase):
assert tree._nodes == [n1, n3, n4, n5, n2]
all_ch = list(tree.get_all_children(n1))
assert all_ch == [n3, n4, n5], all_ch
assert all_ch == [ n3, n4, n5 ], all_ch
tree.remove(n4)
assert tree._nodes == [n1, n3, n2]
@ -141,18 +121,18 @@ class TreeTestCase(unittest.TestCase):
try:
tree.get_next_sibling(n3)
except IndexError:
pass # okay
pass # okay
else:
raise AssertionError('Index should be out of range, not %s' % tree.get_next_sibling(n3))
raise AssertionError, 'Index should be out of range, not %s' % tree.get_next_sibling(n3)
assert tree.get_previous_sibling(n3) is n2
assert tree.get_previous_sibling(n2) is n1
try:
tree.get_previous_sibling(n1)
except IndexError:
pass # okay
pass # okay
else:
raise AssertionError('Index should be out of range, not %s' % tree.get_previous_sibling(n1))
raise AssertionError, 'Index should be out of range, not %s' % tree.get_previous_sibling(n1)
def test_reparent(self):
tree = Tree()
@ -179,4 +159,5 @@ class TreeTestCase(unittest.TestCase):
tree.reparent(n4, parent=None, index=0)
assert tree.nodes == [n4, n5, n1, n2, n3], tree.nodes
# vi:sw=4:et:ai

View File

@ -1,32 +1,6 @@
#!/usr/bin/env python
# Copyright (C) 2010-2017 Arjan Molenaar <gaphor@gmail.com>
# Dan Yeaw <dan@yeaw.me>
#
# This file is part of Gaphas.
#
# This library is free software; you can redistribute it and/or modify it under
# the terms of the GNU Library General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for
# more details.
#
# You should have received a copy of the GNU Library General Public License
# along with this library; if not, see <http://www.gnu.org/licenses/>.
from __future__ import absolute_import
import unittest
from gaphas import state
from gaphas.aspect import Connector, ConnectionSink
from gaphas.canvas import Canvas
from gaphas.examples import Box
from gaphas.item import Line
state.observers.clear()
state.subscribers.clear()
@ -34,11 +8,9 @@ state.subscribers.clear()
undo_list = []
redo_list = []
def undo_handler(event):
undo_list.append(event)
def undo():
apply_me = list(undo_list)
del undo_list[:]
@ -49,7 +21,14 @@ def undo():
del undo_list[:]
from gaphas.canvas import Canvas
from gaphas.examples import Box
from gaphas.item import Line
from gaphas.aspect import Connector, ConnectionSink
class UndoTestCase(unittest.TestCase):
def setUp(self):
state.observers.add(state.revert_handler)
state.subscribers.add(undo_handler)
@ -112,8 +91,7 @@ class UndoTestCase(unittest.TestCase):
cinfo = canvas.get_connection(line.handles()[-1])
self.assertEquals(b2, cinfo.connected)
# self.assertEquals(list(canvas.solver.constraints_with_variable(line.handles()[-1].pos.x)))
# self.assertEquals(list(canvas.solver.constraints_with_variable(line.handles()[-1].pos.x)))
# self.assertTrue(list(canvas.solver.constraints_with_variable(line.handles()[-1].pos.y)))
if __name__ == '__main__':

View File

@ -1,42 +1,19 @@
#!/usr/bin/env python
# Copyright (C) 2007-2017 Arjan Molenaar <gaphor@gmail.com>
# Dan Yeaw <dan@yeaw.me>
#
# This file is part of Gaphas.
#
# This library is free software; you can redistribute it and/or modify it under
# the terms of the GNU Library General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for
# more details.
#
# You should have received a copy of the GNU Library General Public License
# along with this library; if not, see <http://www.gnu.org/licenses/>.
"""
Test cases for the View class.
"""
from __future__ import absolute_import
from __future__ import print_function
import math
import unittest
from gi.repository import Gtk
from gaphas.canvas import Canvas
from gaphas.examples import Box
from gaphas.item import Line
import gtk
import math
from gaphas.view import View, GtkView
from gaphas.canvas import Canvas, Context
from gaphas.item import Line
from gaphas.examples import Box
from gaphas.tool import HoverTool
class ViewTestCase(unittest.TestCase):
def test_bounding_box_calculations(self):
"""
A view created before and after the canvas is populated should contain
@ -44,41 +21,37 @@ class ViewTestCase(unittest.TestCase):
"""
canvas = Canvas()
window1 = Gtk.Window(Gtk.WindowType.TOPLEVEL)
window1 = gtk.Window(gtk.WINDOW_TOPLEVEL)
view1 = GtkView(canvas=canvas)
window1.add(view1)
view1.realize()
window1.show_all()
box = Box()
box.matrix = (1.0, 0.0, 0.0, 1, 10, 10)
box.matrix = (1.0, 0.0, 0.0, 1, 10,10)
canvas.add(box)
line = Line()
line.fyzzyness = 1
line.handles()[1].pos = (30, 30)
# line.split_segment(0, 3)
#line.split_segment(0, 3)
line.matrix.translate(30, 60)
canvas.add(line)
window2 = Gtk.Window(Gtk.WindowType.TOPLEVEL)
window2 = gtk.Window(gtk.WINDOW_TOPLEVEL)
view2 = GtkView(canvas=canvas)
window2.add(view2)
window2.show_all()
# Process pending (expose) events, which cause the canvas to be drawn.
while Gtk.events_pending():
Gtk.main_iteration()
while gtk.events_pending():
gtk.main_iteration()
try:
assert view2.get_item_bounding_box(box)
assert view1.get_item_bounding_box(box)
assert view1.get_item_bounding_box(box) == view2.get_item_bounding_box(box), '%s != %s' % (
view1.get_item_bounding_box(box), view2.get_item_bounding_box(box)
)
assert view1.get_item_bounding_box(line) == view2.get_item_bounding_box(line), '%s != %s' % (
view1.get_item_bounding_box(line), view2.get_item_bounding_box(line)
)
assert view1.get_item_bounding_box(box) == view2.get_item_bounding_box(box), '%s != %s' % (view1.get_item_bounding_box(box), view2.get_item_bounding_box(box))
assert view1.get_item_bounding_box(line) == view2.get_item_bounding_box(line), '%s != %s' % (view1.get_item_bounding_box(line), view2.get_item_bounding_box(line))
finally:
window1.destroy()
window2.destroy()
@ -89,7 +62,7 @@ class ViewTestCase(unittest.TestCase):
"""
canvas = Canvas()
view = GtkView(canvas)
window = Gtk.Window(Gtk.WindowType.TOPLEVEL)
window = gtk.Window(gtk.WINDOW_TOPLEVEL)
window.add(view)
window.show_all()
@ -101,8 +74,8 @@ class ViewTestCase(unittest.TestCase):
box.height = 50
# Process pending (expose) events, which cause the canvas to be drawn.
while Gtk.events_pending():
Gtk.main_iteration()
while gtk.events_pending():
gtk.main_iteration()
assert len(view._qtree._ids) == 1
assert not view._qtree._bucket.bounds == (0, 0, 0, 0), view._qtree._bucket.bounds
@ -115,7 +88,7 @@ class ViewTestCase(unittest.TestCase):
def test_get_handle_at_point(self):
canvas = Canvas()
view = GtkView(canvas)
window = Gtk.Window(Gtk.WindowType.TOPLEVEL)
window = gtk.Window(gtk.WINDOW_TOPLEVEL)
window.add(view)
window.show_all()
@ -123,7 +96,7 @@ class ViewTestCase(unittest.TestCase):
box.min_width = 20
box.min_height = 30
box.matrix.translate(20, 20)
box.matrix.rotate(math.pi / 1.5)
box.matrix.rotate(math.pi/1.5)
canvas.add(box)
i, h = view.get_handle_at_point((20, 20))
@ -133,7 +106,7 @@ class ViewTestCase(unittest.TestCase):
def test_get_handle_at_point_at_pi_div_2(self):
canvas = Canvas()
view = GtkView(canvas)
window = Gtk.Window(Gtk.WindowType.TOPLEVEL)
window = gtk.Window(gtk.WINDOW_TOPLEVEL)
window.add(view)
window.show_all()
@ -141,7 +114,7 @@ class ViewTestCase(unittest.TestCase):
box.min_width = 20
box.min_height = 30
box.matrix.translate(20, 20)
box.matrix.rotate(math.pi / 2)
box.matrix.rotate(math.pi/2)
canvas.add(box)
p = canvas.get_matrix_i2c(box).transform_point(0, 20)
@ -153,7 +126,7 @@ class ViewTestCase(unittest.TestCase):
def test_item_removal(self):
canvas = Canvas()
view = GtkView(canvas)
window = Gtk.Window(Gtk.WindowType.TOPLEVEL)
window = gtk.Window(gtk.WINDOW_TOPLEVEL)
window.add(view)
window.show_all()
@ -163,8 +136,8 @@ class ViewTestCase(unittest.TestCase):
assert not canvas.require_update()
# Process pending (expose) events, which cause the canvas to be drawn.
while Gtk.events_pending():
Gtk.main_iteration()
while gtk.events_pending():
gtk.main_iteration()
assert len(canvas.get_all_items()) == len(view._qtree)
@ -188,8 +161,8 @@ class ViewTestCase(unittest.TestCase):
canvas.add(box)
# By default no complex updating/calculations are done:
assert view not in box._matrix_i2v
assert view not in box._matrix_v2i
assert not box._matrix_i2v.has_key(view)
assert not box._matrix_v2i.has_key(view)
# GTK view does register for updates though
@ -197,28 +170,29 @@ class ViewTestCase(unittest.TestCase):
assert len(canvas._registered_views) == 1
# No entry, since GtkView is not realized and has no window
assert view not in box._matrix_i2v
assert view not in box._matrix_v2i
assert not box._matrix_i2v.has_key(view)
assert not box._matrix_v2i.has_key(view)
window = Gtk.Window(Gtk.WindowType.TOPLEVEL)
window = gtk.Window(gtk.WINDOW_TOPLEVEL)
window.add(view)
window.show_all()
# Now everything is realized and updated
assert view in box._matrix_i2v
assert view in box._matrix_v2i
assert box._matrix_i2v.has_key(view)
assert box._matrix_v2i.has_key(view)
view.canvas = None
assert len(canvas._registered_views) == 0
assert view not in box._matrix_i2v
assert view not in box._matrix_v2i
assert not box._matrix_i2v.has_key(view)
assert not box._matrix_v2i.has_key(view)
view.canvas = canvas
assert len(canvas._registered_views) == 1
assert view in box._matrix_i2v
assert view in box._matrix_v2i
assert box._matrix_i2v.has_key(view)
assert box._matrix_v2i.has_key(view)
def test_view_registration_2(self):
"""
@ -226,7 +200,7 @@ class ViewTestCase(unittest.TestCase):
"""
canvas = Canvas()
view = GtkView(canvas)
window = Gtk.Window(Gtk.WindowType.TOPLEVEL)
window = gtk.Window(gtk.WINDOW_TOPLEVEL)
window.add(view)
window.show_all()
@ -246,15 +220,28 @@ class ViewTestCase(unittest.TestCase):
assert len(canvas._registered_views) == 0
assert view not in box._matrix_i2v
assert view not in box._matrix_v2i
assert not box._matrix_i2v.has_key(view)
assert not box._matrix_v2i.has_key(view)
def test_scroll_adjustments_signal(self):
def handler(self, hadj, vadj):
self.handled = True
sc = gtk.ScrolledWindow()
view = GtkView(Canvas())
view.connect('set-scroll-adjustments', handler)
sc.add(view)
assert view.handled
def test_scroll_adjustments(self):
sc = Gtk.ScrolledWindow()
sc = gtk.ScrolledWindow()
view = GtkView(Canvas())
sc.add(view)
print(sc.get_hadjustment(), view.hadjustment)
print sc.get_hadjustment(), view.hadjustment
assert sc.get_hadjustment() is view.hadjustment
assert sc.get_vadjustment() is view.vadjustment

View File

@ -1,25 +1,3 @@
#!/usr/bin/env python
# Copyright (C) 2006-2017 Adrian Boguszewski <adrbogus1@student.pg.gda.pl>
# Arjan Molenaar <gaphor@gmail.com>
# Artur Wroblewski <wrobell@pld-linux.org>
# Dan Yeaw <dan@yeaw.me>
#
# This file is part of Gaphas.
#
# This library is free software; you can redistribute it and/or modify it under
# the terms of the GNU Library General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for
# more details.
#
# You should have received a copy of the GNU Library General Public License
# along with this library; if not, see <http://www.gnu.org/licenses/>.
"""
Tools provide interactive behavior to a `View` by handling specific events
sent by view.
@ -53,15 +31,21 @@ Tools can handle events in different ways
- tool can handle the event (obviously)
"""
from __future__ import absolute_import
from __future__ import print_function
__version__ = "$Revision$"
# $HeadURL$
from gi.repository import Gtk, Gdk
import sys
from gaphas.aspect import Finder, Selection, InMotion, \
HandleFinder, HandleSelection, HandleInMotion, \
Connector
import cairo
import gtk
from gaphas.canvas import Context
from gaphas.geometry import Rectangle
from gaphas.geometry import distance_point_point_fast, distance_line_point
from gaphas.item import Line
from gaphas.aspect import Finder, Selection, InMotion, \
HandleFinder, HandleSelection, HandleInMotion, \
Connector
DEBUG_TOOL_CHAIN = False
@ -70,7 +54,7 @@ Event = Context
class Tool(object):
"""
Base class for a tool. This class
Base class for a tool. This class
A word on click events:
Mouse (pointer) button click. A button press is normally followed by
@ -98,21 +82,21 @@ class Tool(object):
# Map GDK events to tool methods
EVENT_HANDLERS = {
Gdk.EventType.BUTTON_PRESS: 'on_button_press',
Gdk.EventType.BUTTON_RELEASE: 'on_button_release',
Gdk.EventType._2BUTTON_PRESS: 'on_double_click',
Gdk.EventType._3BUTTON_PRESS: 'on_triple_click',
Gdk.EventType.MOTION_NOTIFY: 'on_motion_notify',
Gdk.EventType.KEY_PRESS: 'on_key_press',
Gdk.EventType.KEY_RELEASE: 'on_key_release',
Gdk.EventType.SCROLL: 'on_scroll',
gtk.gdk.BUTTON_PRESS: 'on_button_press',
gtk.gdk.BUTTON_RELEASE: 'on_button_release',
gtk.gdk._2BUTTON_PRESS: 'on_double_click',
gtk.gdk._3BUTTON_PRESS: 'on_triple_click',
gtk.gdk.MOTION_NOTIFY: 'on_motion_notify',
gtk.gdk.KEY_PRESS: 'on_key_press',
gtk.gdk.KEY_RELEASE: 'on_key_release',
gtk.gdk.SCROLL: 'on_scroll',
# Custom events:
GRAB: 'on_grab',
UNGRAB: 'on_ungrab',
}
# Those events force the tool to release the grabbed tool.
FORCE_UNGRAB_EVENTS = (Gdk.EventType._2BUTTON_PRESS, Gdk.EventType._3BUTTON_PRESS)
FORCE_UNGRAB_EVENTS = (gtk.gdk._2BUTTON_PRESS, gtk.gdk._3BUTTON_PRESS)
def __init__(self, view=None):
self.view = view
@ -120,24 +104,28 @@ class Tool(object):
def set_view(self, view):
self.view = view
def _dispatch(self, event):
"""
Deal with the event. The event is dispatched to a specific handler
for the event type.
"""
handler = self.EVENT_HANDLERS.get(event.type)
#print event.type, handler
if handler:
try:
h = getattr(self, handler)
except AttributeError:
pass # No handler
pass # No handler
else:
return bool(h(event))
return False
def handle(self, event):
return self._dispatch(event)
def draw(self, context):
"""
Some tools (such as Rubberband selection) may need to draw something
@ -151,6 +139,7 @@ class Tool(object):
pass
class ToolChain(Tool):
"""
A ToolChain can be used to chain tools together, for example HoverTool,
@ -180,8 +169,7 @@ class ToolChain(Tool):
def grab(self, tool):
if not self._grabbed_tool:
if DEBUG_TOOL_CHAIN:
print('Grab tool', tool)
if DEBUG_TOOL_CHAIN: print 'Grab tool', tool
# Send grab event
event = Event(type=Tool.GRAB)
tool.handle(event)
@ -189,8 +177,7 @@ class ToolChain(Tool):
def ungrab(self, tool):
if self._grabbed_tool is tool:
if DEBUG_TOOL_CHAIN:
print('UNgrab tool', self._grabbed_tool)
if DEBUG_TOOL_CHAIN: print 'UNgrab tool', self._grabbed_tool
# Send ungrab event
event = Event(type=Tool.UNGRAB)
tool.handle(event)
@ -210,7 +197,7 @@ class ToolChain(Tool):
or grabbed.
If a tool is returning True on a button press event, the motion and
button release events are also passed to this
button release events are also passed to this
"""
handler = self.EVENT_HANDLERS.get(event.type)
@ -220,19 +207,19 @@ class ToolChain(Tool):
try:
return self._grabbed_tool.handle(event)
finally:
if event.type == Gdk.EventType.BUTTON_RELEASE:
if event.type == gtk.gdk.BUTTON_RELEASE:
self.ungrab(self._grabbed_tool)
else:
for tool in self._tools:
if DEBUG_TOOL_CHAIN:
print('tool', tool)
if DEBUG_TOOL_CHAIN: print 'tool', tool
rt = tool.handle(event)
if rt:
if event.type == Gdk.EventType.BUTTON_PRESS:
if event.type == gtk.gdk.BUTTON_PRESS:
self.view.grab_focus()
self.grab(tool)
return rt
def draw(self, context):
if self._grabbed_tool:
self._grabbed_tool.draw(context)
@ -264,6 +251,7 @@ class ItemTool(Tool):
self._buttons = buttons
self._movable_items = set()
def get_item(self):
return self.view.hovered_item
@ -281,23 +269,24 @@ class ItemTool(Tool):
if not set(get_ancestors(item)).intersection(selected_items):
yield InMotion(item, view)
def on_button_press(self, event):
# TODO: make keys configurable
### TODO: make keys configurable
view = self.view
item = self.get_item()
if event.get_button()[1] not in self._buttons:
if event.button not in self._buttons:
return False
# Deselect all items unless CTRL or SHIFT is pressed
# or the item is already selected.
if not (event.get_state()[1] & (Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK)
if not (event.state & (gtk.gdk.CONTROL_MASK | gtk.gdk.SHIFT_MASK)
or item in view.selected_items):
del view.selected_items
if item:
if view.hovered_item in view.selected_items and \
event.get_state()[1] & Gdk.ModifierType.CONTROL_MASK:
event.state & gtk.gdk.CONTROL_MASK:
selection = Selection(item, view)
selection.unselect()
else:
@ -307,7 +296,7 @@ class ItemTool(Tool):
return True
def on_button_release(self, event):
if event.get_button()[1] not in self._buttons:
if event.button not in self._buttons:
return False
for inmotion in self._movable_items:
inmotion.stop_move()
@ -319,7 +308,7 @@ class ItemTool(Tool):
Normally do nothing.
If a button is pressed move the items around.
"""
if event.get_state()[1] & Gdk.EventMask.BUTTON_PRESS_MASK:
if event.state & gtk.gdk.BUTTON_PRESS_MASK:
if not self._movable_items:
self._movable_items = set(self.movable_items())
@ -358,6 +347,7 @@ class HandleTool(Tool):
selection = HandleSelection(item, handle, self.view)
selection.select()
def ungrab_handle(self):
"""
Reset grabbed_handle and grabbed_item.
@ -370,6 +360,7 @@ class HandleTool(Tool):
selection = HandleSelection(item, handle, self.view)
selection.unselect()
def on_button_press(self, event):
"""
Handle button press events. If the (mouse) button is pressed on
@ -383,11 +374,11 @@ class HandleTool(Tool):
if handle:
# Deselect all items unless CTRL or SHIFT is pressed
# or the item is already selected.
# TODO: duplicate from ItemTool
if not (event.get_state()[1] & (Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK)
### TODO: duplicate from ItemTool
if not (event.state & (gtk.gdk.CONTROL_MASK | gtk.gdk.SHIFT_MASK)
or view.hovered_item in view.selected_items):
del view.selected_items
#
###/
view.hovered_item = item
view.focused_item = item
@ -421,7 +412,7 @@ class HandleTool(Tool):
hovered-item.
"""
view = self.view
if self.grabbed_handle and event.get_state()[1] & Gdk.EventMask.BUTTON_PRESS_MASK:
if self.grabbed_handle and event.state & gtk.gdk.BUTTON_PRESS_MASK:
canvas = view.canvas
item = self.grabbed_item
handle = self.grabbed_handle
@ -436,6 +427,7 @@ class HandleTool(Tool):
class RubberbandTool(Tool):
def __init__(self, view=None):
super(RubberbandTool, self).__init__(view)
self.x0, self.y0, self.x1, self.y1 = 0, 0, 0, 0
@ -449,11 +441,11 @@ class RubberbandTool(Tool):
self.queue_draw(self.view)
x0, y0, x1, y1 = self.x0, self.y0, self.x1, self.y1
self.view.select_in_rectangle((min(x0, x1), min(y0, y1),
abs(x1 - x0), abs(y1 - y0)))
abs(x1 - x0), abs(y1 - y0)))
return True
def on_motion_notify(self, event):
if event.get_state()[1] & Gdk.EventMask.BUTTON_PRESS_MASK:
if event.state & gtk.gdk.BUTTON_PRESS_MASK:
view = self.view
self.queue_draw(view)
self.x1, self.y1 = event.x, event.y
@ -468,15 +460,13 @@ class RubberbandTool(Tool):
cr = context.cairo
x0, y0, x1, y1 = self.x0, self.y0, self.x1, self.y1
cr.set_line_width(1.0)
cr.set_source_rgba(.5, .5, .7, .5)
cr.set_source_rgba(.5, .5, .7, .6)
cr.rectangle(min(x0, x1), min(y0, y1), abs(x1 - x0), abs(y1 - y0))
cr.fill()
PAN_MASK = Gdk.ModifierType.SHIFT_MASK | Gdk.ModifierType.MOD1_MASK | Gdk.ModifierType.CONTROL_MASK
PAN_MASK = gtk.gdk.SHIFT_MASK | gtk.gdk.MOD1_MASK | gtk.gdk.CONTROL_MASK
PAN_VALUE = 0
class PanTool(Tool):
"""
Captures drag events with the middle mouse button and uses them to
@ -490,9 +480,9 @@ class PanTool(Tool):
self.speed = 10
def on_button_press(self, event):
if not event.get_state()[1] & PAN_MASK == PAN_VALUE:
if not event.state & PAN_MASK == PAN_VALUE:
return False
if event.get_button()[1] == 2:
if event.button == 2:
self.x0, self.y0 = event.x, event.y
return True
@ -501,12 +491,12 @@ class PanTool(Tool):
return True
def on_motion_notify(self, event):
if event.get_state()[1] & Gdk.EventMask.BUTTON2_MOTION_MASK:
if event.state & gtk.gdk.BUTTON2_MASK:
view = self.view
self.x1, self.y1 = event.x, event.y
dx = self.x1 - self.x0
dy = self.y1 - self.y0
view._matrix.translate(dx / view._matrix[0], dy / view._matrix[3])
view._matrix.translate(dx/view._matrix[0], dy/view._matrix[3])
# Make sure everything's updated
view.request_update((), view._canvas.get_all_items())
self.x0 = self.x1
@ -515,25 +505,25 @@ class PanTool(Tool):
def on_scroll(self, event):
# Ensure no modifiers
if not event.get_state()[1] & PAN_MASK == PAN_VALUE:
if not event.state & PAN_MASK == PAN_VALUE:
return False
view = self.view
direction = event.scroll.direction
if direction == Gdk.ScrollDirection.LEFT:
view._matrix.translate(self.speed / view._matrix[0], 0)
elif direction == Gdk.ScrollDirection.RIGHT:
view._matrix.translate(-self.speed / view._matrix[0], 0)
elif direction == Gdk.ScrollDirection.UP:
view._matrix.translate(0, self.speed / view._matrix[3])
elif direction == Gdk.ScrollDirection.DOWN:
view._matrix.translate(0, -self.speed / view._matrix[3])
direction = event.direction
gdk = gtk.gdk
if direction == gdk.SCROLL_LEFT:
view._matrix.translate(self.speed/view._matrix[0], 0)
elif direction == gdk.SCROLL_RIGHT:
view._matrix.translate(-self.speed/view._matrix[0], 0)
elif direction == gdk.SCROLL_UP:
view._matrix.translate(0, self.speed/view._matrix[3])
elif direction == gdk.SCROLL_DOWN:
view._matrix.translate(0, -self.speed/view._matrix[3])
view.request_update((), view._canvas.get_all_items())
return True
ZOOM_MASK = Gdk.ModifierType.CONTROL_MASK | Gdk.ModifierType.SHIFT_MASK | Gdk.ModifierType.MOD1_MASK
ZOOM_VALUE = Gdk.ModifierType.CONTROL_MASK
ZOOM_MASK = gtk.gdk.CONTROL_MASK | gtk.gdk.SHIFT_MASK | gtk.gdk.MOD1_MASK
ZOOM_VALUE = gtk.gdk.CONTROL_MASK
class ZoomTool(Tool):
"""
@ -549,8 +539,8 @@ class ZoomTool(Tool):
self.lastdiff = 0;
def on_button_press(self, event):
if event.get_button()[1] == 2 \
and event.get_state()[1] & ZOOM_MASK == ZOOM_VALUE:
if event.button == 2 \
and event.state & ZOOM_MASK == ZOOM_VALUE:
self.x0 = event.x
self.y0 = event.y
self.lastdiff = 0
@ -561,8 +551,8 @@ class ZoomTool(Tool):
return True
def on_motion_notify(self, event):
if event.get_state()[1] & ZOOM_MASK == ZOOM_VALUE \
and event.get_state()[1] & Gdk.EventMask.BUTTON2_MOTION_MASK:
if event.state & ZOOM_MASK == ZOOM_VALUE \
and event.state & gtk.gdk.BUTTON2_MASK:
view = self.view
dy = event.y - self.y0
@ -573,7 +563,7 @@ class ZoomTool(Tool):
if abs(dy - self.lastdiff) > 20:
if dy - self.lastdiff < 0:
factor = 1. / 0.9
factor = 1./0.9
else:
factor = 0.9
@ -585,18 +575,18 @@ class ZoomTool(Tool):
# Make sure everything's updated
view.request_update((), view._canvas.get_all_items())
self.lastdiff = dy
self.lastdiff = dy;
return True
def on_scroll(self, event):
if event.get_state()[1] & Gdk.ModifierType.CONTROL_MASK:
if event.state & gtk.gdk.CONTROL_MASK:
view = self.view
sx = view._matrix[0]
sy = view._matrix[3]
ox = (view._matrix[4] - event.scroll.x) / sx
oy = (view._matrix[5] - event.scroll.y) / sy
ox = (view._matrix[4] - event.x) / sx
oy = (view._matrix[5] - event.y) / sy
factor = 0.9
if event.scroll.direction == Gdk.ScrollDirection.UP:
if event.direction == gtk.gdk.SCROLL_UP:
factor = 1. / factor
view._matrix.translate(-ox, -oy)
view._matrix.scale(factor, factor)
@ -607,6 +597,7 @@ class ZoomTool(Tool):
class PlacementTool(Tool):
def __init__(self, view, factory, handle_tool, handle_index):
super(PlacementTool, self).__init__(view)
self._factory = factory
@ -616,10 +607,12 @@ class PlacementTool(Tool):
self._new_item = None
self.grabbed_handle = None
#handle_tool = property(lambda s: s._handle_tool, doc="Handle tool")
handle_index = property(lambda s: s._handle_index,
doc="Index of handle to be used by handle_tool")
new_item = property(lambda s: s._new_item, doc="The newly created item")
def on_button_press(self, event):
view = self.view
canvas = view.canvas
@ -637,6 +630,7 @@ class PlacementTool(Tool):
self.grabbed_handle = h
return True
def _create_item(self, pos, **kw):
view = self.view
item = self._factory(**kw)
@ -644,6 +638,7 @@ class PlacementTool(Tool):
item.matrix.translate(x, y)
return item
def on_button_release(self, event):
if self.grabbed_handle:
self.handle_tool.on_button_release(event)
@ -668,40 +663,48 @@ class TextEditTool(Tool):
def __init__(self, view=None):
super(TextEditTool, self).__init__(view)
def on_double_click(self, event):
"""
Create a popup window with some editable text.
"""
window = Gtk.Window()
window = gtk.Window()
window.set_property('decorated', False)
window.set_resize_mode(Gtk.ResizeMode.IMMEDIATE)
window.set_parent_window(self.view.get_window())
buffer = Gtk.TextBuffer()
text_view = Gtk.TextView()
window.set_resize_mode(gtk.RESIZE_IMMEDIATE)
#window.set_modal(True)
window.set_parent_window(self.view.window)
buffer = gtk.TextBuffer()
text_view = gtk.TextView()
text_view.set_buffer(buffer)
text_view.show()
window.add(text_view)
allocate = Gdk.Rectangle()
allocate.x = int(event.x)
allocate.y = int(event.y)
allocate.width = 50
allocate.height = 50
window.size_allocate(allocate)
window.size_allocate(gtk.gdk.Rectangle(int(event.x), int(event.y), 50, 50))
#window.move(int(event.x), int(event.y))
cursor_pos = self.view.get_toplevel().get_screen().get_display().get_pointer()
window.move(cursor_pos[1], cursor_pos[2])
window.connect('focus-out-event', self._on_focus_out_event, buffer)
text_view.connect('key-press-event', self._on_key_press_event, buffer)
window.show_all()
#text_view.set_size_request(50, 50)
window.show()
#text_view.grab_focus()
#window.set_uposition(event.x, event.y)
#window.focus
return True
def _on_key_press_event(self, widget, event, buffer):
if event.keyval == Gdk.KEY_Escape:
#if event.keyval == gtk.keysyms.Return:
#print 'Enter!'
#widget.get_toplevel().destroy()
if event.keyval == gtk.keysyms.Escape:
#print 'Escape!'
widget.get_toplevel().destroy()
def _on_focus_out_event(self, widget, event, buffer):
#print 'focus out!', buffer.get_text(buffer.get_start_iter(), buffer.get_end_iter())
widget.destroy()
class ConnectHandleTool(HandleTool):
"""
Tool for connecting two items.
@ -723,6 +726,7 @@ class ConnectHandleTool(HandleTool):
else:
return HandleInMotion(item, handle, self.view).glue(vpos)
def connect(self, item, handle, vpos):
"""
Connect a handle of a item to connectable item.
@ -750,6 +754,7 @@ class ConnectHandleTool(HandleTool):
if cinfo:
connector.disconnect()
def on_button_release(self, event):
view = self.view
item = self.grabbed_item
@ -774,4 +779,5 @@ def DefaultTool(view=None):
append(TextEditTool()). \
append(RubberbandTool())
# vim: sw=4:et:ai

View File

@ -1,44 +1,18 @@
#!/usr/bin/env python
# Copyright (C) 2006-2017 Arjan Molenaar <gaphor@gmail.com>
# Artur Wroblewski <wrobell@pld-linux.org>
# Dan Yeaw <dan@yeaw.me>
#
# This file is part of Gaphas.
#
# This library is free software; you can redistribute it and/or modify it under
# the terms of the GNU Library General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for
# more details.
#
# You should have received a copy of the GNU Library General Public License
# along with this library; if not, see <http://www.gnu.org/licenses/>.
"""
Simple class containing the tree structure for the canvas items.
"""
from __future__ import absolute_import
from operator import attrgetter
from six.moves import map
from six.moves import range
__version__ = "$Revision$"
# $HeadURL$
from operator import attrgetter
class Tree(object):
"""
A Tree structure. Nodes are stores in a depth-first order.
``None`` is the root node.
@invariant: len(self._children) == len(self._nodes) + 1
@ -50,10 +24,10 @@ class Tree(object):
self._nodes = []
# Per entry a list of children is maintained.
self._children = {None: []}
self._children = { None: [] }
# For easy and fast lookups, also maintain a child -> parent mapping
self._parents = {}
self._parents = { }
nodes = property(lambda s: list(s._nodes))
@ -207,12 +181,12 @@ class Tree(object):
"""
nodes = self.nodes
lnodes = len(nodes)
list(map(setattr, nodes, [index_key] * lnodes, range(lnodes)))
map(setattr, nodes, [index_key] * lnodes, xrange(lnodes))
def sort(self, nodes, index_key, reverse=False):
"""
Sort a set (or list) of nodes.
>>> class A(object):
... def __init__(self, n):
... self.n = n
@ -246,7 +220,7 @@ class Tree(object):
atnode = siblings[index]
except (TypeError, IndexError):
index = len(siblings)
# self._add_to_nodes(node, parent)
#self._add_to_nodes(node, parent)
if parent:
try:
next_uncle = self.get_next_sibling(parent)
@ -262,6 +236,7 @@ class Tree(object):
else:
nodes.insert(nodes.index(atnode), node)
def _add(self, node, parent=None, index=None):
"""
Helper method for both add() and reparent().
@ -282,6 +257,7 @@ class Tree(object):
if parent:
self._parents[node] = parent
def add(self, node, parent=None, index=None):
"""
Add node to the tree. parent is the parent node, which may
@ -292,6 +268,7 @@ class Tree(object):
self._add(node, parent, index)
self._children[node] = []
def _remove(self, node):
# Remove from parent item
self.get_siblings(node).remove(node)
@ -346,7 +323,7 @@ class Tree(object):
['n1', 'n3', 'n2']
If a node contains children, those are also moved:
>>> tree.add('n4')
>>> tree.nodes
['n1', 'n3', 'n2', 'n4']
@ -374,4 +351,5 @@ class Tree(object):
for c in self._children[node]:
self._reparent_nodes(c, node)
# vi: sw=4:et:ai

View File

@ -1,37 +1,13 @@
#!/usr/bin/env python
# Copyright (C) 2006-2017 Antony N. Pavlov <antony@niisi.msk.ru>
# Arjan Molenaar <gaphor@gmail.com>
# Artur Wroblewski <wrobell@pld-linux.org>
# Dan Yeaw <dan@yeaw.me>
#
# This file is part of Gaphas.
#
# This library is free software; you can redistribute it and/or modify it under
# the terms of the GNU Library General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for
# more details.
#
# You should have received a copy of the GNU Library General Public License
# along with this library; if not, see <http://www.gnu.org/licenses/>.
"""
Helper functions and classes for Cairo (drawing engine used by the canvas).
"""
from __future__ import absolute_import
__version__ = "$Revision$"
# $HeadURL$
from math import pi
import cairo
__version__ = "$Revision$"
# $HeadURL$
def text_extents(cr, text, font=None, multiline=False, padding=1):
"""
@ -51,7 +27,7 @@ def text_extents(cr, text, font=None, multiline=False, padding=1):
height += h + padding
else:
x_bear, y_bear, width, height, x_adv, y_adv = cr.text_extents(text)
# width, height = width + x_bearing, height + y_bearing
#width, height = width + x_bearing, height + y_bearing
if font:
cr.restore()
@ -77,13 +53,13 @@ def text_align(cr, x, y, text, align_x=0, align_y=0, padding_x=0, padding_y=0):
x_bear, y_bear, w, h, x_adv, y_adv = cr.text_extents(text)
if align_x == 0:
x += 0.5 - (w / 2 + x_bear)
x = 0.5 - (w / 2 + x_bear) + x
elif align_x < 0:
x = - (w + x_bear) + x - padding_x
else:
x += padding_x
x = x + padding_x
if align_y == 0:
y += 0.5 - (h / 2 + y_bear)
y = 0.5 - (h / 2 + y_bear) + y
elif align_y < 0:
y = - (h + y_bear) + y - padding_y
else:
@ -101,9 +77,8 @@ def text_multiline(cr, x, y, text, padding=1):
text - text to draw
padding - additional padding between lines.
"""
if not text:
return
# cr.move_to(x, y)
if not text: return
#cr.move_to(x, y)
for line in text.split('\n'):
x_bear, y_bear, w, h, x_adv, y_adv = cr.text_extents(text)
y += h
@ -132,13 +107,14 @@ def text_set_font(cr, font):
"""
font = font.split()
cr.select_font_face(font[0],
'italic' in font and cairo.FONT_SLANT_ITALIC or cairo.FONT_SLANT_NORMAL,
'bold' in font and cairo.FONT_WEIGHT_BOLD or cairo.FONT_WEIGHT_NORMAL
)
'italic' in font and cairo.FONT_SLANT_ITALIC \
or cairo.FONT_SLANT_NORMAL,
'bold' in font and cairo.FONT_WEIGHT_BOLD \
or cairo.FONT_WEIGHT_NORMAL)
cr.set_font_size(float(font[-1]))
def path_ellipse(cr, x, y, width, height, angle=0):
def path_ellipse (cr, x, y, width, height, angle=0):
"""
Draw an ellipse.
x - center x
@ -148,11 +124,12 @@ def path_ellipse(cr, x, y, width, height, angle=0):
angle - angle in radians to rotate, clockwise
"""
cr.save()
cr.translate(x, y)
cr.rotate(angle)
cr.scale(width / 2.0, height / 2.0)
cr.translate (x, y)
cr.rotate (angle)
cr.scale (width / 2.0, height / 2.0)
cr.move_to(1.0, 0.0)
cr.arc(0.0, 0.0, 1.0, 0.0, 2.0 * pi)
cr.arc (0.0, 0.0, 1.0, 0.0, 2.0 * pi)
cr.restore()
# vim:sw=4:et

View File

@ -1,57 +1,32 @@
#!/usr/bin/env python
# Copyright (C) 2006-2017 Adrian Boguszewski <adrbogus1@student.pg.gda.pl>
# Arjan Molenaar <gaphor@gmail.com>
# Artur Wroblewski <wrobell@pld-linux.org>
# Dan Yeaw <dan@yeaw.me>
#
# This file is part of Gaphas.
#
# This library is free software; you can redistribute it and/or modify it under
# the terms of the GNU Library General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for
# more details.
#
# You should have received a copy of the GNU Library General Public License
# along with this library; if not, see <http://www.gnu.org/licenses/>.
"""
This module contains everything to display a Canvas on a screen.
"""
from __future__ import absolute_import
__version__ = "$Revision$"
# $HeadURL$
import gobject
import gtk
from cairo import Matrix
from six.moves import map
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import GObject, Gtk, Gdk
from gaphas.canvas import Context
from gaphas.decorators import async, PRIORITY_HIGH_IDLE
from gaphas.decorators import nonrecursive
from gaphas.geometry import Rectangle, distance_point_point_fast
from gaphas.painter import DefaultPainter, BoundingBoxPainter
from gaphas.quadtree import Quadtree
from gaphas.tool import DefaultTool
from canvas import Context
from geometry import Rectangle, distance_point_point_fast
from quadtree import Quadtree
from tool import DefaultTool
from painter import DefaultPainter, BoundingBoxPainter
from decorators import async, PRIORITY_HIGH_IDLE
from decorators import nonrecursive
# Handy debug flag for drawing bounding boxes around the items.
DEBUG_DRAW_BOUNDING_BOX = False
DEBUG_DRAW_QUADTREE = False
# The default cursor (use in case of a cursor reset)
DEFAULT_CURSOR = Gdk.CursorType.LEFT_PTR
DEFAULT_CURSOR = gtk.gdk.LEFT_PTR
class View(object):
"""
View class for gaphas.Canvas objects.
View class for gaphas.Canvas objects.
"""
def __init__(self, canvas=None):
@ -60,12 +35,12 @@ class View(object):
self._bounding_box_painter = BoundingBoxPainter(self)
# Handling selections.
# TODO: Move this to a context?
### TODO: Move this to a context?
self._selected_items = set()
self._focused_item = None
self._hovered_item = None
self._dropzone_item = None
#/
###/
self._qtree = Quadtree()
self._bounds = Rectangle(0, 0, 0, 0)
@ -74,9 +49,11 @@ class View(object):
if canvas:
self._set_canvas(canvas)
matrix = property(lambda s: s._matrix,
doc="Canvas to view transformation matrix")
def _set_canvas(self, canvas):
"""
Use view.canvas = my_canvas to set the canvas to be rendered
@ -93,18 +70,21 @@ class View(object):
canvas = property(lambda s: s._canvas, _set_canvas)
def emit(self, *args, **kwargs):
"""
Placeholder method for signal emission functionality.
"""
pass
def queue_draw_item(self, *items):
"""
Placeholder for item redraw queueing.
"""
pass
def select_item(self, item):
"""
Select an item. This adds @item to the set of selected items.
@ -114,6 +94,7 @@ class View(object):
self._selected_items.add(item)
self.emit('selection-changed', self._selected_items)
def unselect_item(self, item):
"""
Unselect an item.
@ -123,10 +104,12 @@ class View(object):
self._selected_items.discard(item)
self.emit('selection-changed', self._selected_items)
def select_all(self):
for item in self.canvas.get_all_items():
self.select_item(item)
def unselect_all(self):
"""
Clearing the selected_item also clears the focused_item.
@ -136,10 +119,12 @@ class View(object):
self.focused_item = None
self.emit('selection-changed', self._selected_items)
selected_items = property(lambda s: s._selected_items,
select_item, unselect_all,
"Items selected by the view")
def _set_focused_item(self, item):
"""
Set the focused item, this item is also added to the selected_items
@ -154,16 +139,19 @@ class View(object):
self._focused_item = item
self.emit('focus-changed', item)
def _del_focused_item(self):
"""
Items that loose focus remain selected.
"""
self._set_focused_item(None)
focused_item = property(lambda s: s._focused_item,
_set_focused_item, _del_focused_item,
"The item with focus (receives key events a.o.)")
def _set_hovered_item(self, item):
"""
Set the hovered item.
@ -173,16 +161,19 @@ class View(object):
self._hovered_item = item
self.emit('hover-changed', item)
def _del_hovered_item(self):
"""
Unset the hovered item.
"""
self._set_hovered_item(None)
hovered_item = property(lambda s: s._hovered_item,
_set_hovered_item, _del_hovered_item,
"The item directly under the mouse pointer")
def _set_dropzone_item(self, item):
"""
Set dropzone item.
@ -192,15 +183,18 @@ class View(object):
self._dropzone_item = item
self.emit('dropzone-changed', item)
def _del_dropzone_item(self):
"""
Unset dropzone item.
"""
self._set_dropzone_item(None)
dropzone_item = property(lambda s: s._dropzone_item,
_set_dropzone_item, _del_dropzone_item,
'The item which can group other items')
_set_dropzone_item, _del_dropzone_item,
'The item which can group other items')
def _set_painter(self, painter):
"""
@ -210,8 +204,10 @@ class View(object):
painter.set_view(self)
self.emit('painter-changed')
painter = property(lambda s: s._painter, _set_painter)
def _set_bounding_box_painter(self, painter):
"""
Set the painter to use for bounding box calculations.
@ -220,8 +216,10 @@ class View(object):
painter.set_view(self)
self.emit('painter-changed')
bounding_box_painter = property(lambda s: s._bounding_box_painter, _set_bounding_box_painter)
def get_item_at_point(self, pos, selected=True):
"""
Return the topmost item located at ``pos`` (x, y).
@ -240,12 +238,12 @@ class View(object):
return item
return None
def get_handle_at_point(self, pos, distance=6):
"""
Look for a handle at ``pos`` and return the
tuple (item, handle).
"""
def find(item):
""" Find item's handle at pos """
v2i = self.get_matrix_v2i(item)
@ -282,6 +280,7 @@ class View(object):
return item, h
return None, None
def get_port_at_point(self, vpos, distance=10, exclude=None):
"""
Find item with port closest to specified position.
@ -336,6 +335,7 @@ class View(object):
return item, port, glue_pos
def get_items_in_rectangle(self, rect, intersect=True, reverse=False):
"""
Return the items in the rectangle 'rect'.
@ -347,13 +347,15 @@ class View(object):
items = self._qtree.find_inside(rect)
return self._canvas.sort(items, reverse=reverse)
def select_in_rectangle(self, rect):
"""
Select all items who have their bounding box within the
rectangle @rect.
"""
items = self._qtree.find_inside(rect)
list(map(self.select_item, items))
map(self.select_item, items)
def zoom(self, factor):
"""
@ -363,9 +365,10 @@ class View(object):
self._matrix.scale(factor, factor)
# Make sure everything's updated
# map(self.update_matrix, self._canvas.get_all_items())
#map(self.update_matrix, self._canvas.get_all_items())
self.request_update((), self._canvas.get_all_items())
def set_item_bounding_box(self, item, bounds):
"""
Update the bounding box of the item.
@ -380,17 +383,20 @@ class View(object):
ix1, iy1 = v2i(bounds.x1, bounds.y1)
self._qtree.add(item=item, bounds=bounds, data=(ix0, iy0, ix1, iy1))
def get_item_bounding_box(self, item):
"""
Get the bounding box for the item, in view coordinates.
"""
return self._qtree.get_bounds(item)
bounding_box = property(lambda s: s._bounds)
def update_bounding_box(self, cr, items=None):
"""
Update the bounding boxes of the canvas items for this view, in
Update the bounding boxes of the canvas items for this view, in
canvas coordinates.
"""
painter = self._bounding_box_painter
@ -405,11 +411,13 @@ class View(object):
# Update the view's bounding box with the rest of the items
self._bounds = Rectangle(*self._qtree.soft_bounds)
def paint(self, cr):
self._painter.paint(Context(cairo=cr,
items=self.canvas.get_all_items(),
area=None))
def get_matrix_i2v(self, item):
"""
Get Item to View matrix for ``item``.
@ -418,6 +426,7 @@ class View(object):
self.update_matrix(item)
return item._matrix_i2v[self]
def get_matrix_v2i(self, item):
"""
Get View to Item matrix for ``item``.
@ -426,6 +435,7 @@ class View(object):
self.update_matrix(item)
return item._matrix_v2i[self]
def update_matrix(self, item):
"""
Update item matrices related to view.
@ -443,6 +453,7 @@ class View(object):
v2i.invert()
item._matrix_v2i[self] = v2i
def _clear_matrices(self):
"""
Clear registered data in Item's _matrix{i2c|v2i} attributes.
@ -455,7 +466,9 @@ class View(object):
pass
class GtkView(Gtk.DrawingArea, Gtk.Scrollable, View):
class GtkView(gtk.DrawingArea, View):
# NOTE: Inherit from GTK+ class first, otherwise BusErrors may occur!
"""
GTK+ widget for rendering a canvas.Canvas to a screen.
@ -473,92 +486,58 @@ class GtkView(Gtk.DrawingArea, Gtk.Scrollable, View):
# Signals: emited after the change takes effect.
__gsignals__ = {
'dropzone-changed': (GObject.SignalFlags.RUN_LAST, None,
(GObject.TYPE_PYOBJECT,)),
'hover-changed': (GObject.SignalFlags.RUN_LAST, None,
(GObject.TYPE_PYOBJECT,)),
'focus-changed': (GObject.SignalFlags.RUN_LAST, None,
(GObject.TYPE_PYOBJECT,)),
'selection-changed': (GObject.SignalFlags.RUN_LAST, None,
(GObject.TYPE_PYOBJECT,)),
'tool-changed': (GObject.SignalFlags.RUN_LAST, None,
()),
'painter-changed': (GObject.SignalFlags.RUN_LAST, None,
())
'set-scroll-adjustments': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
(gtk.Adjustment, gtk.Adjustment)),
'dropzone-changed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
(gobject.TYPE_PYOBJECT,)),
'hover-changed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
(gobject.TYPE_PYOBJECT,)),
'focus-changed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
(gobject.TYPE_PYOBJECT,)),
'selection-changed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
(gobject.TYPE_PYOBJECT,)),
'tool-changed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
()),
'painter-changed': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
())
}
__gproperties__ = {
"hscroll-policy": (Gtk.ScrollablePolicy, "hscroll-policy",
"hscroll-policy", Gtk.ScrollablePolicy.MINIMUM,
GObject.PARAM_READWRITE),
"hadjustment": (Gtk.Adjustment, "hadjustment", "hadjustment",
GObject.PARAM_READWRITE),
"vscroll-policy": (Gtk.ScrollablePolicy, "hscroll-policy",
"hscroll-policy", Gtk.ScrollablePolicy.MINIMUM,
GObject.PARAM_READWRITE),
"vadjustment": (Gtk.Adjustment, "hadjustment", "hadjustment",
GObject.PARAM_READWRITE),
}
def __init__(self, canvas=None, hadjustment=None, vadjustment=None):
GObject.GObject.__init__(self)
gtk.DrawingArea.__init__(self)
self._dirty_items = set()
self._dirty_matrix_items = set()
View.__init__(self, canvas)
self.set_can_focus(True)
self.add_events(Gdk.EventMask.BUTTON_PRESS_MASK
| Gdk.EventMask.BUTTON_RELEASE_MASK
| Gdk.EventMask.POINTER_MOTION_MASK
| Gdk.EventMask.KEY_PRESS_MASK
| Gdk.EventMask.KEY_RELEASE_MASK
| Gdk.EventMask.SCROLL_MASK)
self.set_flags(gtk.CAN_FOCUS)
self.add_events(gtk.gdk.BUTTON_PRESS_MASK
| gtk.gdk.BUTTON_RELEASE_MASK
| gtk.gdk.POINTER_MOTION_MASK
| gtk.gdk.KEY_PRESS_MASK
| gtk.gdk.KEY_RELEASE_MASK
| gtk.gdk.SCROLL_MASK)
self._hscroll_policy = None
self._vscroll_policy = None
self._hadjustment = None
self._vadjustment = None
self._hadjustment_handler_id = None
self._vadjustment_handler_id = None
self._set_scroll_adjustments(None, None)
self.emit('set-scroll-adjustments', hadjustment, vadjustment)
self._set_tool(DefaultTool())
# Set background to white.
self.modify_bg(Gtk.StateType.NORMAL, Gdk.color_parse('#FFF'))
self.modify_bg(gtk.STATE_NORMAL, gtk.gdk.color_parse('#FFF'))
def do_set_property(self, pspec, value):
if pspec.name == 'hscroll-policy':
self._hscroll_policy = value
elif pspec.name == 'vscroll-policy':
self._vscroll_policy = value
elif pspec.name == 'hadjustment':
self._set_scroll_adjustments(value, self._vadjustment)
elif pspec.name == 'vadjustment':
self._set_scroll_adjustments(self._hadjustment, value)
else:
raise AttributeError('Unknown property %s' % pspec.name)
def do_get_property(self, pspec):
if pspec.name == 'hscroll-policy':
return self._hscroll_policy
elif pspec.name == 'vscroll-policy':
return self._vscroll_policy
elif pspec.name == 'hadjustment':
return self._hadjustment
elif pspec.name == 'vadjustment':
return self._vadjustment
else:
raise AttributeError('Unknown property %s' % pspec.name)
def emit(self, *args, **kwargs):
"""
Delegate signal emissions to the DrawingArea (=GTK+)
"""
Gtk.DrawingArea.emit(self, *args, **kwargs)
gtk.DrawingArea.emit(self, *args, **kwargs)
def _set_canvas(self, canvas):
"""
@ -580,6 +559,7 @@ class GtkView(Gtk.DrawingArea, Gtk.Scrollable, View):
canvas = property(lambda s: s._canvas, _set_canvas)
def _set_tool(self, tool):
"""
Set the tool to use. Tools should implement tool.Tool.
@ -588,13 +568,17 @@ class GtkView(Gtk.DrawingArea, Gtk.Scrollable, View):
tool.set_view(self)
self.emit('tool-changed')
tool = property(lambda s: s._tool, _set_tool)
hadjustment = property(lambda s: s._hadjustment)
vadjustment = property(lambda s: s._vadjustment)
def _set_scroll_adjustments(self, hadjustment, vadjustment):
def do_set_scroll_adjustments(self, hadjustment, vadjustment):
if self._hadjustment_handler_id:
self._hadjustment.disconnect(self._hadjustment_handler_id)
self._hadjustment_handler_id = None
@ -602,17 +586,18 @@ class GtkView(Gtk.DrawingArea, Gtk.Scrollable, View):
self._vadjustment.disconnect(self._vadjustment_handler_id)
self._vadjustment_handler_id = None
self._hadjustment = hadjustment or Gtk.Adjustment()
self._vadjustment = vadjustment or Gtk.Adjustment()
self._hadjustment = hadjustment or gtk.Adjustment()
self._vadjustment = vadjustment or gtk.Adjustment()
self._hadjustment_handler_id = \
self._hadjustment.connect('value-changed',
self.on_adjustment_changed)
self._hadjustment.connect('value-changed',
self.on_adjustment_changed)
self._vadjustment_handler_id = \
self._vadjustment.connect('value-changed',
self.on_adjustment_changed)
self._vadjustment.connect('value-changed',
self.on_adjustment_changed)
self.update_adjustments()
def zoom(self, factor):
"""
Zoom in/out by factor ``factor``.
@ -620,10 +605,11 @@ class GtkView(Gtk.DrawingArea, Gtk.Scrollable, View):
super(GtkView, self).zoom(factor)
self.queue_draw_refresh()
@async(single=True)
def update_adjustments(self, allocation=None):
if not allocation:
allocation = self.get_allocation()
allocation = self.allocation
hadjustment = self._hadjustment
vadjustment = self._vadjustment
@ -641,28 +627,26 @@ class GtkView(Gtk.DrawingArea, Gtk.Scrollable, View):
u = c + v
# set lower limits
hadjustment.set_lower(u.x)
vadjustment.set_lower(u.y)
hadjustment.lower, vadjustment.lower = u.x, u.y
# set upper limits
hadjustment.set_upper(u.x1)
vadjustment.set_upper(u.y1)
hadjustment.upper, vadjustment.upper = u.x1, u.y1
# set page size
aw, ah = allocation.width, allocation.height
hadjustment.set_page_size(aw)
vadjustment.set_page_size(ah)
hadjustment.page_size = aw
vadjustment.page_size = ah
# set increments
hadjustment.set_page_increment(aw)
hadjustment.set_step_increment(aw / 10)
vadjustment.set_page_increment(ah)
vadjustment.set_step_increment(ah / 10)
hadjustment.page_increment = aw
hadjustment.step_increment = aw / 10
vadjustment.page_increment = ah
vadjustment.step_increment = ah / 10
# set position
if v.x != hadjustment.get_value() or v.y != vadjustment.get_value():
hadjustment.set_value(v.x)
vadjustment.set_value(v.y)
if v.x != hadjustment.value or v.y != vadjustment.value:
hadjustment.value, vadjustment.value = v.x, v.y
def queue_draw_item(self, *items):
"""
@ -673,7 +657,7 @@ class GtkView(Gtk.DrawingArea, Gtk.Scrollable, View):
TODO: Should we also create a (sorted) list of items that need redrawal?
"""
get_bounds = self._qtree.get_bounds
items = [_f for _f in items if _f]
items = filter(None, items)
try:
# create a copy, otherwise we'll change the original rectangle
bounds = Rectangle(*get_bounds(items[0]))
@ -683,26 +667,29 @@ class GtkView(Gtk.DrawingArea, Gtk.Scrollable, View):
except IndexError:
pass
except KeyError:
pass # No bounds calculated yet? bummer.
pass # No bounds calculated yet? bummer.
def queue_draw_area(self, x, y, w, h):
"""
Wrap draw_area to convert all values to ints.
"""
try:
super(GtkView, self).queue_draw_area(int(x), int(y), int(w + 1), int(h + 1))
super(GtkView, self).queue_draw_area(int(x), int(y), int(w+1), int(h+1))
except OverflowError:
# Okay, now the zoom factor is very large or something
a = self.get_allocation()
a = self.allocation
super(GtkView, self).queue_draw_area(0, 0, a.width, a.height)
def queue_draw_refresh(self):
"""
Redraw the entire view.
"""
a = self.get_allocation()
a = self.allocation
super(GtkView, self).queue_draw_area(0, 0, a.width, a.height)
def request_update(self, items, matrix_only_items=(), removed_items=()):
"""
Request update for items. Items will get a full update treatment, while
@ -731,13 +718,13 @@ class GtkView(Gtk.DrawingArea, Gtk.Scrollable, View):
self.update()
@async(single=True, priority=PRIORITY_HIGH_IDLE)
def update(self):
"""
Update view status according to the items updated by the canvas.
"""
if not self.get_window():
return
if not self.window: return
dirty_items = self._dirty_items
dirty_matrix_items = self._dirty_matrix_items
@ -773,12 +760,13 @@ class GtkView(Gtk.DrawingArea, Gtk.Scrollable, View):
self._dirty_items.clear()
self._dirty_matrix_items.clear()
@async(single=False)
def update_bounding_box(self, items):
"""
Update bounding box is not necessary.
"""
cr = self.get_window().cairo_create()
cr = self.window.cairo_create()
cr.save()
cr.rectangle(0, 0, 0, 0)
@ -790,19 +778,19 @@ class GtkView(Gtk.DrawingArea, Gtk.Scrollable, View):
self.queue_draw_item(*items)
self.update_adjustments()
@nonrecursive
def do_configure_event(self, event):
def do_size_allocate(self, allocation):
"""
Widget size has changed.
Allocate the widget size ``(x, y, width, height)``.
"""
self.update_adjustments(event)
self._qtree.resize((0, 0, event.width, event.height))
gtk.DrawingArea.do_size_allocate(self, allocation)
self.update_adjustments(allocation)
self._qtree.resize((0, 0, allocation.width, allocation.height))
def do_realize(self):
"""
The ::realize signal is emitted when widget is associated with a ``GdkWindow``.
"""
Gtk.DrawingArea.do_realize(self)
gtk.DrawingArea.do_realize(self)
# Ensure updates are propagated
self._canvas.register_view(self)
@ -811,9 +799,6 @@ class GtkView(Gtk.DrawingArea, Gtk.Scrollable, View):
self.request_update(self._canvas.get_all_items())
def do_unrealize(self):
"""
The ::unrealize signal is emitted when the ``GdkWindow`` associated with widget is destroyed.
"""
if self.canvas:
# Although Item._matrix_{i2v|v2i} keys are automatically removed
# (weak refs), better do it explicitly to be sure.
@ -825,17 +810,23 @@ class GtkView(Gtk.DrawingArea, Gtk.Scrollable, View):
self._canvas.unregister_view(self)
Gtk.DrawingArea.do_unrealize(self)
gtk.DrawingArea.do_unrealize(self)
def do_draw(self, cr):
def do_expose_event(self, event):
"""
Render canvas to the screen.
"""
if not self._canvas:
return
_, allocation = Gdk.cairo_get_clip_rectangle(cr)
x, y, w, h = allocation.x, allocation.y, allocation.width, allocation.height
area = event.area
x, y, w, h = area.x, area.y, area.width, area.height
cr = self.window.cairo_create()
# Draw no more than nessesary.
cr.rectangle(x, y, w, h)
cr.clip()
area = Rectangle(x, y, width=w, height=h)
self._painter.paint(Context(cairo=cr,
items=self.get_items_in_rectangle(area),
@ -858,13 +849,13 @@ class GtkView(Gtk.DrawingArea, Gtk.Scrollable, View):
cr.stroke()
for b in bucket._buckets:
draw_qtree_bucket(b)
cr.set_source_rgb(0, 0, .8)
cr.set_line_width(1.0)
draw_qtree_bucket(self._qtree._bucket)
return False
def do_event(self, event):
"""
Handle GDK events. Events are delegated to a `tool.Tool`.
@ -873,14 +864,13 @@ class GtkView(Gtk.DrawingArea, Gtk.Scrollable, View):
return self._tool.handle(event) and True or False
return False
def on_adjustment_changed(self, adj):
"""
Change the transformation matrix of the view to reflect the
value of the x/y adjustment (scrollbar).
"""
value = adj.get_value()
if value == 0.0:
return
if adj.value == 0.0: return
# Can not use self._matrix.translate( - adj.value , 0) here, since
# the translate method effectively does a m * self._matrix, which
@ -888,9 +878,9 @@ class GtkView(Gtk.DrawingArea, Gtk.Scrollable, View):
m = Matrix()
if adj is self._hadjustment:
m.translate(- value, 0)
m.translate( - adj.value, 0)
elif adj is self._vadjustment:
m.translate(0, - value)
m.translate(0, - adj.value)
self._matrix *= m
# Force recalculation of the bounding boxes:
@ -898,4 +888,14 @@ class GtkView(Gtk.DrawingArea, Gtk.Scrollable, View):
self.queue_draw_refresh()
# Set a signal to set adjustments. This way a ScrolledWindow can set its own
# Adjustment objects on the View. Otherwise a warning is shown:
#
# GtkWarning: gtk_scrolled_window_add(): cannot add non scrollable widget
# use gtk_scrolled_window_add_with_viewport() instead
GtkView.set_set_scroll_adjustments_signal("set-scroll-adjustments")
# vim: sw=4:et:ai

View File

@ -6,22 +6,17 @@ this file is licensed under the Python Software Foundation License, version 2.
"""
from __future__ import absolute_import
from _weakref import ref
__all__ = ['WeakSet']
class WeakSet:
def __init__(self, data=None):
self.data = set()
def _remove(item, selfref=ref(self)):
self = selfref()
if self is not None:
self.data.discard(item)
self._remove = _remove
if data is not None:
self.update(data)
@ -37,15 +32,15 @@ class WeakSet:
def __contains__(self, item):
"""
>>> class C(object): pass
>>> a = C()
>>> b = C()
>>> ws = WeakSet((a, b))
>>> a in ws
True
>>> a = C()
>>> a in ws
False
>>> class C(object): pass
>>> a = C()
>>> b = C()
>>> ws = WeakSet((a, b))
>>> a in ws
True
>>> a = C()
>>> a in ws
False
"""
return ref(item) in self.data
@ -106,7 +101,6 @@ class WeakSet:
else:
for element in other:
self.add(element)
def __ior__(self, other):
self.update(other)
return self
@ -122,7 +116,6 @@ class WeakSet:
def difference(self, other):
return self._apply(other, self.data.difference)
__sub__ = difference
def difference_update(self, other):
@ -140,19 +133,16 @@ class WeakSet:
def intersection(self, other):
return self._apply(other, self.data.intersection)
__and__ = intersection
def intersection_update(self, other):
self.data.intersection_update(ref(item) for item in other)
def __iand__(self, other):
self.data.intersection_update(ref(item) for item in other)
return self
def issubset(self, other):
return self.data.issubset(ref(item) for item in other)
__lt__ = issubset
def __le__(self, other):
@ -160,7 +150,6 @@ class WeakSet:
def issuperset(self, other):
return self.data.issuperset(ref(item) for item in other)
__gt__ = issuperset
def __ge__(self, other):

View File

@ -3,7 +3,11 @@
#tag_svn_revision = 1
[nosetests]
with-doctest=1
doctest-extension=.txt
tests=gaphas,doc
verbosity=2
detailed-errors=1
with-coverage=1
cover-package=gaphas

View File

@ -1,23 +1,4 @@
#!/usr/bin/env python
# Copyright (C) 2006-2017 Arjan Molenaar <gaphor@gmail.com>
# Dan Yeaw <dan@yeaw.me>
#
# This file is part of Gaphas.
#
# This library is free software; you can redistribute it and/or modify it under
# the terms of the GNU Library General Public License as published by the Free
# Software Foundation; either version 2 of the License, or (at your option) any
# later version.
#
# This library is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for
# more details.
#
# You should have received a copy of the GNU Library General Public License
# along with this library; if not, see <http://www.gnu.org/licenses/>.
"""
"""\
Gaphas is a MVC canvas that uses Cairo_ for rendering. One of the nicer things
of this widget is that the user (model) is not bothered with bounding box
calculations: this is all done through Cairo.
@ -34,22 +15,21 @@ Some more features:
- User interaction is handled by Tools. Tools can be chained.
- Versatile undo/redo system
GTK+ and PyGObject3_ are required.
GTK+ and PyGTK_ are required.
.. _Cairo: http://cairographics.org/
.. _PyGObject3: http://live.gnome.org/PyGObject
.. _PyGTK: http://www.pygtk.org/
"""
from __future__ import absolute_import
from setuptools import setup, find_packages
VERSION = '0.7.2'
from ez_setup import use_setuptools
VERSION = '0.8.0'
use_setuptools()
from setuptools import setup, find_packages
from distutils.cmd import Command
setup(
name='gaphas',
version=VERSION,
@ -57,47 +37,48 @@ setup(
long_description=__doc__,
classifiers=[
'Development Status :: 5 - Production/Stable',
'Environment :: X11 Applications :: GTK',
'Intended Audience :: Developers',
'License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)',
'Programming Language :: Python',
'Topic :: Software Development :: Libraries :: Python Modules',
'Development Status :: 5 - Production/Stable',
'Environment :: X11 Applications :: GTK',
'Intended Audience :: Developers',
'License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)',
'Programming Language :: Python',
'Topic :: Software Development :: Libraries :: Python Modules',
],
keywords='',
author="Arjan J. Molenaar",
author_email='gaphor@gmail.com',
author_email='arjanmol@users.sourceforge.net',
url='http://github.com/amolenaar/gaphas',
url='http://gaphor.sourceforge.net',
# download_url='http://cheeseshop.python.org/',
#download_url='http://cheeseshop.python.org/',
license='GNU Library General Public License (LGPL, see COPYING)',
packages=find_packages(exclude=['ez_setup']),
setup_requires=[
'nose >= 0.10.4',
'setuptools-git >= 0.3.4'
setup_requires = [
'nose >= 0.10.4',
'setuptools-git >= 0.3.4'
],
install_requires=[
'decorator >= 3.0.0',
'simplegeneric >= 0.6',
'six >= 1.0'
# 'pygobject >= 3.10.0'
'decorator >= 3.0.0',
'simplegeneric >= 0.6',
# 'PyGTK >= 2.8.0',
# 'cairo >= 1.8.2'
],
zip_safe=False,
package_data={
# -*- package_data: -*-
# -*- package_data: -*-
},
entry_points={
entry_points = {
},
test_suite='nose.collector',
)
test_suite = 'nose.collector',
)