Merge branch 'painter-package'
This commit is contained in:
commit
03e1b72fbf
@ -19,11 +19,11 @@ import gi
|
||||
from examples.exampleitems import Box, Circle, FatLine, PortoBox, Text
|
||||
from gaphas import Canvas, GtkView, View, state
|
||||
from gaphas.canvas import Context
|
||||
from gaphas.freehand import FreeHandPainter
|
||||
from gaphas.item import Line
|
||||
from gaphas.painter import (
|
||||
BoundingBoxPainter,
|
||||
FocusedItemPainter,
|
||||
FreeHandPainter,
|
||||
HandlePainter,
|
||||
ItemPainter,
|
||||
PainterChain,
|
||||
@ -112,12 +112,14 @@ def create_window(canvas, title, zoom=1.0): # noqa too complex
|
||||
view = GtkView()
|
||||
view.painter = (
|
||||
PainterChain()
|
||||
.append(FreeHandPainter(ItemPainter()))
|
||||
.append(HandlePainter())
|
||||
.append(FocusedItemPainter())
|
||||
.append(ToolPainter())
|
||||
.append(FreeHandPainter(ItemPainter(view)))
|
||||
.append(HandlePainter(view))
|
||||
.append(FocusedItemPainter(view))
|
||||
.append(ToolPainter(view))
|
||||
)
|
||||
view.bounding_box_painter = BoundingBoxPainter(
|
||||
FreeHandPainter(ItemPainter(view)), view
|
||||
)
|
||||
view.bounding_box_painter = BoundingBoxPainter(FreeHandPainter(ItemPainter()))
|
||||
w = Gtk.Window()
|
||||
w.set_title(title)
|
||||
w.set_default_size(400, 120)
|
||||
@ -235,7 +237,7 @@ def create_window(canvas, title, zoom=1.0): # noqa too complex
|
||||
|
||||
def on_write_demo_png_clicked(button):
|
||||
svgview = View(view.canvas)
|
||||
svgview.painter = ItemPainter()
|
||||
svgview.painter = ItemPainter(svgview)
|
||||
|
||||
# Update bounding boxes with a temporary CairoContext
|
||||
# (used for stuff like calculating font metrics)
|
||||
@ -263,7 +265,7 @@ def create_window(canvas, title, zoom=1.0): # noqa too complex
|
||||
|
||||
def on_write_demo_svg_clicked(button):
|
||||
svgview = View(view.canvas)
|
||||
svgview.painter = ItemPainter()
|
||||
svgview.painter = ItemPainter(svgview)
|
||||
|
||||
# Update bounding boxes with a temporaly CairoContext
|
||||
# (used for stuff like calculating font metrics)
|
||||
|
@ -5,7 +5,6 @@ import gi
|
||||
from examples.exampleitems import Box
|
||||
from gaphas import Canvas, GtkView
|
||||
from gaphas.item import Line
|
||||
from gaphas.painter import DefaultPainter
|
||||
|
||||
# fmt: off
|
||||
gi.require_version("Gtk", "3.0") # noqa: isort:skip
|
||||
@ -16,7 +15,6 @@ from gi.repository import Gtk # noqa: isort:skip
|
||||
def create_canvas(canvas, title):
|
||||
# Setup drawing window
|
||||
view = GtkView()
|
||||
view.painter = DefaultPainter()
|
||||
view.canvas = canvas
|
||||
window = Gtk.Window()
|
||||
window.set_title(title)
|
||||
|
@ -325,7 +325,7 @@ class ItemPaintFocused:
|
||||
self.item = item
|
||||
self.view = view
|
||||
|
||||
def paint(self, context):
|
||||
def paint(self, cairo):
|
||||
pass
|
||||
|
||||
|
||||
|
@ -291,13 +291,12 @@ class GuidedItemHandleInMotion(GuideMixin, ItemHandleInMotion):
|
||||
|
||||
@PaintFocused.register(Item)
|
||||
class GuidePainter(ItemPaintFocused):
|
||||
def paint(self, context):
|
||||
def paint(self, cr):
|
||||
try:
|
||||
guides = self.view.guides
|
||||
except AttributeError:
|
||||
return
|
||||
|
||||
cr = context.cairo
|
||||
view = self.view
|
||||
allocation = view.get_allocation()
|
||||
w, h = allocation.width, allocation.height
|
||||
|
@ -1,351 +0,0 @@
|
||||
"""The painter module provides different painters for parts of the canvas.
|
||||
|
||||
Painters can be swapped in and out.
|
||||
|
||||
Each painter takes care of a layer in the canvas (such as grid, items
|
||||
and handles).
|
||||
"""
|
||||
from cairo import ANTIALIAS_NONE, LINE_JOIN_ROUND
|
||||
|
||||
from gaphas.aspect import PaintFocused
|
||||
from gaphas.canvas import Context
|
||||
from gaphas.geometry import Rectangle
|
||||
|
||||
DEBUG_DRAW_BOUNDING_BOX = False
|
||||
|
||||
# The tolerance for Cairo. Bigger values increase speed and reduce accuracy
|
||||
# (default: 0.1)
|
||||
TOLERANCE = 0.8
|
||||
|
||||
|
||||
class Painter:
|
||||
"""Painter interface."""
|
||||
|
||||
def __init__(self, view=None):
|
||||
self.view = view
|
||||
|
||||
def set_view(self, view):
|
||||
self.view = view
|
||||
|
||||
def paint(self, context):
|
||||
"""Do the paint action (called from the View)."""
|
||||
pass
|
||||
|
||||
|
||||
class PainterChain(Painter):
|
||||
"""Chain up a set of painters.
|
||||
|
||||
like ToolChain.
|
||||
"""
|
||||
|
||||
def __init__(self, view=None):
|
||||
super().__init__(view)
|
||||
self._painters = []
|
||||
|
||||
def set_view(self, view):
|
||||
self.view = view
|
||||
for painter in self._painters:
|
||||
painter.set_view(self.view)
|
||||
|
||||
def append(self, painter):
|
||||
"""Add a painter to the list of painters."""
|
||||
self._painters.append(painter)
|
||||
painter.set_view(self.view)
|
||||
return self
|
||||
|
||||
def prepend(self, painter):
|
||||
"""Add a painter to the beginning of the list of painters."""
|
||||
self._painters.insert(0, painter)
|
||||
|
||||
def paint(self, context):
|
||||
"""See Painter.paint()."""
|
||||
for painter in self._painters:
|
||||
painter.paint(context)
|
||||
|
||||
|
||||
class DrawContext(Context):
|
||||
"""Special context for draw()'ing the item.
|
||||
|
||||
The draw-context contains stuff like the cairo context and
|
||||
properties like selected and focused.
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
|
||||
class ItemPainter(Painter):
|
||||
|
||||
draw_all = False
|
||||
|
||||
def draw_item(self, item, cairo):
|
||||
view = self.view
|
||||
cairo.save()
|
||||
try:
|
||||
cairo.set_matrix(view.matrix.to_cairo())
|
||||
cairo.transform(view.canvas.get_matrix_i2c(item).to_cairo())
|
||||
|
||||
item.draw(
|
||||
DrawContext(
|
||||
painter=self,
|
||||
cairo=cairo,
|
||||
_item=item,
|
||||
selected=(item in view.selection.selected_items),
|
||||
focused=(item is view.selection.focused_item),
|
||||
hovered=(item is view.selection.hovered_item),
|
||||
dropzone=(item is view.selection.dropzone_item),
|
||||
draw_all=self.draw_all,
|
||||
)
|
||||
)
|
||||
|
||||
finally:
|
||||
cairo.restore()
|
||||
|
||||
def draw_items(self, items, cairo):
|
||||
"""Draw the items."""
|
||||
for item in items:
|
||||
self.draw_item(item, cairo)
|
||||
if DEBUG_DRAW_BOUNDING_BOX:
|
||||
self._draw_bounds(item, cairo)
|
||||
|
||||
def _draw_bounds(self, item, cairo):
|
||||
view = self.view
|
||||
try:
|
||||
b = view.get_item_bounding_box(item)
|
||||
except KeyError:
|
||||
pass # No bounding box right now..
|
||||
else:
|
||||
cairo.save()
|
||||
cairo.identity_matrix()
|
||||
cairo.set_source_rgb(0.8, 0, 0)
|
||||
cairo.set_line_width(1.0)
|
||||
cairo.rectangle(*b)
|
||||
cairo.stroke()
|
||||
cairo.restore()
|
||||
|
||||
def paint(self, context):
|
||||
cairo = context.cairo
|
||||
cairo.set_tolerance(TOLERANCE)
|
||||
cairo.set_line_join(LINE_JOIN_ROUND)
|
||||
self.draw_items(context.items, cairo)
|
||||
|
||||
|
||||
class CairoBoundingBoxContext:
|
||||
"""Delegate all calls to the wrapped CairoBoundingBoxContext, intercept
|
||||
``stroke()``, ``fill()`` and a few others so the bounding box of the item
|
||||
involved can be calculated."""
|
||||
|
||||
def __init__(self, cairo):
|
||||
self._cairo = cairo
|
||||
self._bounds = None # a Rectangle object
|
||||
|
||||
def __getattr__(self, key):
|
||||
return getattr(self._cairo, key)
|
||||
|
||||
def get_bounds(self):
|
||||
"""Return the bounding box."""
|
||||
return self._bounds or Rectangle()
|
||||
|
||||
def _update_bounds(self, bounds):
|
||||
if bounds:
|
||||
if not self._bounds:
|
||||
self._bounds = bounds
|
||||
else:
|
||||
self._bounds += bounds
|
||||
|
||||
def _extents(self, extents_func, line_width=False):
|
||||
"""Calculate the bounding box for a given drawing operation.
|
||||
|
||||
if ``line_width`` is True, the current line-width is taken into
|
||||
account.
|
||||
"""
|
||||
cr = self._cairo
|
||||
cr.save()
|
||||
cr.identity_matrix()
|
||||
x0, y0, x1, y1 = extents_func()
|
||||
b = Rectangle(x0, y0, x1=x1, y1=y1)
|
||||
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
|
||||
d = cr.user_to_device_distance(lw, lw)
|
||||
b.expand(d[0] + d[1])
|
||||
self._update_bounds(b)
|
||||
return b
|
||||
|
||||
def fill(self, b=None):
|
||||
"""Interceptor for Cairo drawing method."""
|
||||
cr = self._cairo
|
||||
if not b:
|
||||
b = self._extents(cr.fill_extents)
|
||||
cr.fill()
|
||||
|
||||
def fill_preserve(self, b=None):
|
||||
"""Interceptor for Cairo drawing method."""
|
||||
if not b:
|
||||
cr = self._cairo
|
||||
b = self._extents(cr.fill_extents)
|
||||
|
||||
def stroke(self, b=None):
|
||||
"""Interceptor for Cairo drawing method."""
|
||||
cr = self._cairo
|
||||
if not b:
|
||||
b = self._extents(cr.stroke_extents, line_width=True)
|
||||
cr.stroke()
|
||||
|
||||
def stroke_preserve(self, b=None):
|
||||
"""Interceptor for Cairo drawing method."""
|
||||
if not b:
|
||||
cr = self._cairo
|
||||
b = self._extents(cr.stroke_extents, line_width=True)
|
||||
|
||||
def show_text(self, utf8, b=None):
|
||||
"""Interceptor for Cairo drawing method."""
|
||||
cr = self._cairo
|
||||
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])
|
||||
b = Rectangle(x0, y0, x1=x1, y1=y1)
|
||||
self._update_bounds(b)
|
||||
cr.show_text(utf8)
|
||||
|
||||
|
||||
class BoundingBoxPainter(Painter):
|
||||
"""This specific case of an ItemPainter is used to calculate the bounding
|
||||
boxes (in canvas coordinates) for the items."""
|
||||
|
||||
draw_all = True
|
||||
|
||||
def __init__(self, item_painter=None, view=None):
|
||||
super().__init__(view)
|
||||
self.item_painter = item_painter or ItemPainter(view)
|
||||
|
||||
def set_view(self, view):
|
||||
super().set_view(view)
|
||||
self.item_painter.set_view(view)
|
||||
|
||||
def draw_item(self, item, cairo):
|
||||
cairo = CairoBoundingBoxContext(cairo)
|
||||
self.item_painter.draw_item(item, cairo)
|
||||
bounds = cairo.get_bounds()
|
||||
|
||||
# Update bounding box with handles.
|
||||
view = self.view
|
||||
i2v = view.get_matrix_i2v(item).transform_point
|
||||
for h in item.handles():
|
||||
cx, cy = i2v(*h.pos)
|
||||
bounds += (cx - 5, cy - 5, 9, 9)
|
||||
|
||||
bounds.expand(1)
|
||||
view.set_item_bounding_box(item, bounds)
|
||||
|
||||
def draw_items(self, items, cairo):
|
||||
"""Draw the items."""
|
||||
for item in items:
|
||||
self.draw_item(item, cairo)
|
||||
|
||||
def paint(self, context):
|
||||
self.draw_items(context.items, context.cairo)
|
||||
|
||||
|
||||
class HandlePainter(Painter):
|
||||
"""Draw handles of items that are marked as selected in the view."""
|
||||
|
||||
def _draw_handles(self, item, cairo, opacity=None, inner=False):
|
||||
"""Draw handles for an item.
|
||||
|
||||
The handles are drawn in non-antialiased mode for clarity.
|
||||
"""
|
||||
view = self.view
|
||||
cairo.save()
|
||||
i2v = view.get_matrix_i2v(item)
|
||||
if not opacity:
|
||||
opacity = (item is view.selection.focused_item) and 0.7 or 0.4
|
||||
|
||||
cairo.set_line_width(1)
|
||||
|
||||
get_connection = view.canvas.get_connection
|
||||
for h in item.handles():
|
||||
if not h.visible:
|
||||
continue
|
||||
# connected and not being moved, see HandleTool.on_button_press
|
||||
if get_connection(h):
|
||||
r, g, b = 1.0, 0.0, 0.0
|
||||
# connected but being moved, see HandleTool.on_button_press
|
||||
elif get_connection(h):
|
||||
r, g, b = 1, 0.6, 0
|
||||
elif h.movable:
|
||||
r, g, b = 0, 1, 0
|
||||
else:
|
||||
r, g, b = 0, 0, 1
|
||||
|
||||
cairo.identity_matrix()
|
||||
cairo.set_antialias(ANTIALIAS_NONE)
|
||||
cairo.translate(*i2v.transform_point(*h.pos))
|
||||
cairo.rectangle(-4, -4, 8, 8)
|
||||
if inner:
|
||||
cairo.rectangle(-3, -3, 6, 6)
|
||||
cairo.set_source_rgba(r, g, b, opacity)
|
||||
cairo.fill_preserve()
|
||||
if h.connectable:
|
||||
cairo.move_to(-2, -2)
|
||||
cairo.line_to(2, 3)
|
||||
cairo.move_to(2, -2)
|
||||
cairo.line_to(-2, 3)
|
||||
cairo.set_source_rgba(r / 4.0, g / 4.0, b / 4.0, opacity * 1.3)
|
||||
cairo.stroke()
|
||||
cairo.restore()
|
||||
|
||||
def paint(self, context):
|
||||
view = self.view
|
||||
canvas = view.canvas
|
||||
cairo = context.cairo
|
||||
selection = view.selection
|
||||
# Order matters here:
|
||||
for item in canvas.sort(selection.selected_items):
|
||||
self._draw_handles(item, cairo)
|
||||
# Draw nice opaque handles when hovering an item:
|
||||
item = selection.hovered_item
|
||||
if item and item not in selection.selected_items:
|
||||
self._draw_handles(item, cairo, opacity=0.25)
|
||||
item = selection.dropzone_item
|
||||
if item and item not in selection.selected_items:
|
||||
self._draw_handles(item, cairo, opacity=0.25, inner=True)
|
||||
|
||||
|
||||
class ToolPainter(Painter):
|
||||
"""ToolPainter allows the Tool defined on a view to do some special
|
||||
drawing."""
|
||||
|
||||
def paint(self, context):
|
||||
view = self.view
|
||||
if view.tool:
|
||||
cairo = context.cairo
|
||||
cairo.save()
|
||||
cairo.identity_matrix()
|
||||
view.tool.draw(context)
|
||||
cairo.restore()
|
||||
|
||||
|
||||
class FocusedItemPainter(Painter):
|
||||
"""This painter allows for drawing on top of all the other layers for the
|
||||
focused item."""
|
||||
|
||||
def paint(self, context):
|
||||
view = self.view
|
||||
item = view.selection.hovered_item
|
||||
if item and item is view.selection.focused_item:
|
||||
PaintFocused(item, view).paint(context)
|
||||
|
||||
|
||||
def DefaultPainter(view=None):
|
||||
"""Default painter, containing item, handle and tool painters."""
|
||||
return (
|
||||
PainterChain(view)
|
||||
.append(ItemPainter())
|
||||
.append(HandlePainter())
|
||||
.append(FocusedItemPainter())
|
||||
.append(ToolPainter())
|
||||
)
|
19
gaphas/painter/__init__.py
Normal file
19
gaphas/painter/__init__.py
Normal file
@ -0,0 +1,19 @@
|
||||
from gaphas.painter.boundingboxpainter import BoundingBoxPainter
|
||||
from gaphas.painter.chain import PainterChain
|
||||
from gaphas.painter.focuseditempainter import FocusedItemPainter
|
||||
from gaphas.painter.freehand import FreeHandPainter
|
||||
from gaphas.painter.handlepainter import HandlePainter
|
||||
from gaphas.painter.itempainter import ItemPainter
|
||||
from gaphas.painter.painter import Painter
|
||||
from gaphas.painter.toolpainter import ToolPainter
|
||||
|
||||
|
||||
def DefaultPainter(view) -> Painter:
|
||||
"""Default painter, containing item, handle and tool painters."""
|
||||
return (
|
||||
PainterChain()
|
||||
.append(ItemPainter(view))
|
||||
.append(HandlePainter(view))
|
||||
.append(FocusedItemPainter(view))
|
||||
.append(ToolPainter(view))
|
||||
)
|
123
gaphas/painter/boundingboxpainter.py
Normal file
123
gaphas/painter/boundingboxpainter.py
Normal file
@ -0,0 +1,123 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Optional, Sequence
|
||||
|
||||
from gaphas.geometry import Rectangle
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from gaphas.item import Item
|
||||
from gaphas.painter.painter import ItemPainterType
|
||||
from gaphas.view import View
|
||||
|
||||
|
||||
class CairoBoundingBoxContext:
|
||||
"""Delegate all calls to the wrapped CairoBoundingBoxContext, intercept
|
||||
``stroke()``, ``fill()`` and a few others so the bounding box of the item
|
||||
involved can be calculated."""
|
||||
|
||||
def __init__(self, cairo):
|
||||
self._cairo = cairo
|
||||
self._bounds: Optional[Rectangle] = None # a Rectangle object
|
||||
|
||||
def __getattr__(self, key):
|
||||
return getattr(self._cairo, key)
|
||||
|
||||
def get_bounds(self):
|
||||
"""Return the bounding box."""
|
||||
return self._bounds or Rectangle()
|
||||
|
||||
def _update_bounds(self, bounds):
|
||||
if bounds:
|
||||
if not self._bounds:
|
||||
self._bounds = bounds
|
||||
else:
|
||||
self._bounds += bounds
|
||||
|
||||
def _extents(self, extents_func, line_width=False):
|
||||
"""Calculate the bounding box for a given drawing operation.
|
||||
|
||||
if ``line_width`` is True, the current line-width is taken into
|
||||
account.
|
||||
"""
|
||||
cr = self._cairo
|
||||
cr.save()
|
||||
cr.identity_matrix()
|
||||
x0, y0, x1, y1 = extents_func()
|
||||
b = Rectangle(x0, y0, x1=x1, y1=y1)
|
||||
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
|
||||
d = cr.user_to_device_distance(lw, lw)
|
||||
b.expand(d[0] + d[1])
|
||||
self._update_bounds(b)
|
||||
return b
|
||||
|
||||
def fill(self, b=None):
|
||||
"""Interceptor for Cairo drawing method."""
|
||||
cr = self._cairo
|
||||
if not b:
|
||||
b = self._extents(cr.fill_extents)
|
||||
cr.fill()
|
||||
|
||||
def fill_preserve(self, b=None):
|
||||
"""Interceptor for Cairo drawing method."""
|
||||
if not b:
|
||||
cr = self._cairo
|
||||
b = self._extents(cr.fill_extents)
|
||||
|
||||
def stroke(self, b=None):
|
||||
"""Interceptor for Cairo drawing method."""
|
||||
cr = self._cairo
|
||||
if not b:
|
||||
b = self._extents(cr.stroke_extents, line_width=True)
|
||||
cr.stroke()
|
||||
|
||||
def stroke_preserve(self, b=None):
|
||||
"""Interceptor for Cairo drawing method."""
|
||||
if not b:
|
||||
cr = self._cairo
|
||||
b = self._extents(cr.stroke_extents, line_width=True)
|
||||
|
||||
def show_text(self, utf8, b=None):
|
||||
"""Interceptor for Cairo drawing method."""
|
||||
cr = self._cairo
|
||||
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])
|
||||
b = Rectangle(x0, y0, x1=x1, y1=y1)
|
||||
self._update_bounds(b)
|
||||
cr.show_text(utf8)
|
||||
|
||||
|
||||
class BoundingBoxPainter:
|
||||
"""This specific case of an ItemPainter is used to calculate the bounding
|
||||
boxes (in canvas coordinates) for the items."""
|
||||
|
||||
draw_all = True
|
||||
|
||||
def __init__(self, item_painter: ItemPainterType, view: View):
|
||||
self.item_painter = item_painter
|
||||
self.view = view
|
||||
|
||||
def paint_item(self, item, cairo):
|
||||
cairo = CairoBoundingBoxContext(cairo)
|
||||
self.item_painter.paint_item(item, cairo)
|
||||
bounds = cairo.get_bounds()
|
||||
|
||||
# Update bounding box with handles.
|
||||
view = self.view
|
||||
i2v = view.get_matrix_i2v(item).transform_point
|
||||
for h in item.handles():
|
||||
cx, cy = i2v(*h.pos)
|
||||
bounds += (cx - 5, cy - 5, 9, 9)
|
||||
|
||||
bounds.expand(1)
|
||||
view.set_item_bounding_box(item, bounds)
|
||||
|
||||
def paint(self, items: Sequence[Item], cairo):
|
||||
"""Draw the items."""
|
||||
for item in items:
|
||||
self.paint_item(item, cairo)
|
31
gaphas/painter/chain.py
Normal file
31
gaphas/painter/chain.py
Normal file
@ -0,0 +1,31 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, Sequence
|
||||
|
||||
from gaphas.item import Item
|
||||
from gaphas.painter.painter import Painter
|
||||
|
||||
|
||||
class PainterChain:
|
||||
"""Chain up a set of painters.
|
||||
|
||||
like ToolChain.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._painters: List[Painter] = []
|
||||
|
||||
def append(self, painter: Painter) -> PainterChain:
|
||||
"""Add a painter to the list of painters."""
|
||||
self._painters.append(painter)
|
||||
return self
|
||||
|
||||
def prepend(self, painter: Painter) -> PainterChain:
|
||||
"""Add a painter to the beginning of the list of painters."""
|
||||
self._painters.insert(0, painter)
|
||||
return self
|
||||
|
||||
def paint(self, items: Sequence[Item], cairo):
|
||||
"""See Painter.paint()."""
|
||||
for painter in self._painters:
|
||||
painter.paint(items, cairo)
|
19
gaphas/painter/focuseditempainter.py
Normal file
19
gaphas/painter/focuseditempainter.py
Normal file
@ -0,0 +1,19 @@
|
||||
from typing import Sequence
|
||||
|
||||
from gaphas.aspect import PaintFocused
|
||||
from gaphas.item import Item
|
||||
|
||||
|
||||
class FocusedItemPainter:
|
||||
"""This painter allows for drawing on top of all the other layers for the
|
||||
focused item."""
|
||||
|
||||
def __init__(self, view):
|
||||
assert view
|
||||
self.view = view
|
||||
|
||||
def paint(self, items: Sequence[Item], cairo):
|
||||
view = self.view
|
||||
item = view.selection.hovered_item
|
||||
if item and item is view.selection.focused_item:
|
||||
PaintFocused(item, view).paint(cairo)
|
@ -11,8 +11,10 @@ See: http://stevehanov.ca/blog/index.php?id=33 and
|
||||
"""
|
||||
from math import sqrt
|
||||
from random import Random
|
||||
from typing import Sequence
|
||||
|
||||
from gaphas.painter import Context, Painter
|
||||
from gaphas.item import Item
|
||||
from gaphas.painter.painter import ItemPainterType
|
||||
|
||||
|
||||
class FreeHandCairoContext:
|
||||
@ -131,23 +133,16 @@ class FreeHandCairoContext:
|
||||
self.close_path()
|
||||
|
||||
|
||||
class FreeHandPainter(Painter):
|
||||
def __init__(self, subpainter, sloppiness=1.0, view=None):
|
||||
class FreeHandPainter:
|
||||
def __init__(self, subpainter: ItemPainterType, sloppiness=1.0):
|
||||
self.subpainter = subpainter
|
||||
self.sloppiness = sloppiness
|
||||
if view:
|
||||
self.set_view(view)
|
||||
|
||||
def set_view(self, view):
|
||||
self.subpainter.set_view(view)
|
||||
def paint_item(self, item: Item, cairo):
|
||||
# Bounding painter requires painting per item
|
||||
self.subpainter.paint_item(item, cairo)
|
||||
|
||||
def draw_item(self, item, cairo):
|
||||
# Bounding box painter requires painting per item
|
||||
self.subpainter.draw_item(item, cairo)
|
||||
|
||||
def paint(self, context):
|
||||
subcontext = Context(
|
||||
cairo=FreeHandCairoContext(context.cairo, self.sloppiness),
|
||||
items=context.items,
|
||||
def paint(self, items: Sequence[Item], cairo):
|
||||
self.subpainter.paint(
|
||||
items, FreeHandCairoContext(cairo, self.sloppiness),
|
||||
)
|
||||
self.subpainter.paint(subcontext)
|
73
gaphas/painter/handlepainter.py
Normal file
73
gaphas/painter/handlepainter.py
Normal file
@ -0,0 +1,73 @@
|
||||
from typing import Sequence
|
||||
|
||||
from cairo import ANTIALIAS_NONE
|
||||
|
||||
from gaphas.item import Item
|
||||
|
||||
|
||||
class HandlePainter:
|
||||
"""Draw handles of items that are marked as selected in the view."""
|
||||
|
||||
def __init__(self, view):
|
||||
assert view
|
||||
self.view = view
|
||||
|
||||
def _draw_handles(self, item, cairo, opacity=None, inner=False):
|
||||
"""Draw handles for an item.
|
||||
|
||||
The handles are drawn in non-antialiased mode for clarity.
|
||||
"""
|
||||
view = self.view
|
||||
cairo.save()
|
||||
i2v = view.get_matrix_i2v(item)
|
||||
if not opacity:
|
||||
opacity = (item is view.selection.focused_item) and 0.7 or 0.4
|
||||
|
||||
cairo.set_line_width(1)
|
||||
|
||||
get_connection = view.canvas.get_connection
|
||||
for h in item.handles():
|
||||
if not h.visible:
|
||||
continue
|
||||
# connected and not being moved, see HandleTool.on_button_press
|
||||
if get_connection(h):
|
||||
r, g, b = 1.0, 0.0, 0.0
|
||||
# connected but being moved, see HandleTool.on_button_press
|
||||
elif get_connection(h):
|
||||
r, g, b = 1, 0.6, 0
|
||||
elif h.movable:
|
||||
r, g, b = 0, 1, 0
|
||||
else:
|
||||
r, g, b = 0, 0, 1
|
||||
|
||||
cairo.identity_matrix()
|
||||
cairo.set_antialias(ANTIALIAS_NONE)
|
||||
cairo.translate(*i2v.transform_point(*h.pos))
|
||||
cairo.rectangle(-4, -4, 8, 8)
|
||||
if inner:
|
||||
cairo.rectangle(-3, -3, 6, 6)
|
||||
cairo.set_source_rgba(r, g, b, opacity)
|
||||
cairo.fill_preserve()
|
||||
if h.connectable:
|
||||
cairo.move_to(-2, -2)
|
||||
cairo.line_to(2, 3)
|
||||
cairo.move_to(2, -2)
|
||||
cairo.line_to(-2, 3)
|
||||
cairo.set_source_rgba(r / 4.0, g / 4.0, b / 4.0, opacity * 1.3)
|
||||
cairo.stroke()
|
||||
cairo.restore()
|
||||
|
||||
def paint(self, items: Sequence[Item], cairo):
|
||||
view = self.view
|
||||
canvas = view.canvas
|
||||
selection = view.selection
|
||||
# Order matters here:
|
||||
for item in canvas.sort(selection.selected_items):
|
||||
self._draw_handles(item, cairo)
|
||||
# Draw nice opaque handles when hovering an item:
|
||||
item = selection.hovered_item
|
||||
if item and item not in selection.selected_items:
|
||||
self._draw_handles(item, cairo, opacity=0.25)
|
||||
item = selection.dropzone_item
|
||||
if item and item not in selection.selected_items:
|
||||
self._draw_handles(item, cairo, opacity=0.25, inner=True)
|
80
gaphas/painter/itempainter.py
Normal file
80
gaphas/painter/itempainter.py
Normal file
@ -0,0 +1,80 @@
|
||||
from typing import Sequence
|
||||
|
||||
from cairo import LINE_JOIN_ROUND
|
||||
|
||||
from gaphas.canvas import Context
|
||||
from gaphas.item import Item
|
||||
|
||||
DEBUG_DRAW_BOUNDING_BOX = False
|
||||
|
||||
# The tolerance for Cairo. Bigger values increase speed and reduce accuracy
|
||||
# (default: 0.1)
|
||||
TOLERANCE = 0.8
|
||||
|
||||
|
||||
class DrawContext(Context):
|
||||
"""Special context for draw()'ing the item.
|
||||
|
||||
The draw-context contains stuff like the cairo context and
|
||||
properties like selected and focused.
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
|
||||
class ItemPainter:
|
||||
|
||||
draw_all = False
|
||||
|
||||
def __init__(self, view):
|
||||
assert view
|
||||
self.view = view
|
||||
|
||||
def paint_item(self, item, cairo):
|
||||
view = self.view
|
||||
cairo.save()
|
||||
try:
|
||||
cairo.set_matrix(view.matrix.to_cairo())
|
||||
cairo.transform(view.canvas.get_matrix_i2c(item).to_cairo())
|
||||
|
||||
item.draw(
|
||||
DrawContext(
|
||||
painter=self,
|
||||
cairo=cairo,
|
||||
_item=item,
|
||||
selected=(item in view.selection.selected_items),
|
||||
focused=(item is view.selection.focused_item),
|
||||
hovered=(item is view.selection.hovered_item),
|
||||
dropzone=(item is view.selection.dropzone_item),
|
||||
draw_all=self.draw_all,
|
||||
)
|
||||
)
|
||||
|
||||
finally:
|
||||
cairo.restore()
|
||||
|
||||
def _draw_bounds(self, item, cairo):
|
||||
view = self.view
|
||||
try:
|
||||
b = view.get_item_bounding_box(item)
|
||||
except KeyError:
|
||||
pass # No bounding box right now..
|
||||
else:
|
||||
cairo.save()
|
||||
cairo.identity_matrix()
|
||||
cairo.set_source_rgb(0.8, 0, 0)
|
||||
cairo.set_line_width(1.0)
|
||||
cairo.rectangle(*b)
|
||||
cairo.stroke()
|
||||
cairo.restore()
|
||||
|
||||
def paint(self, items: Sequence[Item], cairo):
|
||||
"""Draw the items."""
|
||||
cairo.set_tolerance(TOLERANCE)
|
||||
cairo.set_line_join(LINE_JOIN_ROUND)
|
||||
|
||||
for item in items:
|
||||
self.paint_item(item, cairo)
|
||||
if DEBUG_DRAW_BOUNDING_BOX:
|
||||
self._draw_bounds(item, cairo)
|
30
gaphas/painter/painter.py
Normal file
30
gaphas/painter/painter.py
Normal file
@ -0,0 +1,30 @@
|
||||
"""The painter module provides different painters for parts of the canvas.
|
||||
|
||||
Painters can be swapped in and out.
|
||||
|
||||
Each painter takes care of a layer in the canvas (such as grid, items
|
||||
and handles).
|
||||
"""
|
||||
|
||||
from typing import Sequence
|
||||
|
||||
from typing_extensions import Protocol
|
||||
|
||||
from gaphas.item import Item
|
||||
|
||||
|
||||
class Painter(Protocol):
|
||||
"""Painter interface."""
|
||||
|
||||
def paint(self, items: Sequence[Item], cairo):
|
||||
"""Do the paint action (called from the View)."""
|
||||
pass
|
||||
|
||||
|
||||
class ItemPainterType(Protocol):
|
||||
def paint_item(self, item: Item, cairo):
|
||||
"""Draw a single item."""
|
||||
|
||||
def paint(self, items: Sequence[Item], cairo):
|
||||
"""Do the paint action (called from the View)."""
|
||||
pass
|
21
gaphas/painter/toolpainter.py
Normal file
21
gaphas/painter/toolpainter.py
Normal file
@ -0,0 +1,21 @@
|
||||
from typing import Sequence
|
||||
|
||||
from gaphas.canvas import Context
|
||||
from gaphas.item import Item
|
||||
|
||||
|
||||
class ToolPainter:
|
||||
"""ToolPainter allows the Tool defined on a view to do some special
|
||||
drawing."""
|
||||
|
||||
def __init__(self, view):
|
||||
assert view
|
||||
self.view = view
|
||||
|
||||
def paint(self, items: Sequence[Item], cairo):
|
||||
view = self.view
|
||||
if view.tool:
|
||||
cairo.save()
|
||||
cairo.identity_matrix()
|
||||
view.tool.draw(Context(items=items, cairo=cairo))
|
||||
cairo.restore()
|
@ -238,11 +238,10 @@ class LineSegmentPainter(ItemPaintFocused):
|
||||
required for this feature.
|
||||
"""
|
||||
|
||||
def paint(self, context):
|
||||
def paint(self, cr):
|
||||
view = self.view
|
||||
item = view.selection.hovered_item
|
||||
if item and item is view.selection.focused_item:
|
||||
cr = context.cairo
|
||||
h = item.handles()
|
||||
for h1, h2 in zip(h[:-1], h[1:]):
|
||||
p1, p2 = h1.pos, h2.pos
|
||||
|
@ -5,7 +5,7 @@ from typing import Optional, Set
|
||||
import cairo
|
||||
from gi.repository import Gdk, GLib, GObject, Gtk
|
||||
|
||||
from gaphas.canvas import Canvas, Context, instant_cairo_context
|
||||
from gaphas.canvas import Canvas, instant_cairo_context
|
||||
from gaphas.decorators import AsyncIO
|
||||
from gaphas.geometry import Rectangle, distance_point_point_fast
|
||||
from gaphas.item import Item
|
||||
@ -471,7 +471,7 @@ class GtkView(Gtk.DrawingArea, Gtk.Scrollable, View):
|
||||
(0, 0, allocation.width, allocation.height)
|
||||
)
|
||||
|
||||
self.painter.paint(Context(cairo=cr, items=items))
|
||||
self.painter.paint(items, cr)
|
||||
|
||||
if DEBUG_DRAW_BOUNDING_BOX:
|
||||
cr.save()
|
||||
|
@ -1,10 +1,10 @@
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from gaphas.canvas import Canvas, Context
|
||||
from gaphas.canvas import Canvas
|
||||
from gaphas.geometry import Rectangle
|
||||
from gaphas.item import Item
|
||||
from gaphas.matrix import Matrix
|
||||
from gaphas.painter import BoundingBoxPainter, DefaultPainter, ItemPainter
|
||||
from gaphas.painter import BoundingBoxPainter, DefaultPainter, ItemPainter, Painter
|
||||
from gaphas.quadtree import Quadtree
|
||||
|
||||
|
||||
@ -13,8 +13,10 @@ class View:
|
||||
|
||||
def __init__(self, canvas=None):
|
||||
self._matrix = Matrix()
|
||||
self._painter = DefaultPainter(self)
|
||||
self._bounding_box_painter = BoundingBoxPainter(ItemPainter(self), self)
|
||||
self._painter: Painter = DefaultPainter(self)
|
||||
self._bounding_box_painter: Painter = BoundingBoxPainter(
|
||||
ItemPainter(self), self
|
||||
)
|
||||
|
||||
self._qtree: Quadtree[Item, Tuple[float, float, float, float]] = Quadtree()
|
||||
|
||||
@ -39,20 +41,18 @@ class View:
|
||||
|
||||
canvas = property(lambda s: s._canvas, _set_canvas)
|
||||
|
||||
def _set_painter(self, painter):
|
||||
def _set_painter(self, painter: Painter):
|
||||
"""Set the painter to use.
|
||||
|
||||
Painters should implement painter.Painter.
|
||||
"""
|
||||
self._painter = painter
|
||||
painter.set_view(self)
|
||||
|
||||
painter = property(lambda s: s._painter, _set_painter)
|
||||
|
||||
def _set_bounding_box_painter(self, painter):
|
||||
def _set_bounding_box_painter(self, painter: Painter):
|
||||
"""Set the painter to use for bounding box calculations."""
|
||||
self._bounding_box_painter = painter
|
||||
painter.set_view(self)
|
||||
|
||||
bounding_box_painter = property(
|
||||
lambda s: s._bounding_box_painter, _set_bounding_box_painter
|
||||
@ -151,7 +151,7 @@ class View:
|
||||
items = self.canvas.get_all_items()
|
||||
|
||||
# The painter calls set_item_bounding_box() for each rendered item.
|
||||
painter.paint(Context(cairo=cr, items=items))
|
||||
painter.paint(items, cr)
|
||||
|
||||
def get_matrix_i2v(self, item):
|
||||
"""Get Item to View matrix for ``item``."""
|
||||
|
4
poetry.lock
generated
4
poetry.lock
generated
@ -399,7 +399,7 @@ python-versions = "*"
|
||||
name = "typing-extensions"
|
||||
version = "3.7.4.3"
|
||||
description = "Backported and Experimental Type Hints for Python 3.5+"
|
||||
category = "dev"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
@ -438,7 +438,7 @@ testing = ["jaraco.itertools", "func-timeout"]
|
||||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.6"
|
||||
content-hash = "c9f4a89ddb95ef0dd32d318103d592314f44986c3f583cf199de777d6bd3e92f"
|
||||
content-hash = "49eac21141f96b333fb34b2b1a1331bb93e58a18f77aae9d89f4f8adad481778"
|
||||
|
||||
[metadata.files]
|
||||
appdirs = [
|
||||
|
@ -34,6 +34,7 @@ python = "^3.6"
|
||||
PyGObject = "^3.20.0"
|
||||
pycairo = "^1.13.0"
|
||||
importlib_metadata = ">=1.3,<3.0"
|
||||
typing-extensions = "^3.7.4"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
pytest = "^6.1"
|
||||
|
@ -1,6 +1,6 @@
|
||||
import cairo
|
||||
|
||||
from gaphas.freehand import FreeHandCairoContext
|
||||
from gaphas.painter.freehand import FreeHandCairoContext
|
||||
|
||||
|
||||
def test_drawing_lines():
|
||||
|
Loading…
x
Reference in New Issue
Block a user