library/python/workbench/graphics/canvas.py (487 lines of code) (raw):
# Copyright (c) 2013, 2019, Oracle and/or its affiliates. All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License, version 2.0,
# as published by the Free Software Foundation.
#
# This program is designed to work with certain software (including
# but not limited to OpenSSL) that is licensed under separate terms, as
# designated in a particular file or component or in included license
# documentation. The authors of MySQL hereby grant you an additional
# permission to link the program and your derivative works with the
# separately licensed software that they have either included with
# the program or referenced in the documentation.
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
# the GNU General Public License, version 2.0, for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
#import cairo
from . import cairo_utils
def draw_varrow(cr, tip_pos, ah = 4, aw = 4):
cr.new_path()
x, y = tip_pos
cr.move_to(x, y)
cr.line_to(x - aw/2, y + ah - 0.5)
cr.line_to(x + aw/2, y + ah - 0.5)
cr.close_path()
def draw_harrow(cr, tip_pos, ah = 4, aw = 4):
cr.new_path()
x, y = tip_pos
cr.move_to(x, y)
cr.line_to(x - ah - 0.5, y - aw/2)
cr.line_to(x - ah - 0.5, y + aw/2)
cr.close_path()
#
class Settings:
default_font = "Helvetica"
settings = Settings()
class Canvas(object):
def __init__(self, set_needs_repaint_cb):
self._size = (0, 0)
self.tooltip = None
self.set_needs_repaint = set_needs_repaint_cb
self._figures = []
self._mouse_down_figures = [None] * 5
self._hover_figure = None
self._background_color = (1,1,1)
def deactivate(self):
if self.tooltip:
self.tooltip.close()
self.tooltip = None
def activate(self):
pass
def set_background_color(self, r, g, b):
self._background_color = r, g, b
def add(self, obj):
obj.set_canvas(self)
self._figures.append(obj)
obj.invalidate()
def remove(self, obj):
self.invalidate(*obj.bounds)
self.set_needs_repaint()
obj.set_canvas(None)
self._figures.remove(obj)
def repaint(self, cr, x, y, w, h):
cr.set_source_rgb(*self._background_color)
cr.rectangle(0, 0, w, h)
cr.fill()
cr.translate(x, y)
for f in self._figures:
f.relayout(cr)
f.render(cr)
def invalidate(self, x, y, w, h):
self.set_needs_repaint(x, y, w, h)
def figure_at(self, x, y):
for f in self._figures:
if f.contains_point(x, y):
return f
return None
# event handling
def mouse_down(self, button, x, y):
fig = self.figure_at(x, y)
if fig:
self._mouse_down_figures[button] = fig
fig.mouse_down(button, x, y)
return True
return False
def mouse_up(self, button, x, y):
if self._mouse_down_figures[button]:
self._mouse_down_figures[button].mouse_up(button, x, y)
self._mouse_down_figures[button] = None
def mouse_leave(self):
#if self.tooltip:
# self.tooltip.close()
# self.tooltip = None
pass
def mouse_move(self, x, y):
dragged = False
for b, f in enumerate(self._mouse_down_figures):
if f:
dragged = True
f.mouse_drag(b, x, y)
break
if not dragged:
fig = self.figure_at(x, y)
if fig != self._hover_figure:
if self._hover_figure:
self._hover_figure.hover_out(x, y)
if fig:
fig.hover_in(x, y)
self._hover_figure = fig
else:
if self._hover_figure:
self._hover_figure.hover(x, y)
if self._hover_figure:
self._hover_figure.mouse_move(x, y)
# Base Classes
class Element(object):
def __init__(self):
self.canvas = None
self.parent = None
self._layout_dirty = False
self._color = (0, 0, 0, 1)
self.on_hover_in = None
self.on_hover_out = None
def set_canvas(self, canvas):
self.canvas = canvas
def invalidate(self, repaint_only = False):
if not repaint_only:
self._layout_dirty = True
if self.canvas:
self.canvas.invalidate(*self.bounds)
def relayout(self, cr):
if self._layout_dirty:
self.do_relayout(cr)
self._layout_dirty = False
def do_relayout(self, cr):
pass
def render(self, cr):
pass
def set_color(self, r, g, b, a = 1.0):
self._color = (r, g, b, a)
# event handling
def mouse_down(self, button, x, y):
pass
def mouse_up(self, button, x, y):
pass
def mouse_drag(self, button, x, y):
pass
def mouse_move(self, x, y):
pass
def hover_in(self, x, y):
if self.on_hover_in:
self.on_hover_in(self, x, y)
def hover_out(self, x, y):
if self.on_hover_out:
self.on_hover_out(self, x, y)
def hover(self, x, y):
pass
HFill = 1 << 0
VFill = 1 << 1
class Figure(Element):
def __init__(self):
Element.__init__(self)
self._x = 0
self._y = 0
self._width = 0
self._height = 0
self._uwidth = None
self._uheight = None
self._fill_color = (1, 1, 1, 1)
self._line_width = 1
self._dash = (None, None)
self._padding = (0, 0, 0, 0)
self._layout_flags = HFill | VFill
@property
def x(self):
return self._x
@property
def y(self):
return self._y
@property
def root_x(self):
return self.x + (self.parent.root_x if self.parent else 0)
@property
def root_y(self):
return self.y + (self.parent.root_y if self.parent else 0)
@property
def width(self):
return self._width if self._uwidth is None else self._uwidth
@property
def height(self):
return self._height if self._uheight is None else self._uheight
@property
def pos(self):
return (self._x, self._y)
@property
def size(self):
return (self.width, self.height)
@property
def bounds(self):
return 0, 0, self.width, self.height
@property
def frame(self):
return self.x, self.y, self.width, self.height
@property
def padding_top(self):
return self._padding[0]
@property
def padding_left(self):
return self._padding[1]
@property
def padding_bottom(self):
return self._padding[2]
@property
def padding_right(self):
return self._padding[3]
def move(self, x, y):
if self.canvas:
self.canvas.invalidate(*self.frame)
self._x = x
self._y = y
if self.canvas:
self.canvas.invalidate(*self.frame)
def contains_point(self, x, y):
pos = self.pos
size = self.size
return x >= pos[0] and x < pos[0] + size[0] and y >= pos[1] and y < pos[1] + size[1]
def center(self):
x, y = self.pos
w, h = self.size
return int(x + w/2), int(y + h/2)
def top_vertex(self):
x, y = self.pos
w, h = self.size
return (x, y), (x+w, y)
def bottom_vertex(self):
x, y = self.pos
w, h = self.size
return (x, y+h), (x+w, y+h)
def right_vertex(self):
x, y = self.pos
w, h = self.size
return (x+w, y), (x+w, y+h)
def left_vertex(self):
x, y = self.pos
w, h = self.size
return (x, y), (x, y+h)
def set_padding(self, t, l, b, r):
self._padding = t, l, b, r
def set_layout_flags(self, flags):
self._layout_flags = flags
def set_line_dash(self, dash, offset):
self._dash = (dash, offset)
self.invalidate()
def set_line_width(self, l):
self._line_width = l
self.invalidate()
def set_fill_color(self, r, g, b, a = 1.0):
self._fill_color = (r, g, b, a)
self.invalidate()
def apply_attributes(self, c):
c.set_source_rgba(*self._color)
if self._dash[0]:
c.set_dash(self._dash[0], self._dash[1])
c.set_line_width(self._line_width)
def apply_fill_attributes(self, c):
c.set_source_rgba(*self._fill_color)
def set_usize(self, w, h):
self._uwidth = w
self._uheight = h
class Line(object):
def __init__(self):
Element.__init__(self)
self._end1 = None
self._end2 = None
@property
def bounds(self):
return 0, 0, 0, 0
def contains_point(self, x, y):
return None
#
#
class Container(Figure):
def __init__(self):
Figure.__init__(self)
self._items = []
def add(self, item):
item.parent = self
self._items.append(item)
def remove(self, item):
item.parent = None
self._items.remove(item)
def render(self, cr):
cr.save()
cr.translate(self.x, self.y)
for i in self._items:
i.render(cr)
cr.restore()
class VBoxFigure(Container):
def __init__(self):
Container.__init__(self)
self._spacing = 0
def set_spacing(self, sp):
self._spacing = sp
def do_relayout(self, cr):
lp, tp, rp, bp = self._padding
y = tp
x = lp
max_width = self._width
for i in self._items:
i.do_relayout(cr)
i.move(x, y)
max_width = max(max_width, i.width)
y += int(i.height) + self._spacing
if self._items:
y -= self._spacing
for i in self._items:
if i._layout_flags & HFill:
i.set_usize(max_width, i._uheight)
i.do_relayout(cr)
else:
i.move((max_width - i.width) / 2, i.y)
self._width = max_width + lp + rp
self._height = y + bp
self.invalidate()
#
#
class TextFigure(Figure):
def __init__(self, text=""):
Figure.__init__(self)
self._text = text
self._font = settings.default_font
self._font_size = 12
self._text_color = (0, 0, 0, 1)
self._line_spacing = 2
self._bold = False
self._xalignment = 0.0
self._yalignment = 0.0
self._line_height = 14
self._text_height = 0
def set_text_color(self, r, g, b, a=1.0):
self._text_color = r, g, b, a
self.invalidate()
def set_font_size(self, s):
self._font_size = s
def set_font_bold(self, s):
self._bold = s
def set_alignment(self, x, y):
self._xalignment = x
self._yalignment = y
self.invalidate(False)
def set_text(self, text):
self._text = text
self.invalidate(False)
def do_relayout(self, ctx):
ctx.save()
ctx.set_font(self._font, False, self._bold)
ctx.set_font_size(self._font_size)
if "\n" in self._text:
lines = self._text.split("\n")
w, h = 0, 0
lh = 0
self._text_height = 0
for line in lines:
ext = ctx.text_extents(line)
w = max(w, int(ext.x_bearing + ext.x_advance))
lh = max(lh, int(ext.height + ext.height + ext.y_bearing))
self._text_height += ext.height + self._line_spacing
if lines:
self._text_height -= self._line_spacing
self._line_height = int(lh)
t, l, b, r = self._padding
self._width, self._height = (w + r+l, self._text_height + t+b)
else:
ext = ctx.text_extents(self._text)
self._extents = ext
t, l, b, r = self._padding
self._line_height = int(ext.height) + int(ext.height + ext.y_bearing)
self._width = int(ext.x_bearing + ext.x_advance) + r+l
self._height = self._line_height + t+b
self._text_height = self._line_height
ctx.restore()
def render(self, ctx):
ctx.save()
ctx.translate(self.x, self.y)
ctx.set_source_rgba(*self._text_color)
ctx.set_font(self._font, False, self._bold)
ctx.set_font_size(self._font_size)
t, l, b, r = self._padding
w, h = self.size
x = int(l) + 0.5
y = int(t) + 0.5 + int((self.height - t - b - self._text_height) * self._yalignment)
for line in self._text.split("\n"):
extents = ctx.text_extents(line)
ctx.move_to(x + int((self.width - l - r - extents.width) * self._xalignment),
y + int(extents.height - (extents.height + extents.y_bearing)))
ctx.show_text(line)
y += extents.height + self._line_spacing
ctx.stroke()
ctx.restore()
class ImageFigure(Figure):
def __init__(self, file):
Figure.__init__(self)
self.set_line_width(1)
# if this is a @2x image (for hidpi), we need to scale it by half when painting on screen
# XXX: this implementation is nonsense, we never specify a @2x image (that is handled implicitly).
if "@2x" in file:
self._scale = 0.5
else:
self._scale = 1.0
self._file = file
self._image = cairo_utils.ImageSurface.from_png(file)
self._width = self._image.get_width() * self._scale
self._height = self._image.get_height() * self._scale
def set_image_file(self, path):
self._image = cairo_utils.ImageSurface.from_png(path)
self._width = self._image.get_width() * self._scale
self._height = self._image.get_height() * self._scale
def switch_image_mode(self, dark):
if dark:
self._file = self._file.replace("light", "dark")
else:
self._file = self._file.replace("dark", "light")
self._image = cairo_utils.ImageSurface.from_png(self._file)
def render(self, ctx):
ctx.save()
ctx.translate(self.x, self.y)
if self._scale != 1.0:
ctx.scale(self._scale, self._scale)
ctx.set_source_surface(self._image, 0, 0)
ctx.paint()
ctx.restore()
class ShapeFigure(TextFigure):
def __init__(self, caption):
TextFigure.__init__(self, caption)
self.set_alignment(0.5, 0.5)
self.set_line_width(1)
def render(self, ctx):
ctx.save()
ctx.translate(self.x, self.y)
self.make_path(ctx)
self.apply_fill_attributes(ctx)
ctx.fill_preserve()
self.apply_attributes(ctx)
ctx.stroke()
ctx.restore()
TextFigure.render(self, ctx)
def make_path(self, ctx):
ctx.rectangle(*self.bounds)
class DiamondShapeFigure(ShapeFigure):
def make_path(self, ctx):
ctx.move_to(0, self.height/2)
ctx.line_to(self.width/2, 0)
ctx.line_to(self.width, self.height/2)
ctx.line_to(self.width/2, self.height)
ctx.close_path()
class RectangleShapeFigure(ShapeFigure):
_corner_radius = 0
def make_path(self, ctx):
if self._corner_radius == 0:
ctx.rectangle(0.5, 0.5, self.width, self.height)
else:
ctx.rounded_rect(0.5, 0.5, self.width, self.height, self._corner_radius)
def set_corner_radius(self, r):
self._corner_radius = r
#
#
class HoverTip(Figure):
def __init__(self, text):
Figure.__init__(self)
self.text = text
self.font_size = 12
self.bold = False
self.line_spacing = 4
self.line_height = 10
self.padding = (4, 4, 4, 4)
def calc(self, ctx):
ctx.save()
ctx.set_font_size(self.font_size)
if self.bold:
ctx.set_font("Helvetica", False, self.bold)
if "\n" in self.text:
lines = self.text.split("\n")
w, h = 0, 0
lh = 0
for line in lines:
ext = ctx.text_extents(line)
w = max(w, int(ext.x_bearing + ext.x_advance))
lh = max(lh, int(ext.height + (ext.height + ext.y_advance + ext.y_bearing)))
self.line_height = lh
h = lh * len(lines) + self.line_spacing * (len(lines)-1)
t, l, b, r = self.padding
self._width, self._height = (w + r+l, h + t+b)
else:
ext = ctx.text_extents(self.text)
self._extents = ext
t, l, b, r = self.padding
self.line_height = int(ext.height + (ext.height + ext.y_advance + ext.y_bearing))
self._width = int(ext.x_bearing + ext.x_advance) + r+l
self._height = self.line_height + t+b
ctx.restore()
def render(self, ctx):
ctx.save()
ctx.rectangle(*self.bounds)
ctx.set_source_rgb(0.9, 0.9, 0.8)
ctx.stroke_preserve()
ctx.set_source_rgb(1, 1, 0.9)
ctx.fill()
ctx.set_source_rgb(0.2, 0.2, 0.2)
ctx.set_font_size(self.font_size)
if self.bold:
ctx.set_font("Helvetica", False, self.bold)
t, l, b, r = self.padding
x, y = self.pos
w, h = self.size
ctx.show_text_lines_at(int(x+l)+0.5, int(y+t)+0.5, self.text, self.line_spacing, self.line_height)
ctx.stroke()
ctx.restore()
def do_relayout(self, cr):
self.calc(cr)