From 7875a59124489397f661684c1fc40c5d18009234 Mon Sep 17 00:00:00 2001 From: Arjan Molenaar Date: Thu, 15 Dec 2022 22:58:22 +0100 Subject: [PATCH 1/4] Implement bigger handles --- gaphas/painter/handlepainter.py | 64 ++++++++++++++++++++++----------- gaphas/segment.py | 16 ++------- 2 files changed, 46 insertions(+), 34 deletions(-) diff --git a/gaphas/painter/handlepainter.py b/gaphas/painter/handlepainter.py index c57813ac..c80566c3 100644 --- a/gaphas/painter/handlepainter.py +++ b/gaphas/painter/handlepainter.py @@ -2,7 +2,6 @@ from typing import TYPE_CHECKING, Collection -from cairo import ANTIALIAS_NONE from cairo import Context as CairoContext from gaphas.item import Item @@ -11,6 +10,13 @@ from gaphas.view import GtkView +# Colors from the GNOME Palette +RED_4 = (0.753, 0.110, 0.157) +ORANGE_4 = (0.902, 0.380, 0) +GREEN_4 = (0.180, 0.7608, 0.494) +BLUE_4 = (0.110, 0.443, 0.847) + + class HandlePainter: """Draw handles of items that are marked as selected in the view.""" @@ -33,10 +39,7 @@ def _draw_handles( assert model cairo.save() if not opacity: - opacity = 0.7 if item is view.selection.focused_item else 0.4 - - cairo.set_antialias(ANTIALIAS_NONE) - cairo.set_line_width(1) + opacity = 0.9 if item is view.selection.focused_item else 0.6 get_connection = model.connections.get_connection for h in item.handles(): @@ -44,28 +47,19 @@ def _draw_handles( continue # connected and not being moved, see HandleTool.on_button_press if get_connection(h): - r, g, b = 1.0, 0.0, 0.0 + color = RED_4 elif h.glued: - r, g, b = 1, 0.6, 0 + color = ORANGE_4 elif h.movable: - r, g, b = 0, 1, 0 + color = GREEN_4 else: - r, g, b = 0, 0, 1 + color = BLUE_4 vx, vy = cairo.user_to_device(*item.matrix_i2c.transform_point(*h.pos)) + cairo.set_source_rgba(*color, opacity) + + draw_handle(cairo, vx, vy) - cairo.save() - cairo.identity_matrix() - cairo.translate(vx, vy) - cairo.rectangle(-4, -4, 8, 8) - cairo.set_source_rgba(r, g, b, opacity) - cairo.fill_preserve() - cairo.set_source_rgba(r / 4.0, g / 4.0, b / 4.0, opacity * 1.3) - cairo.stroke() - if h.connectable: - cairo.rectangle(-2, -2, 4, 4) - cairo.fill() - cairo.restore() cairo.restore() def paint(self, items: Collection[Item], cairo: CairoContext) -> None: @@ -80,3 +74,31 @@ def paint(self, items: Collection[Item], cairo: CairoContext) -> None: hovered = selection.hovered_item if hovered and hovered not in selection.selected_items: self._draw_handles(hovered, cairo, opacity=0.25) + + +def draw_handle( + cairo: CairoContext, vx: float, vy: float, size: float = 12.0, corner: float = 2.0 +) -> None: + """Draw a handle with rounded corners.""" + radius = size / 2.0 + lower_right = size - corner + + pi_05 = 0.5 * 3.142 + pi = 3.142 + pi_15 = 1.5 * 3.142 + + cairo.save() + cairo.identity_matrix() + cairo.translate(vx - radius, vy - radius) + + cairo.move_to(0.0, corner) + cairo.arc(corner, corner, corner, pi, pi_15) + cairo.line_to(lower_right, 0.0) + cairo.arc(lower_right, corner, corner, pi_15, 0) + cairo.line_to(size, lower_right) + cairo.arc(lower_right, lower_right, corner, 0, pi_05) + cairo.line_to(corner, size) + cairo.arc(corner, lower_right, corner, pi_05, pi) + cairo.close_path() + cairo.fill() + cairo.restore() diff --git a/gaphas/segment.py b/gaphas/segment.py index 6cd3fc07..5b9c520a 100644 --- a/gaphas/segment.py +++ b/gaphas/segment.py @@ -2,12 +2,11 @@ from functools import singledispatch -from cairo import ANTIALIAS_NONE - from gaphas.connector import Handle, LinePort from gaphas.geometry import distance_point_point_fast from gaphas.item import Line, matrix_i2i from gaphas.model import Model +from gaphas.painter.handlepainter import GREEN_4, draw_handle from gaphas.selection import Selection from gaphas.solver import WEAK @@ -204,14 +203,5 @@ def paint(self, _items, cairo): cx = (p1.x + p2.x) / 2 cy = (p1.y + p2.y) / 2 vx, vy = cairo.user_to_device(*item.matrix_i2c.transform_point(cx, cy)) - cairo.save() - cairo.set_antialias(ANTIALIAS_NONE) - cairo.identity_matrix() - cairo.translate(vx, vy) - cairo.rectangle(-3, -3, 6, 6) - cairo.set_source_rgba(0, 0.5, 0, 0.4) - cairo.fill_preserve() - cairo.set_source_rgba(0.25, 0.25, 0.25, 0.6) - cairo.set_line_width(1) - cairo.stroke() - cairo.restore() + cairo.set_source_rgba(*GREEN_4, 0.4) + draw_handle(cairo, vx, vy, size=9.0, corner=1.5) From c5a93f33bac414782e559744b3f6c59cc333e725 Mon Sep 17 00:00:00 2001 From: Arjan Molenaar Date: Fri, 16 Dec 2022 14:23:52 +0100 Subject: [PATCH 2/4] Update cursor when hovering over handle --- gaphas/aspect/handlemove.py | 7 +------ gaphas/cursor.py | 32 ++++++++++++++++++++++++++++++ gaphas/guide.py | 4 ++-- gaphas/handlemove.py | 39 +------------------------------------ gaphas/tool/hover.py | 25 ++++++++++++++---------- gaphas/view/__init__.py | 2 +- gaphas/view/gtkview.py | 7 ------- 7 files changed, 52 insertions(+), 64 deletions(-) create mode 100644 gaphas/cursor.py diff --git a/gaphas/aspect/handlemove.py b/gaphas/aspect/handlemove.py index 0d8e659c..df0972b0 100644 --- a/gaphas/aspect/handlemove.py +++ b/gaphas/aspect/handlemove.py @@ -1,8 +1,3 @@ # flake8: noqa F401 from gaphas.connector import ConnectionSinkType -from gaphas.handlemove import ( - ElementHandleMove, - HandleMove, - ItemHandleMove, - item_at_point, -) +from gaphas.handlemove import HandleMove, ItemHandleMove, item_at_point diff --git a/gaphas/cursor.py b/gaphas/cursor.py new file mode 100644 index 00000000..9bd372c1 --- /dev/null +++ b/gaphas/cursor.py @@ -0,0 +1,32 @@ +from __future__ import annotations + +from functools import singledispatch + +from gi.repository import Gtk + +from gaphas.connector import Handle +from gaphas.item import Element, Item, Line + +DEFAULT_CURSOR = "left_ptr" if Gtk.get_major_version() == 3 else "default" + + +@singledispatch +def cursor(item: Item | None, handle: Handle | None) -> str: + return DEFAULT_CURSOR + + +ELEMENT_CURSORS = ("nw-resize", "ne-resize", "se-resize", "sw-resize") + + +@cursor.register +def element_hover(item: Element, handle: Handle | None) -> str: + index = item.handles().index(handle) + return ELEMENT_CURSORS[index] if index < 4 else DEFAULT_CURSOR + + +LINE_CURSOR = "fleur" if Gtk.get_major_version() == 3 else "move" + + +@cursor.register +def line_hover(item: Line, handle: Handle | None) -> str: + return LINE_CURSOR if handle else DEFAULT_CURSOR diff --git a/gaphas/guide.py b/gaphas/guide.py index 9352a86c..a8c1a797 100644 --- a/gaphas/guide.py +++ b/gaphas/guide.py @@ -8,7 +8,7 @@ from gaphas.canvas import all_children from gaphas.connector import Handle -from gaphas.handlemove import ElementHandleMove, HandleMove, ItemHandleMove +from gaphas.handlemove import HandleMove, ItemHandleMove from gaphas.item import Element, Item, Line from gaphas.move import ItemMove, Move from gaphas.types import Pos @@ -255,7 +255,7 @@ def stop_move(self, pos: Pos) -> None: @HandleMove.register(Element) -class GuidedElementHandleMove(GuidedItemHandleMoveMixin, ElementHandleMove): +class GuidedElementHandleMove(GuidedItemHandleMoveMixin, ItemHandleMove): pass diff --git a/gaphas/handlemove.py b/gaphas/handlemove.py index b88df619..e4c9267c 100644 --- a/gaphas/handlemove.py +++ b/gaphas/handlemove.py @@ -3,11 +3,9 @@ from operator import itemgetter from typing import Iterable, Optional, Sequence, Tuple -from gi.repository import Gdk, Gtk - from gaphas.connector import ConnectionSink, ConnectionSinkType, Connector from gaphas.handle import Handle -from gaphas.item import Element, Item +from gaphas.item import Item from gaphas.types import Pos from gaphas.view import GtkView @@ -112,41 +110,6 @@ def connect(self, pos: Pos) -> None: HandleMove = singledispatch(ItemHandleMove) -@HandleMove.register(Element) -class ElementHandleMove(ItemHandleMove): - CURSORS = ("nw-resize", "ne-resize", "se-resize", "sw-resize") - - def __init__(self, item: Item, handle: Handle, view: GtkView): - super().__init__(item, handle, view) - self.cursor: Optional[Gdk.Cursor] = None - - def start_move(self, pos: Pos) -> None: - super().start_move(pos) - self.set_cursor() - - def stop_move(self, pos: Pos) -> None: - self.reset_cursor() - super().stop_move(pos) - - def set_cursor(self) -> None: - index = self.item.handles().index(self.handle) - if index < 4: - display = self.view.get_display() - if Gtk.get_major_version() == 3: - cursor = Gdk.Cursor.new_from_name(display, self.CURSORS[index]) - self.cursor = self.view.get_window().get_cursor() - self.view.get_window().set_cursor(cursor) - else: - cursor = Gdk.Cursor.new_from_name(self.CURSORS[index]) - self.cursor = self.view.get_cursor() - self.view.set_cursor(cursor) - - def reset_cursor(self) -> None: - self.view.get_window().set_cursor( - self.cursor - ) if Gtk.get_major_version() == 3 else self.view.set_cursor(self.cursor) - - def item_distance( view: GtkView, pos: Pos, diff --git a/gaphas/tool/hover.py b/gaphas/tool/hover.py index c51cb063..0ccb77ed 100644 --- a/gaphas/tool/hover.py +++ b/gaphas/tool/hover.py @@ -1,10 +1,7 @@ -from typing import Optional +from gi.repository import Gdk, Gtk -from gi.repository import Gtk - -from gaphas.item import Item +from gaphas.cursor import cursor from gaphas.tool.itemtool import handle_at_point, item_at_point -from gaphas.types import Pos from gaphas.view import GtkView @@ -21,9 +18,17 @@ def hover_tool(view: GtkView) -> Gtk.EventController: def on_motion(ctrl, x, y): view = ctrl.get_widget() - view.selection.hovered_item = find_item_at_point(view, (x, y)) - - -def find_item_at_point(view: GtkView, pos: Pos) -> Optional[Item]: + pos = (x, y) item, handle = handle_at_point(view, pos) - return item or next(item_at_point(view, pos), None) # type: ignore[call-overload] + view.selection.hovered_item = item or next(item_at_point(view, pos), None) # type: ignore[call-overload] + set_cursor(view, cursor(item, handle)) + + +def set_cursor(view, cursor_name): + if Gtk.get_major_version() == 3: + display = view.get_display() + cursor = Gdk.Cursor.new_from_name(display, cursor_name) + view.get_window().set_cursor(cursor) + else: + cursor = Gdk.Cursor.new_from_name(cursor_name) + view.set_cursor(cursor) diff --git a/gaphas/view/__init__.py b/gaphas/view/__init__.py index 6293a63b..3480d7c7 100644 --- a/gaphas/view/__init__.py +++ b/gaphas/view/__init__.py @@ -2,4 +2,4 @@ from gaphas import model from gaphas.selection import Selection -from gaphas.view.gtkview import DEFAULT_CURSOR, GtkView +from gaphas.view.gtkview import GtkView diff --git a/gaphas/view/gtkview.py b/gaphas/view/gtkview.py index 93c13d42..4ce1f1c6 100644 --- a/gaphas/view/gtkview.py +++ b/gaphas/view/gtkview.py @@ -21,13 +21,6 @@ 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 - if Gtk.get_major_version() == 3 - else Gdk.Cursor.new_from_name("default") -) - # The tolerance for Cairo. Bigger values increase speed and reduce accuracy # (default: 0.1) PAINT_TOLERANCE = 0.8 From eea8583c694fcb0ac6909a95fc2218fe87fed8d9 Mon Sep 17 00:00:00 2001 From: Arjan Molenaar Date: Fri, 16 Dec 2022 14:51:35 +0100 Subject: [PATCH 3/4] Change cursor on line split handles --- gaphas/cursor.py | 13 ++++++++----- gaphas/segment.py | 20 ++++++++++++++++++++ gaphas/tool/hover.py | 12 ++++++++---- 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/gaphas/cursor.py b/gaphas/cursor.py index 9bd372c1..285b36ef 100644 --- a/gaphas/cursor.py +++ b/gaphas/cursor.py @@ -6,12 +6,13 @@ from gaphas.connector import Handle from gaphas.item import Element, Item, Line +from gaphas.types import Pos DEFAULT_CURSOR = "left_ptr" if Gtk.get_major_version() == 3 else "default" @singledispatch -def cursor(item: Item | None, handle: Handle | None) -> str: +def cursor(item: Item | None, handle: Handle | None, pos: Pos) -> str: return DEFAULT_CURSOR @@ -19,14 +20,16 @@ def cursor(item: Item | None, handle: Handle | None) -> str: @cursor.register -def element_hover(item: Element, handle: Handle | None) -> str: - index = item.handles().index(handle) - return ELEMENT_CURSORS[index] if index < 4 else DEFAULT_CURSOR +def element_hover(item: Element, handle: Handle | None, pos: Pos) -> str: + if handle: + index = item.handles().index(handle) + return ELEMENT_CURSORS[index] if index < 4 else DEFAULT_CURSOR + return DEFAULT_CURSOR LINE_CURSOR = "fleur" if Gtk.get_major_version() == 3 else "move" @cursor.register -def line_hover(item: Line, handle: Handle | None) -> str: +def line_hover(item: Line, handle: Handle | None, pos: Pos) -> str: return LINE_CURSOR if handle else DEFAULT_CURSOR diff --git a/gaphas/segment.py b/gaphas/segment.py index 5b9c520a..50b3e1f8 100644 --- a/gaphas/segment.py +++ b/gaphas/segment.py @@ -1,14 +1,18 @@ """Allow for easily adding segments to lines.""" +from __future__ import annotations + from functools import singledispatch from gaphas.connector import Handle, LinePort +from gaphas.cursor import DEFAULT_CURSOR, LINE_CURSOR, cursor, line_hover from gaphas.geometry import distance_point_point_fast from gaphas.item import Line, matrix_i2i from gaphas.model import Model from gaphas.painter.handlepainter import GREEN_4, draw_handle from gaphas.selection import Selection from gaphas.solver import WEAK +from gaphas.types import Pos @singledispatch @@ -205,3 +209,19 @@ def paint(self, _items, cairo): vx, vy = cairo.user_to_device(*item.matrix_i2c.transform_point(cx, cy)) cairo.set_source_rgba(*GREEN_4, 0.4) draw_handle(cairo, vx, vy, size=9.0, corner=1.5) + + +@cursor.register +def line_segment_hover(item: Line, handle: Handle | None, pos: Pos) -> str: + if not handle: + handles = item.handles() + if any( + distance_point_point_fast( + pos, ((h1.pos.x + h2.pos.x) / 2, (h1.pos.y + h2.pos.y) / 2) + ) + <= 4.5 + for h1, h2 in zip(handles, handles[1:]) + ): + return LINE_CURSOR + return DEFAULT_CURSOR + return line_hover(item, handle, pos) diff --git a/gaphas/tool/hover.py b/gaphas/tool/hover.py index 0ccb77ed..e1397da8 100644 --- a/gaphas/tool/hover.py +++ b/gaphas/tool/hover.py @@ -1,7 +1,7 @@ from gi.repository import Gdk, Gtk from gaphas.cursor import cursor -from gaphas.tool.itemtool import handle_at_point, item_at_point +from gaphas.tool.itemtool import find_item_and_handle_at_point from gaphas.view import GtkView @@ -19,9 +19,13 @@ def hover_tool(view: GtkView) -> Gtk.EventController: def on_motion(ctrl, x, y): view = ctrl.get_widget() pos = (x, y) - item, handle = handle_at_point(view, pos) - view.selection.hovered_item = item or next(item_at_point(view, pos), None) # type: ignore[call-overload] - set_cursor(view, cursor(item, handle)) + item, handle = find_item_and_handle_at_point(view, pos) + view.selection.hovered_item = item + + if item: + v2i = view.get_matrix_v2i(item) + pos = v2i.transform_point(x, y) + set_cursor(view, cursor(item, handle, pos)) def set_cursor(view, cursor_name): From adb908f94270dd41d56ae00c87781932276bb1f6 Mon Sep 17 00:00:00 2001 From: Arjan Molenaar Date: Fri, 16 Dec 2022 21:56:49 +0100 Subject: [PATCH 4/4] Use optional for singledispatch functions --- gaphas/cursor.py | 9 ++++----- gaphas/segment.py | 5 ++--- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/gaphas/cursor.py b/gaphas/cursor.py index 285b36ef..f367ba53 100644 --- a/gaphas/cursor.py +++ b/gaphas/cursor.py @@ -1,6 +1,5 @@ -from __future__ import annotations - from functools import singledispatch +from typing import Optional from gi.repository import Gtk @@ -12,7 +11,7 @@ @singledispatch -def cursor(item: Item | None, handle: Handle | None, pos: Pos) -> str: +def cursor(item: Optional[Item], handle: Optional[Handle], pos: Pos) -> str: return DEFAULT_CURSOR @@ -20,7 +19,7 @@ def cursor(item: Item | None, handle: Handle | None, pos: Pos) -> str: @cursor.register -def element_hover(item: Element, handle: Handle | None, pos: Pos) -> str: +def element_hover(item: Element, handle: Optional[Handle], pos: Pos) -> str: if handle: index = item.handles().index(handle) return ELEMENT_CURSORS[index] if index < 4 else DEFAULT_CURSOR @@ -31,5 +30,5 @@ def element_hover(item: Element, handle: Handle | None, pos: Pos) -> str: @cursor.register -def line_hover(item: Line, handle: Handle | None, pos: Pos) -> str: +def line_hover(item: Line, handle: Optional[Handle], pos: Pos) -> str: return LINE_CURSOR if handle else DEFAULT_CURSOR diff --git a/gaphas/segment.py b/gaphas/segment.py index 50b3e1f8..212f56e5 100644 --- a/gaphas/segment.py +++ b/gaphas/segment.py @@ -1,8 +1,7 @@ """Allow for easily adding segments to lines.""" -from __future__ import annotations - from functools import singledispatch +from typing import Optional from gaphas.connector import Handle, LinePort from gaphas.cursor import DEFAULT_CURSOR, LINE_CURSOR, cursor, line_hover @@ -212,7 +211,7 @@ def paint(self, _items, cairo): @cursor.register -def line_segment_hover(item: Line, handle: Handle | None, pos: Pos) -> str: +def line_segment_hover(item: Line, handle: Optional[Handle], pos: Pos) -> str: if not handle: handles = item.handles() if any(