From c4406cf38150cb6bd1951ae4fb938b544d38b84b Mon Sep 17 00:00:00 2001 From: Matthijs Date: Tue, 12 Apr 2022 20:49:02 +0200 Subject: [PATCH 1/6] FactTree: Remove action_row This was created and maintained, but not actually made visible, so effectively this code is unused. Remove it to avoid having to maintain it. Turns out this code was added in 2014 in commit 5a5f94fd but has been inactive and incomplete since it was first added. --- src/hamster/widgets/facttree.py | 37 +-------------------------------- 1 file changed, 1 insertion(+), 36 deletions(-) diff --git a/src/hamster/widgets/facttree.py b/src/hamster/widgets/facttree.py index 0304a8590..23f000553 100644 --- a/src/hamster/widgets/facttree.py +++ b/src/hamster/widgets/facttree.py @@ -33,20 +33,6 @@ from hamster.lib.fact import Fact -class ActionRow(graphics.Sprite): - def __init__(self): - graphics.Sprite.__init__(self) - self.visible = False - - self.restart = graphics.Icon("view-refresh-symbolic", size=18, - interactive=True, - mouse_cursor=gdk.CursorType.HAND1, - y=4) - self.add_child(self.restart) - - self.width = 50 # Simon says - - class TotalFact(Fact): """An extension of Fact that is used for daily totals. Instances of this class are rendered differently than instances @@ -308,9 +294,6 @@ def __init__(self): self.fact_row = FactRow() - self.action_row = ActionRow() - # self.add_child(self.action_row) - self.row_positions = [] self.row_heights = [] @@ -356,6 +339,7 @@ def on_mouse_down(self, scene, event): # Totals can't be selected elif not isinstance(self.hover_fact, TotalFact): self.set_current_fact(self.hover_fact) + self.redraw() def activate_row(self, day, fact): self.emit("on-activate-row", day, fact) @@ -445,11 +429,6 @@ def on_mouse_move(self, tree, event): hover_day = rec break - if hover_day != self.hover_day: - # Facts are considered equal if their content is the same, - # even if their id is different. - # redraw only cares about content, not id. - self.redraw() # make sure it is always fully updated, including facts ids. self.hover_day = hover_day @@ -459,22 +438,9 @@ def on_mouse_move(self, tree, event): hover_fact = fact break - if (hover_fact - and self.hover_fact - and hover_fact.id != self.hover_fact.id - ): - self.move_actions() # idem, always update hover_fact, not just if they appear different self.hover_fact = hover_fact - def move_actions(self): - if self.hover_fact: - self.action_row.visible = True - self.action_row.x = self.width - 80 - self.action_row.width - self.action_row.y = self.hover_fact.y - self.y - else: - self.action_row.visible = False - def _on_vadjustment_change(self, scene, vadjustment): if not self.vadjustment: return @@ -596,7 +562,6 @@ def on_scroll(self, scene=None, event=None): self.vadjustment.set_value(y_pos) self.y = y_pos - self.move_actions() self.redraw() self.visible_range = self.get_visible_range() From 8c46c78cc33c06ae4f27642f49323a3bfdf50d4f Mon Sep 17 00:00:00 2001 From: Matthijs Date: Wed, 13 Apr 2022 00:06:14 +0200 Subject: [PATCH 2/6] FactTree: Remove day argument from on-activate-row signal It was unused, and removing simplifies future refactoring. --- src/hamster/overview.py | 2 +- src/hamster/widgets/facttree.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/hamster/overview.py b/src/hamster/overview.py index 1af1b0b80..bc3eba7f6 100644 --- a/src/hamster/overview.py +++ b/src/hamster/overview.py @@ -559,7 +559,7 @@ def on_add_activity_clicked(self, button): def on_stop_clicked(self, button): self.storage.stop_tracking() - def on_row_activated(self, tree, day, fact): + def on_row_activated(self, tree, fact): self.present_fact_controller("edit", fact_id=fact.id) def on_row_delete_called(self, tree, fact): diff --git a/src/hamster/widgets/facttree.py b/src/hamster/widgets/facttree.py index 23f000553..45f3ae91a 100644 --- a/src/hamster/widgets/facttree.py +++ b/src/hamster/widgets/facttree.py @@ -273,7 +273,7 @@ class FactTree(graphics.Scene, gtk.Scrollable): __gsignals__ = { # enter or double-click, passes in current day and fact - 'on-activate-row': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT, gobject.TYPE_PYOBJECT)), + 'on-activate-row': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)), 'on-delete-called': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,)), } @@ -341,15 +341,15 @@ def on_mouse_down(self, scene, event): self.set_current_fact(self.hover_fact) self.redraw() - def activate_row(self, day, fact): - self.emit("on-activate-row", day, fact) + def activate_row(self, fact): + self.emit("on-activate-row", fact) def delete_row(self, fact): self.emit("on-delete-called", fact) def on_double_click(self, scene, event): if self.hover_fact and not isinstance(self.hover_fact, TotalFact): - self.activate_row(self.hover_day, self.hover_fact) + self.activate_row(self.hover_fact) def on_key_press(self, scene, event): # all keys should appear also in the Overview.on_key_press @@ -390,7 +390,7 @@ def on_key_press(self, scene, event): elif event.keyval == gdk.KEY_Return: if self.current_fact: - self.activate_row(self.hover_day, self.current_fact) + self.activate_row(self.current_fact) elif event.keyval == gdk.KEY_Delete: if self.current_fact: From 4db0d5aa3ec5629a77c430c26a4083118b64e620 Mon Sep 17 00:00:00 2001 From: Matthijs Date: Wed, 13 Apr 2022 00:20:39 +0200 Subject: [PATCH 3/6] FactTree: Remove Scene superclass This removes the lib.graphics.Scene superclass, since pretty much nothing of that class was used and it did complicate an upcoming refactor. Removing it makes FactTree a plain Gtk widget. Some of the code (e.g. managing width/height) is now duplicated from Scene, but this will be removed in an upcoming simplification anyway. --- src/hamster/widgets/facttree.py | 80 ++++++++++++++++++--------------- 1 file changed, 44 insertions(+), 36 deletions(-) diff --git a/src/hamster/widgets/facttree.py b/src/hamster/widgets/facttree.py index 45f3ae91a..99b4fde6d 100644 --- a/src/hamster/widgets/facttree.py +++ b/src/hamster/widgets/facttree.py @@ -248,7 +248,7 @@ def show(self, g, colors, fact=None, is_selected=False): g.restore_context() -class FactTree(graphics.Scene, gtk.Scrollable): +class FactTree(gtk.DrawingArea, gtk.Scrollable): """ The fact tree is a painter. It does not change facts by itself, only sends signals. @@ -283,7 +283,7 @@ class FactTree(graphics.Scene, gtk.Scrollable): vscroll_policy = gobject.property(type=gtk.ScrollablePolicy, default=gtk.ScrollablePolicy.MINIMUM) def __init__(self): - graphics.Scene.__init__(self, style_class=gtk.STYLE_CLASS_VIEW) + super().__init__() self.date_label = Label(10, 3) fontdesc = pango.FontDescription(graphics._font_desc) @@ -304,20 +304,26 @@ def __init__(self): self.hover_fact = None self.current_fact = None - self.style = self._style + self.colors = graphics.Colors + self.style = self.get_style_context() + self.style.add_class(gtk.STYLE_CLASS_VIEW) self.visible_range = None self.set_size_request(500, 400) - - self.connect("on-mouse-scroll", self.on_scroll) - self.connect("on-mouse-move", self.on_mouse_move) - self.connect("on-mouse-down", self.on_mouse_down) - - self.connect("on-resize", self.on_resize) - self.connect("on-key-press", self.on_key_press) + self.width = 0 + self.height = 0 + + self.set_can_focus(True) + self.set_events(gdk.EventMask.POINTER_MOTION_MASK + | gdk.EventMask.BUTTON_PRESS_MASK + | gdk.EventMask.SCROLL_MASK + | gdk.EventMask.KEY_PRESS_MASK) + self.connect("scroll-event", self.on_scroll) + self.connect("motion-notify-event", self.on_mouse_move) + self.connect("button-press-event", self.on_mouse_down) + self.connect("key-press-event", self.on_key_press) + self.connect("size-allocate", self.on_resize) self.connect("notify::vadjustment", self._on_vadjustment_change) - self.connect("on-enter-frame", self.on_enter_frame) - self.connect("on-double-click", self.on_double_click) @property def current_fact_index(self): @@ -325,21 +331,25 @@ def current_fact_index(self): facts_ids = [fact.id for fact in self.facts] return facts_ids.index(self.current_fact.id) - def on_mouse_down(self, scene, event): - self.on_mouse_move(None, event) - self.grab_focus() - if self.hover_fact: - # match either content or id - if (self.hover_fact == self.current_fact - or (self.hover_fact - and self.current_fact - and self.hover_fact.id == self.current_fact.id) - ): - self.unset_current_fact() - # Totals can't be selected - elif not isinstance(self.hover_fact, TotalFact): - self.set_current_fact(self.hover_fact) - self.redraw() + def on_mouse_down(self, widget, event): + if event.type == gdk.EventType.BUTTON_PRESS: + self.on_mouse_move(None, event) + self.grab_focus() + if self.hover_fact: + # match either content or id + if (self.hover_fact == self.current_fact + or (self.hover_fact + and self.current_fact + and self.hover_fact.id == self.current_fact.id) + ): + self.unset_current_fact() + # Totals can't be selected + elif not isinstance(self.hover_fact, TotalFact): + self.set_current_fact(self.hover_fact) + self.queue_draw() + elif event.type == gdk.EventType._2BUTTON_PRESS: + if self.hover_fact and not isinstance(self.hover_fact, TotalFact): + self.activate_row(self.hover_fact) def activate_row(self, fact): self.emit("on-activate-row", fact) @@ -347,11 +357,7 @@ def activate_row(self, fact): def delete_row(self, fact): self.emit("on-delete-called", fact) - def on_double_click(self, scene, event): - if self.hover_fact and not isinstance(self.hover_fact, TotalFact): - self.activate_row(self.hover_fact) - - def on_key_press(self, scene, event): + def on_key_press(self, widget, event): # all keys should appear also in the Overview.on_key_press # to be forwarded here even without focus. if event.keyval == gdk.KEY_Up: @@ -421,7 +427,7 @@ def get_visible_range(self): self.row_heights[start:end], self.days[start:end]))] - def on_mouse_move(self, tree, event): + def on_mouse_move(self, widget, event): hover_day, hover_fact = None, None for rec in self.visible_range: @@ -537,7 +543,9 @@ def set_row_heights(self): self.vadjustment.set_upper(max(maxy, self.height)) self.vadjustment.set_page_size(self.height) - def on_resize(self, scene, event): + def on_resize(self, widget, event): + self.width = event.width + self.height = event.height self.set_row_heights() self.fact_row.width = self.width - 105 self.on_scroll() @@ -562,11 +570,11 @@ def on_scroll(self, scene=None, event=None): self.vadjustment.set_value(y_pos) self.y = y_pos - self.redraw() + self.queue_draw() self.visible_range = self.get_visible_range() - def on_enter_frame(self, scene, context): + def do_draw(self, context): has_focus = self.get_toplevel().has_toplevel_focus() if has_focus: colors = { From db1ee0433d4c6e264c4b5bd8ee95c46d831930af Mon Sep 17 00:00:00 2001 From: Matthijs Date: Wed, 13 Apr 2022 01:24:44 +0200 Subject: [PATCH 4/6] FactTree: Search all days in on_mouse_move When finding the fact under the cursor, this now no longer looks at the list of currently visible days, but instead looks at all of them. This prepares for an upcoming refactoring where the list of visible days is no longer easily available. To ensure that this does not result in any slowdowns, this now uses bisect rather than looking at each day in turn. --- src/hamster/widgets/facttree.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/hamster/widgets/facttree.py b/src/hamster/widgets/facttree.py index 99b4fde6d..880f153d6 100644 --- a/src/hamster/widgets/facttree.py +++ b/src/hamster/widgets/facttree.py @@ -428,21 +428,18 @@ def get_visible_range(self): self.days[start:end]))] def on_mouse_move(self, widget, event): - hover_day, hover_fact = None, None + facts = [] + hover_fact = None - for rec in self.visible_range: - if rec['y'] <= event.y <= (rec['y'] + rec['h']): - hover_day = rec - break + y = event.y + candidate = bisect.bisect(self.row_positions, y) - 1 + if candidate >= 0 and y < self.row_positions[candidate] + self.row_heights[candidate]: + day, facts = self.days[candidate] - # make sure it is always fully updated, including facts ids. - self.hover_day = hover_day - - if self.hover_day: - for fact in self.hover_day.get('facts', []): - if (fact.y - self.y) <= event.y <= (fact.y - self.y + fact.height): - hover_fact = fact - break + for fact in facts: + if (fact.y - self.y) <= event.y <= (fact.y - self.y + fact.height): + hover_fact = fact + break # idem, always update hover_fact, not just if they appear different self.hover_fact = hover_fact From 76f291c2cbfc15a1e7dcd7a281aff276ca6deb06 Mon Sep 17 00:00:00 2001 From: Matthijs Date: Wed, 13 Apr 2022 00:20:39 +0200 Subject: [PATCH 5/6] FactTree: Simplify to fix touchpad scrolling This simplifies FactTree by no longer making it a gtk.Scrollable. This means that rather than having to manually do scrollbar management and adjust coordinates for drawing and input, FactTree will just set its size to fit all facts in the list and rely on its ScrolledWindow and Viewport containers to handle scrolling and simply change the drawingarea position to only show the relevant portion. Where the old code knew the scroll position exactly and used that to only draw visible facts, the new code just uses gtk's dirty clip rectangle for detecting what facts to draw. It turns out this is actually more efficient, since any content that was already visible before scrolling does not need to be redrawn now. The incentive for this change is that scrolling with a touchpad (and probably also a touch screen) did not work (at least on Wayland), because that produces "smooth scrolling" events that the old code did not handle. Rather than further increasing complexity by handling these, this just reduces complexity by deferring to Gtk default code, fixing touchpad scrolling in the process. --- src/hamster/widgets/facttree.py | 121 ++++++++++---------------------- 1 file changed, 36 insertions(+), 85 deletions(-) diff --git a/src/hamster/widgets/facttree.py b/src/hamster/widgets/facttree.py index 880f153d6..238b02237 100644 --- a/src/hamster/widgets/facttree.py +++ b/src/hamster/widgets/facttree.py @@ -248,7 +248,7 @@ def show(self, g, colors, fact=None, is_selected=False): g.restore_context() -class FactTree(gtk.DrawingArea, gtk.Scrollable): +class FactTree(gtk.DrawingArea): """ The fact tree is a painter. It does not change facts by itself, only sends signals. @@ -297,7 +297,6 @@ def __init__(self): self.row_positions = [] self.row_heights = [] - self.y = 0 self.day_padding = 20 self.hover_day = None @@ -310,20 +309,14 @@ def __init__(self): self.visible_range = None self.set_size_request(500, 400) - self.width = 0 - self.height = 0 self.set_can_focus(True) self.set_events(gdk.EventMask.POINTER_MOTION_MASK | gdk.EventMask.BUTTON_PRESS_MASK - | gdk.EventMask.SCROLL_MASK | gdk.EventMask.KEY_PRESS_MASK) - self.connect("scroll-event", self.on_scroll) self.connect("motion-notify-event", self.on_mouse_move) self.connect("button-press-event", self.on_mouse_down) self.connect("key-press-event", self.on_key_press) - self.connect("size-allocate", self.on_resize) - self.connect("notify::vadjustment", self._on_vadjustment_change) @property def current_fact_index(self): @@ -386,14 +379,6 @@ def on_key_press(self, widget, event): if self.facts: self.set_current_fact(self.facts[-1]) - elif event.keyval == gdk.KEY_Page_Down: - self.y += self.height * 0.8 - self.on_scroll() - - elif event.keyval == gdk.KEY_Page_Up: - self.y -= self.height * 0.8 - self.on_scroll() - elif event.keyval == gdk.KEY_Return: if self.current_fact: self.activate_row(self.current_fact) @@ -405,24 +390,33 @@ def on_key_press(self, widget, event): def set_current_fact(self, fact): self.current_fact = fact - if fact.y < self.y: - self.y = fact.y - if (fact.y + fact.height) > (self.y + self.height): - self.y = fact.y + fact.height - self.height + self.scroll_to(fact=fact) + self.queue_draw() - self.on_scroll() + def scroll_to(self, y=0, fact=None): + # If we are inside a scrollable viewport, that viewport will + # have a vadjustment property that stores the scroll position, + # so update that here. + parent = self.get_parent() + if parent and hasattr(parent, 'get_vadjustment'): + vadj = parent.get_vadjustment() + if fact is not None: + vadj.clamp_page(fact.y, fact.y + fact.height) + else: + vadj.set_value(y) + + self.queue_draw() def unset_current_fact(self): """Deselect fact.""" self.current_fact = None - self.on_scroll() + self.queue_draw() - def get_visible_range(self): - start, end = (bisect.bisect(self.row_positions, self.y) - 1, - bisect.bisect(self.row_positions, self.y + self.height)) + def get_visible_range(self, y0, y1): + start, end = (max(0, bisect.bisect(self.row_positions, y0) - 1), + bisect.bisect(self.row_positions, y1)) - y = self.y - return [{"i": start + i, "y": pos - y, "h": height, "day": day, "facts": facts} + return [{"i": start + i, "y": pos, "h": height, "day": day, "facts": facts} for i, (pos, height, (day, facts)) in enumerate(zip(self.row_positions[start:end], self.row_heights[start:end], self.days[start:end]))] @@ -437,18 +431,13 @@ def on_mouse_move(self, widget, event): day, facts = self.days[candidate] for fact in facts: - if (fact.y - self.y) <= event.y <= (fact.y - self.y + fact.height): + if fact.y <= y <= (fact.y + fact.height): hover_fact = fact break # idem, always update hover_fact, not just if they appear different self.hover_fact = hover_fact - def _on_vadjustment_change(self, scene, vadjustment): - if not self.vadjustment: - return - self.vadjustment.connect("value_changed", self.on_scroll_value_changed) - self.set_size_request(500, 300) def set_facts(self, facts, scroll_to_top=False): # FactTree adds attributes to its facts. isolate these side effects @@ -458,10 +447,8 @@ def set_facts(self, facts, scroll_to_top=False): # If we get an entirely new set of facts, scroll back to the top if scroll_to_top: - self.y = 0 + self.scroll_to(y=0) self.hover_fact = None - if self.vadjustment: - self.vadjustment.set_value(self.y) if self.facts: start = self.facts[0].date @@ -495,10 +482,10 @@ def set_facts(self, facts, scroll_to_top=False): if (self.current_fact and self.current_fact.id in (fact.id for fact in self.facts) ): - self.on_scroll() + self.scroll_to(fact=self.current_fact) else: - # will also trigger an on_scroll self.unset_current_fact() + self.queue_draw() def set_row_heights(self): """ @@ -509,9 +496,6 @@ def set_row_heights(self): This func creates a list of row start positions to be able to quickly determine what to display """ - if not self.height: - return - y, pos, heights = 0, [], [] for date, facts in self.days: @@ -532,44 +516,7 @@ def set_row_heights(self): y += height self.row_positions, self.row_heights = pos, heights - - maxy = max(y, 1) - - if self.vadjustment: - self.vadjustment.set_lower(0) - self.vadjustment.set_upper(max(maxy, self.height)) - self.vadjustment.set_page_size(self.height) - - def on_resize(self, widget, event): - self.width = event.width - self.height = event.height - self.set_row_heights() - self.fact_row.width = self.width - 105 - self.on_scroll() - - def on_scroll_value_changed(self, scroll): - self.y = int(scroll.get_value()) - self.on_scroll() - - def on_scroll(self, scene=None, event=None): - if not self.height: - return - y_pos = self.y - direction = 0 - if event and event.direction == gdk.ScrollDirection.UP: - direction = -1 - elif event and event.direction == gdk.ScrollDirection.DOWN: - direction = 1 - - y_pos += 15 * direction - if self.vadjustment: - y_pos = max(0, min(self.vadjustment.get_upper() - self.height, y_pos)) - self.vadjustment.set_value(y_pos) - self.y = y_pos - - self.queue_draw() - - self.visible_range = self.get_visible_range() + self.set_size_request(-1, max(y, 1)) def do_draw(self, context): has_focus = self.get_toplevel().has_toplevel_focus() @@ -588,20 +535,24 @@ def do_draw(self, context): "selected_bg": self.style.get_background_color(gtk.StateFlags.BACKDROP), } - if not self.height: - return + width = self.get_allocation().width + self.fact_row.width = width - 105 g = graphics.Graphics(context) g.set_line_style(1) g.translate(0.5, 0.5) - date_bg_color = self.colors.mix(colors["normal_bg"], colors["normal"], 0.15) - g.fill_area(0, 0, 105, self.height, date_bg_color) + # The clip region tells us what part needs to be redrawn. This + # also prevents drawing things that are outside of the scroll + # area. + x0, y0, x1, y1 = context.clip_extents() - y = int(self.y) + date_bg_color = self.colors.mix(colors["normal_bg"], colors["normal"], 0.15) + g.fill_area(0, y0, 105, (y1 - y0), date_bg_color) + g.fill_area(105, y0, width, (y1 - y0), colors["normal_bg"]) - for rec in self.visible_range: + for rec in self.get_visible_range(y0, y1): g.save_context() g.translate(0, rec['y']) g.set_color(colors["normal"]) From 9b557ab4a5b57e7fa4fd69036cf52178623297da Mon Sep 17 00:00:00 2001 From: Matthijs Date: Wed, 13 Apr 2022 01:39:30 +0200 Subject: [PATCH 6/6] FactTree: Remove mouse move handler Previously, the mouse move handler would figure out the fact under the cursor whenever it moved, filling the hover_day and hover_fact attributes. However, in practice hover_day was unused, and hover_fact only used when clicking, so this wasted resources for no good reason. This changes the mouse handler into a get_hover_fact() helper that is just called on from the mouse button down handler instead. --- src/hamster/widgets/facttree.py | 35 +++++++++++++-------------------- 1 file changed, 14 insertions(+), 21 deletions(-) diff --git a/src/hamster/widgets/facttree.py b/src/hamster/widgets/facttree.py index 238b02237..d318f5a0a 100644 --- a/src/hamster/widgets/facttree.py +++ b/src/hamster/widgets/facttree.py @@ -299,8 +299,6 @@ def __init__(self): self.day_padding = 20 - self.hover_day = None - self.hover_fact = None self.current_fact = None self.colors = graphics.Colors @@ -311,10 +309,8 @@ def __init__(self): self.set_size_request(500, 400) self.set_can_focus(True) - self.set_events(gdk.EventMask.POINTER_MOTION_MASK - | gdk.EventMask.BUTTON_PRESS_MASK + self.set_events(gdk.EventMask.BUTTON_PRESS_MASK | gdk.EventMask.KEY_PRESS_MASK) - self.connect("motion-notify-event", self.on_mouse_move) self.connect("button-press-event", self.on_mouse_down) self.connect("key-press-event", self.on_key_press) @@ -325,24 +321,25 @@ def current_fact_index(self): return facts_ids.index(self.current_fact.id) def on_mouse_down(self, widget, event): + hover_fact = self.get_hover_fact(event.y) + if event.type == gdk.EventType.BUTTON_PRESS: - self.on_mouse_move(None, event) self.grab_focus() - if self.hover_fact: + if hover_fact: # match either content or id - if (self.hover_fact == self.current_fact - or (self.hover_fact + if (hover_fact == self.current_fact + or (hover_fact and self.current_fact - and self.hover_fact.id == self.current_fact.id) + and hover_fact.id == self.current_fact.id) ): self.unset_current_fact() # Totals can't be selected - elif not isinstance(self.hover_fact, TotalFact): - self.set_current_fact(self.hover_fact) + elif not isinstance(hover_fact, TotalFact): + self.set_current_fact(hover_fact) self.queue_draw() elif event.type == gdk.EventType._2BUTTON_PRESS: - if self.hover_fact and not isinstance(self.hover_fact, TotalFact): - self.activate_row(self.hover_fact) + if hover_fact and not isinstance(hover_fact, TotalFact): + self.activate_row(hover_fact) def activate_row(self, fact): self.emit("on-activate-row", fact) @@ -421,22 +418,18 @@ def get_visible_range(self, y0, y1): self.row_heights[start:end], self.days[start:end]))] - def on_mouse_move(self, widget, event): + def get_hover_fact(self, y): facts = [] - hover_fact = None - y = event.y candidate = bisect.bisect(self.row_positions, y) - 1 if candidate >= 0 and y < self.row_positions[candidate] + self.row_heights[candidate]: day, facts = self.days[candidate] for fact in facts: if fact.y <= y <= (fact.y + fact.height): - hover_fact = fact - break + return fact - # idem, always update hover_fact, not just if they appear different - self.hover_fact = hover_fact + return None def set_facts(self, facts, scroll_to_top=False):